How to create an arbitrary mapping in javascript? - javascript

I have a React app where I want to display lunar phases. To that end I tried to create an empty array and then map it appropriately:
import React from 'react'
export const MoonPhase = (props) => {
let start = new Date(props.start)
let end = new Date(props.end)
let delta = (end.getTime() - start.getTime()) / 1000
let quarters = new Array(Math.floor(
// Each lunar month is 2.55ᴇ6s.
(delta / (2.55 * Math.pow(10, 6))) * 4
))
return (
<div className='phases'>
{quarters.map((_, i) => {
switch(i % 4) {
case 0: return <span>🌑</span>
case 1: return <span>🌓</span>
case 2: return <span>🌕</span>
case 3: return <span>🌗</span>
}
})}
</div>
)
}
This doesn't produce anything, but if I change quarters to [1,2,3,4,5] I get output.
How should I approach this?

Related

JS - Calculate an interval for a given number

I am trying to generalize the following function that I have implemented:
/**
* Calculates an interval for the given age.
*
* #memberof module:Users/Functions
* #function getAgeInterval
* #param {number} age - The age of the user.
* #param {number} [minimumAge=18] - The minimum age.
* #param {number} [range=10] - The range.
* #throws {Error} The given age must be greater or equal than the minimum age.
* #returns {string} The age interval.
*/
export default (age, minimumAge = 18, range = 10) => {
if (age < minimumAge) {
throw new Error(
"The given age must be greater or equal than the minimum age.";
);
}
const start = Math.floor((age - 1) / range) * range + 1;
const end = start + range - 1;
const interval = `${Math.max(start, minimumAge)}-${end}`;
return interval;
};
Basically, in this method, I group the age of my users using a minimum age and a range. Here is an example:
const getAgeInterval = (age, minimumAge = 18, range = 10) => {
if (age < minimumAge) {
throw new Error(
"The given age must be greater or equal than the minimum age."
);
}
const start = Math.floor((age - 1) / range) * range + 1;
const end = start + range - 1;
const interval = `${Math.max(start, minimumAge)}-${end}`;
return interval;
};
//
// MAIN
//
for (let age = 18; age < 100; age += Math.round(Math.random() * 10)) {
console.log(`${age}: ${getAgeInterval(age)}`);
}
For now, the method is only working for "ages". But I suppose it is possible to make it work with any type of numbers, (i.e. the total followers counter of a user).
Users might have different number of followers, and I need to group it reusing the method I implemented. The output should look like:
0: "0-10"
100: "11-100"
999: "101-1000"
1117: "1001-10000"
9999: "1001-10000"
15201: "10001-100000";
1620620: "1000001-10000000"
As you can see, the only difference, in order to make it work, is the "dynamic" range. If you take a look at the output, the range goes from 10 to millions.
Any ideas? Any generic implementation to allow dynamic ranges?
UPDATE
Here is the generic method:
const calculateInterval = (counter, minimumCounter = 0, range = 10) => {
if (counter < minimumCounter) {
throw new Error(
"The given counter must be greater or equal than the minimum counter."
);
}
const start = Math.floor((counter - 1) / range) * range + 1;
const end = start + range - 1;
const interval = `${Math.max(start, minimumCounter)}-${end}`;
return interval;
};
//
// MAIN
//
const counters = [0, 100, 999, 1117, 9999, 15201, 1620620];
counters.forEach((totalFollowers) => {
console.log(`${totalFollowers}: ${calculateInterval(totalFollowers)}`);
});
//
// Q: HOW DO I MAKE THE RANGE DYNAMIC (BASED ON THE AMOUNT OF FOLLOWERS)?
//
OUTPUT MUST BE:
0: "0-10"
100: "11-100"
999: "101-1000"
1117: "1001-10000"
9999: "1001-10000"
15201: "10001-100000";
1620620: "1000001-10000000"
What you're looking for is called a logarithmic scale. In this case, the interval is not incremented but multiplied by the range in each step.
You can find the beginning of the range by raising r to the floor of the r-base logarithm of n-1, where r is the range and n is the number.
To get the edge cases right though, you need to make some adjustments (add one to the start of the range, add a default for values smaller or equal to the range, etc):
const baseNlog = (base, x) => Math.log(x) / Math.log(base)
const logarithmicInterval = (n, range = 10) => {
if(n <= range)
return `0-${range}`
const start = range ** Math.floor(baseNlog(range, n-1));
const end = start * range;
const interval = `${start + 1}-${end}`;
return interval;
};
//
// MAIN
//
console.log([
0,
1,
10,
11,
100,
999,
1117,
9999,
15201,
1620620
].map(e => `${e}: ${logarithmicInterval(e)}`))
What you can simply do is counting the number of digit in the number and creating your range using this.
For the low range it will be 10 ** (nbDigits - 1) + 1 (or 0 if the number is 0)
For the high range it will be 10 ** (nbDigits)
const calculateInterval = (number, minimumCounter = 0) => {
if (number < minimumCounter) {
throw new Error(
"The given counter must be greater or equal than the minimum counter."
);
}
const nbDigits = number > 0 ? (number - 1).toString().length : 1
const start = number > 0 ? 10**(nbDigits - 1) + 1 : 0
const end = 10 ** (nbDigits)
const interval = `${Math.max(start, minimumCounter)}-${end}`;
return interval;
};
//
// MAIN
//
const counters = [0, 100, 999, 1117, 9999, 15201, 1620620];
counters.forEach((totalFollowers) => {
console.log(`${totalFollowers}: ${calculateInterval(totalFollowers)}`);
});
One simple implementation could use a switch block to return a range string based on the length of the passed number.
Working snippet:
console.log(getRange(100))
console.log(getRange(999))
function getRange(num) {
numdigits= parseInt(num-1).toString().length;
switch(numdigits) {
case 1:
return "0..10"
case 2:
return "11..100"
case 3:
return "101..1000"
case 4:
return "1001..10000"
case 5:
return "10001..100000"
case 6:
return "100001..1000000"
case 7:
return "1000001..10000000"
default:
return "too large"
}
}// end function
Another approach could build the string by catenating zeros to the upper and lower limits in a loop iterating the length of the passed number times.
Like this:
console.log(getRange(100))
console.log(getRange(999))
function getRange(num) {
numdigits= parseInt(num-1).toString().length;
if (numdigits == 0) {return "0..10"}
else {
return `1${"0".repeat(numdigits-2)}..1${"0".repeat(numdigits)}`;
}
} // end function

useEffect EventListener problem REACT HOOKS

I was trying to make a simple Snake-Game with REACT. Everything was good till I needed to use useEffect to move "snake" when keydown fire.
When I try to put moveSnake() into useEffect it's giving me some error. When I move it and call it outside effect it's making an infinity loop. I'm using functional components. App.js is messed around because I was stressed out because it's not working and tried every single option. Hope you will get everything.
https://github.com/Pijano97/snake-game code is here, thanks.
Also if someone can not access that, here is the code.
The snake component just renders snakeDots with a map.
Food component just creating random food on the map.
import { useEffect, useState } from "react";
import "./App.css";
import Snake from "./Snake";
import Food from "./Food";
function App() {
const getRandomFoodDot = () => {
let min = 1;
let max = 98;
let x = Math.floor((Math.random() * (max - min + 1) + min) / 2) * 2;
let y = Math.floor((Math.random() * (max - min + 1) + min) / 2) * 2;
return [x, y];
};
const [snakeDots, setSnakeDots] = useState([
[0, 0],
[2, 0],
]);
const [foodDots, setFoodDots] = useState(getRandomFoodDot());
const [direction, setDirection] = useState("");
const moveSnake = () => {
let dots = [...snakeDots];
let head = dots[dots.length - 1];
switch (direction) {
case "UP":
head = [head[0], head[1] - 2];
break;
case "DOWN":
head = [head[0], head[1] + 2];
break;
case "LEFT":
head = [head[0] - 2, head[1]];
break;
case "RIGHT":
head = [head[0] + 2, head[1]];
break;
default:
break;
}
// adding new head
dots.push(head);
// removing last dot
dots.shift();
setSnakeDots(dots);
};
moveSnake();
useEffect(() => {
const keyPressHandler = (e) => {
switch (e.keyCode) {
case 38:
setDirection("UP");
break;
case 40:
setDirection("DOWN");
break;
case 37:
setDirection("LEFT");
break;
case 39:
setDirection("RIGHT");
break;
default:
setDirection("");
break;
}
};
document.addEventListener("keydown", keyPressHandler);
return () => {
document.removeEventListener("keydown", keyPressHandler);
};
}, []);
console.log(direction);
return (
<div className="app">
<div className="snake">
<Snake snakeDots={snakeDots} />
<Food foodDots={foodDots} />
</div>
</div>
);
}
export default App;
I am not sure if I got it correct, but probably you can try this:
import { useEffect, useState } from "react";
import "./App.css";
import Snake from "./Snake";
import Food from "./Food";
const getDirection = e => {
switch (e.keyCode) {
case 38:
return 'UP';
case 40:
return 'DOWN';
case 37:
return 'LEFT';
case 39:
return 'RIGHT';
default:
return '';
}
}
function App() {
const getRandomFoodDot = () => {
let min = 1;
let max = 98;
let x = Math.floor((Math.random() * (max - min + 1) + min) / 2) * 2;
let y = Math.floor((Math.random() * (max - min + 1) + min) / 2) * 2;
return [x, y];
};
const [snakeDots, setSnakeDots] = useState([
[0, 0],
[2, 0],
]);
const [foodDots, setFoodDots] = useState(getRandomFoodDot());
const [direction, setDirection] = useState({val: ''});
useEffect(() => {
let dots = [...snakeDots];
let head = dots[dots.length - 1];
switch (direction.val) {
case "UP":
head = [head[0], head[1] - 2];
break;
case "DOWN":
head = [head[0], head[1] + 2];
break;
case "LEFT":
head = [head[0] - 2, head[1]];
break;
case "RIGHT":
head = [head[0] + 2, head[1]];
break;
default:
break;
}
// adding new head
dots.push(head);
// removing last dot
dots.shift();
setSnakeDots(dots);
}, [direction]);
useEffect(() => {
const keyPressHandler = (e) => {
setDirection({ val: getDirection(e) });
};
document.addEventListener("keydown", keyPressHandler);
return () => {
document.removeEventListener("keydown", keyPressHandler);
};
}, []);
console.log(direction);
return (
<div className="app">
<div className="snake">
<Snake snakeDots={snakeDots} />
<Food foodDots={foodDots} />
</div>
</div>
);
}
export default App;
I just created getDirection func for getting direction from a key event and added one more useEffect with logic from the moveSnake function
I didn't know the current functionality of your project, but the code above works, and the shake can be moved.
The reason you're getting that error which says React limits the amount of re-renders to provent infinate re-renders is because you call moveSnake() inside your App() component, and moveSnake() calls setSnakeDots() which updates the state of snakeDots. When the state of snakeDots gets updated, your App component rerenders, which triggers the calling of moveSnake(), which calls setSnakeDots() which updates the state of your snakeDots which re-renders your App component. Your stuck in an inifinate loop here.
Something that might fix it(although I tested it and the game doesn't proceed past one step) would be setting your moveSnake() function in a timer like
function timer(){
setInterval(function() {
moveSnake();
timer()
}, 1000);
}
timer()

onChange event impacts String length (Sanitizer)

I am currently working on an input label, which formats and fixes an user input to a correct date-time format.
Currently, nothing except digits will be formatted to a date.
For instance: 11302020 => 11 / 30 / 2020
Now I want to set a range the string parts for day, month, year.
If a user exceeds the limit, the number (or part of the string) will be sanitized.
My function chops the input string into chunks, so I can write the values into a new array.
However, at the end my chopped array has a) size of 6 and b) an overall char length with blanks of 15. When I put these conditions in an if-question to save these values in separate parts, it starts saving at a char length of 16, which means, after an user enters the full date and an additional char, which is not what I want with my (b). Can someone help me out?
import React, { useState } from "react";
// export const dateFormatter = (input) => {
// return input;
// };
export default function App() {
const [maskedState, setMaskedState] = useState("");
const dateFormatter = (date) => {
setMaskedState(date.replace(/\D/g, "")
.replace(/(\d{2})(\d)/, " $1 / $2")
.replace(/(\d{2})(\d)/, "$1 / $2")
.replace(/(\d{4})\d+?$/, "$1"));
const maskedStateStr = maskedState.split(" ");
const charLength = 15;
const arrLength = 6;
if ((maskedStateStr.length === arrLength) && (maskedState.length === charLength)){
maskedStateStr.shift();
var day = maskedStateStr[0];
var month = maskedStateStr[2];
var year = maskedStateStr[4];
console.log(day,month,year);
}
//console.log(maskedStateStr, maskedStateStr.length, maskedState, maskedState.length)
}
const handleInput = (date) => {
const inputValue = date.target.value;
dateFormatter(inputValue);
};
return (
<div className="App">
<input onChange={handleInput} value={maskedState} />
</div>
);
}
There are so many issues with that code but I only focus on react part of it.
setMaskedState doesn't update maskedState immediately so maskedState will most likely point to a stale state.
setting value on an input renders it uneditable so you don't even see what you're typing. Use defaultValue.
That said you should operate on the date value and only set the state at the end of your block to reflect the result. Like:
export default function App() {
const [maskedState, setMaskedState] = useState(null);
const dateFormatter = value => {
let formattedDate = value
.replace(/\D/g, "")
.replace(/(\d{2})(\d)/, " $1 / $2")
.replace(/(\d{2})(\d)/, "$1 / $2")
.replace(/(\d{4})\d+?$/, "$1");
const maskedStateStr = formattedDate.split(" ");
const charLength = 15;
const arrLength = 6;
if (
maskedStateStr.length === arrLength &&
formattedDate.length === charLength
) {
maskedStateStr.shift();
let day = maskedStateStr[0];
let month = maskedStateStr[2];
let year = maskedStateStr[4];
setMaskedState(`${day} / ${month} / ${year}`);
} else {
setMaskedState(value);
}
};
const handleInput = date => {
const inputValue = date.target.value;
dateFormatter(inputValue);
};
return (
<div className="App">
<input onChange={handleInput} value={maskedState} />
<pre>
<code>
{maskedState
? JSON.stringify(maskedState, null, 2)
: "Not a valid date yet"}
</code>
</pre>
</div>
);
}
See the demo on StackBlitz

Search for closest date in dates array

I have a dates array in javascript
I want to search for a date and I dont expect it to be there
but I want to get the most closest date to it
so how can I do this ?
function search_date(date) {
var m = 1;
var dateFound;
var xdate = function (m) {
mh.data.find(function (d) {
if (Math.abs((new Date(d.x)).getTime() - date.getTime()) <= 30) {
return d;
} else {
if ((new Date(d.x)).getMinutes() - date.getMinutes() <= m) {
if (Math.abs((new Date(d.x)).getSeconds() - date.getSeconds()) <= 30)
return d;
}
}
});
};
var found = false;
while(!found){
if (xdate(m) != undefined){
dateFound = xdate(m);
break;
}else{
m++;
}
}
console.log(dateFound);
}
One possibility is to break this down into some reusable parts. We need to be able to calculate the difference in two dates, and we need to be able to find the minimum value of an array based on some calculation (in this case the difference from our target date.)
Here's an approach which does that:
const dates = [new Date("2018-02-18T20:24:33.000Z"), new Date("2018-02-18T20:45:02.000Z"), new Date("2018-02-18T21:22:50.000Z"), new Date("2018-02-18T22:06:31.000Z"), new Date("2018-02-18T22:47:18.000Z")]
const timeDiff = (first) => (second) => Math.abs(first - second)
const minBy = (fn, list) => list.reduce(
({d, val}, item) => fn(item) < d ? {val: item, d: fn(item)} : {d, val},
{d: Infinity, val: null}
).val
const closestDate = (dates) => (testDate) => minBy(timeDiff(testDate), dates)
console.log(closestDate(dates)(new Date('2018-02-18T16:52:35')))

React changing order of components does not trigger a re-render

I'm designing a system that shows when my Heroku apps have been updated. The problem I had was the parent component (Herokus) decided the order, but I wanted them to sort based on when they had been updated starting with the most recent. I was able to achieve this, by passing the dates from the Heroku component back to the parent Herokus component. I tried adding a console.log(this.state.apps) which shows the sorted order, but that is not reflected by the rendered component. My guess is react is not seeing the changed order of the components as a reason to update the view - but to be honest I have no clue. Any help is appreciated. Files are below. Sorry, my code is a mess, I rewrote it three times trying to fix this issue.
Herokus Component:
import React from 'react';
import Heroku from './Heroku';
export default class Herokus extends React.Component {
constructor(props) {
super(props);
var apps = [
'example-heroku-app'
]
this.state = {
apps: apps.map((h) => {
return {
app: h,
updatedAt: new Date()
};
})
}
}
sortApps() {
var sorted = this.state.apps.sort((a, b) => {
return a.updatedAt < b.updatedAt;
});
this.setState({
apps: sorted
});
}
updateParrent(data){
var apps = this.state.apps;
apps.find(x => x.app === data.app).updatedAt = data.updatedAt;
this.setState({
apps: apps
});
this.sortApps();
this.forceUpdate();
}
render() {
var s = this;
return (
<div>
{s.state.apps.map((app, i) => {
return (
<Heroku app={app.app} compact key={`heroku-${i}`} updateParrent={s.updateParrent.bind(s)}/>
);
})}
</div>
);
}
}
Heroku Component:
import React from 'react';
export default class Ping extends React.Component {
constructor(props) {
super(props);
this.state = {
app: props.app,
updatedAt: new Date()
};
}
componentDidMount() {
var s = this;
axios.get(`/api/heroku/app/${s.props.app}`, {
timeout: 20000,
})
.then(res => {
var data = res.data;
s.setState({
app: data.app.name,
updatedAt: new Date(data.app['updated_at'])
});
s.props.updateParrent(s.state);
setInterval(() => {
var updatedAt = new Date(s.state.updatedAt);
s.setState({
updatedAt: updatedAt
});
}, 1000);
});
}
howLongAgo(date) {
var ms = new Date() - date;
var seconds = ms / 1000;
var n = seconds;
var suffix = '';
// less than 1 minute
if(seconds < 60){
n = seconds;
suffix = 'second' + (n > 1.5 ? 's' : '');
}
// less than 1 hour
else if(seconds < 3600) {
n = seconds / 60
suffix = 'minute' + (n > 1.5 ? 's' : '');
}
// less than 1 day
else if(seconds < 86400) {
n = seconds / 3600;
suffix = 'hour' + (n > 1.5 ? 's' : '');
}
// less than 1 week
else if(seconds < 604800) {
n = seconds / 86400;
suffix = 'day' + (n > 1.5 ? 's' : '');
}
// more than 1 week
else {
n = seconds / 604800;
suffix = 'week' + (n > 1.5 ? 's' : '');
}
return `${Math.round(n)} ${suffix} ago`;
}
render() {
var s = this.state;
var self = this;
var output;
// COMPACT VIEW
if(this.props.compact){
output = (
<div className={'heroku heroku-compact'}>
<h3>{s.app}</h3>
<p>{self.howLongAgo(s.updatedAt)}</p>
<div className='clearfix'></div>
</div>
)
// FULL SIZE VIEW
} else{
output = ()
}
return output;
}
}
The problem seems to be that the order changes, but since the keys are based off the i index variable, the recorded list still has the same keys in the same order. React therefore does not see a difference.
Example:
Unsorted apps a, b, and c
<div key="1"> //app b
<div key="2"> //app a
<div key="3"> //app c
Sorted apps
<div key="1"> //app a
<div key="2"> //app b
<div key="3"> //app c
The apps are now in the right order, but the parent component Herokus still seems them with keys 1, 2, and 3 in the same order.
The solution:
Change the key to something unique to the individual Heroku components, but something that will remain unchanged unlike updatedAt. In this case the most logical choice is the app name.
This can be implemented in the Herokus component's render function like so:
export default class Herokus extends React.Component {
render() {
var s = this;
return (
<div>
{s.state.apps.map((app, i) => {
return (
<Heroku app={app.app} compact key={`heroku-${app.app}`} updateParrent={s.updateParrent.bind(s)}/>
);
})}
</div>
);
}
}

Categories