Find closest to the number inside array - javascript

I have below array
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200]
From the frontEnd user enters any number say it is
const number = 136
I need to find closest to the number but the lesser one.
So the output should be 125
Even if the number is 149 the output should be 125
How can I do this. I tried many way but could get the answer.
Thanks!!!

You can use Array.reduce for this
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200]
function getClosestNumber(d) {
return floorPerDayMilestones.reduce((a, b) => b <=d && a < b ? b : a, 0 )
}
console.log(getClosestNumber(135) || 'No lesser number available')
console.log(getClosestNumber(149) || 'No lesser number available')
console.log(getClosestNumber(22) || 'No lesser number available')

Maybe you should look at this: get closest number out of array
And in your foreach, save your closest number in a var. Then check if your number is bigger than the one in your array . if yes, take your last var, else continue your foreach

You can sort the array in ascending order and then find the index of number that is immediately higher than number then one position less of that value will be the immediate number that is less than number
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200]
const number = 136;
floorPerDayMilestones.sort((a,b)=> a-b);
var index = floorPerDayMilestones.findIndex(val => val>number);
var num = floorPerDayMilestones[index-1];
console.log(num);

Try this
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200];
const number = 136;
const nextLesser = floorPerDayMilestones.reduce((nl, curr) => (curr <= number) && (curr > nl) ? curr : nl , 0)
console.log(nextLesser)
Used Array.prototype.reduce

You can also filter out numbers greater than given and select max from this subset.
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200]
const number = 136
const filtered = floorPerDayMilestones.filter(el=>el<=number);
console.log(Math.max(...filtered))
Or, if you're already using lodash (only in this case - don't import lodash just to use solution below), you can do it with maxBy. maxBy treats numbers greater than number as null.
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200]
const number = 136
let result = _.maxBy(floorPerDayMilestones, el=>el<=number?el:null);
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>

With lodash you could use _.sortedIndex:
const numbers = [25, 50, 75, 100, 125, 150, 175, 200]
const closestNum = (arr, n) => {
let index = _.sortedIndex(arr, n)
return arr[index] == n ? arr[index] : arr[index-1]
}
console.log(closestNum(numbers, 135)) // 120
console.log(closestNum(numbers, 160)) // 150
console.log(closestNum(numbers, 180)) // 175
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
With JS you can simply use Array.reduceRight:
const numbers = [25, 50, 75, 100, 125, 150, 175, 200]
const closestNum = (arr, n) => arr.reduceRight((r,c) => !r && c < n ? c : r, 0)
console.log(closestNum(numbers, 135)) // 120
console.log(closestNum(numbers, 160)) // 150
console.log(closestNum(numbers, 180)) // 175
Since it will start from the right side all you care about is to find the first number less than your argument n.
You could also do Array.reverse and then just Array.filter (use Array.from if you do not want to mutate the array):
const numbers = [25, 50, 75, 100, 125, 150, 175, 200]
const closestNum = (arr, n) => arr.reverse().find(x => x < n)
// To not mutate use `Array.from`
// const closestNum = (arr, n) => Array.from(arr).reverse().find(x => x < n)
console.log(closestNum(numbers, 135)) // 120
console.log(closestNum(numbers, 160)) // 150
console.log(closestNum(numbers, 180)) // 175

If it's sorted in ascending order like in the question this should work.
const floorPerDayMilestones = [25, 50, 75, 100, 125, 150, 175, 200];
const number = 136;
function findClosest(arr, num) {
for (let i = 0; i < arr.length; ++i) {
if (arr[i] > num) {
return arr[i - 1];
}
}
}
console.log(findClosest(floorPerDayMilestones,number));

Related

Distribute an array of values to another array of values

I have a logic problem. I need to distribute an array of values evenly to another array of values. To illustrate:
const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250]
const withColors = plots.map(e => ({
value: e, color: ???
}))
/* expected output:
[
{value: 0, color: 'green'},
{value: 10, color: 'green'},
{value: 40, color: 'yellow'},
{value: 90, color: 'yellow'},
{value: 150, color: 'red'},
{value: 230, color: 'red'},
{value: 250, color: 'red'},
]
*/
current solution, I definitely have no idea yet, and I will update my question as I am currently brainstorming how to solve this.
Here's a brute force method where I iterate through the array and give each third of the plots a color.
Another way would be to write a helper function that handles turning the values of i/plots.length into 0, 1, or 2 and return that.
const colors = ['green', 'yellow', 'red'];
const plots = [0, 10, 40, 90, 150, 230, 250];
const result = {};
for (var i=0; i<plots.length; i++) {
var thisPlot = plots[i];
if (i / plots.length < 1/3.0) {
result[thisPlot] = colors[0];
}
else if (i / plots.length < 2/3.0) {
result[thisPlot] = colors[1];
}
else {
result[thisPlot] = colors[2];
}
}
console.log(result);
Can't see any logic behind this how this pattern will grow in future, but certainly you can try something like below.
// const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250]
function getColors(val) {
if(val >= 150 && val <=250) return "red";
if(val >= 40 && val <= 90) return "yellow";
return "green"
}
const withColors = plots.map(e => ({
value: e, color: getColors(e)
}))
Assigning colors one by one to each index
This will spread colors evenly, and is the simplest algorithm, but does not keep the colors grouped as in your example.
const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250, 260]
const withColors = plots.map((plot, i) => ({ value: plot, color: colors[i % colors.length]}));
console.log(withColors);
Keeping colors grouped
Just calculate the number of plots to map to each color, and assign each color to the appropriate number of plots.
One caveat: We require plots per color to be at least one in the case that there are less plots than colors. In this case, some colors will not be included.
There is one thing you haven't specified: what to do with the remainder. In your example plots.length / colors.length == 7 / 3 is 2 with a remainder of 1. In other words, 2 plots per color, and 1 plot left over. Having a remainder of 1 is easy: just assign any color to an extra plot. But what about larger remainders? There's a few strategies.
Assigning the last color to all trailing plots
Just round down plots per color and keep using the last color for any extra plots.
const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250, 260]
const plotsPerColor = Math.max(1, Math.floor(plots.length / colors.length));
let colorIdx = 0;
let count = 0;
const withColors = plots.map((plot) => {
const result = { value: plot, color: colors[colorIdx]};
if (++count === plotsPerColor && colorIdx < colors.length-1) {
colorIdx++;
count = 0;
}
return result;
})
console.log(withColors);
Spreading remainder evenly at end
Round down plots per color and increase it by one at the appropriate time.
const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250, 260]
let plotsPerColor = Math.max(1, Math.floor(plots.length / colors.length));
let remainder = plots.length % colors.length;
let colorIdx = 0;
let count = 0;
const withColors = plots.map((plot) => {
const result = { value: plot, color: colors[colorIdx]};
if (++count === plotsPerColor) {
if (++colorIdx === colors.length - remainder) plotsPerColor++;
count = 0;
}
return result;
});
console.log(withColors);
Spreading remainder evenly at beginning
Round up plots per color and decrease it by one at the appropriate time.
const colors = ['green', 'yellow', 'red']
const plots = [0, 10, 40, 90, 150, 230, 250, 260]
let plotsPerColor = Math.ceil(plots.length / colors.length);
let remainder = plots.length % colors.length;
let colorIdx = 0;
let count = 0;
const withColors = plots.map((plot) => {
const result = { value: plot, color: colors[colorIdx]};
if (++count === plotsPerColor) {
if (++colorIdx === remainder) plotsPerColor--;
count = 0;
}
return result;
});
console.log(withColors);

Round off number to the nearest Highest number from array besides last one

var counts = [2, 33, 61, 92, 125, 153, 184, 215, 245, 278, 306, 335, 365],
goal = 35;
let min = Math.min(...counts.filter(num => num >= goal));
console.log(min)
This works but in case goal=400, I will get 365 back as it's the last number in the array
You could find the wanted value or take the value at last index.
const
find = (array, value) => array.find((v, i, { length }) =>
v >= value || i + 1 === length
),
counts = [2, 33, 61, 92, 125, 153, 184, 215, 245, 278, 306, 335, 365];
console.log(find(counts, 35));
console.log(find(counts, 400));
A slightly different approach by taking the last item, if undefined.
const
find = (array, value) => array.find(v => v >= value) ?? array.at(-1),
counts = [2, 33, 61, 92, 125, 153, 184, 215, 245, 278, 306, 335, 365];
console.log(find(counts, 35));
console.log(find(counts, 400));
This will give you the smallest number above the goal, or the largest number in counts if there are no numbers above the goal
const counts = [2,33,61,92,125,153,184,215,245,278,306,335,365]
goal = 35;
const above_goal = counts.filter(num => num >= goal)
const min = above_goal.length === 0 ? Math.max(counts) : Math.min(above__goal)
console.log(min)

Group array base on maximum and length

Having a hard time to group this array. any suggestions.
As an example I have an array var a = [10, 100, 20, 50, 20, 50, 70, 120]
and I have a maximum of 150 and minimum length of 3 i.e each sub array can have a total maximum sum of 150 and a maximum length of 3
any suggestion to make it like this [[10, 100, 20], [50, 20, 50], [70], [120]]
thanks in advance
Here you go, the groupArray function will iterate on your input and build groups based on max length and max sum provided.
function groupArray(input, maxSum, maxLen) {
const res = [[]];
let mark = 0;
let sum = 0;
input.forEach( ele => {
// if the current group has already reach maxLenght or maxSum
// then create a new group
if ( res[mark].length > (maxLen-1)
|| (sum + ele) > maxSum ) {
res.push([ele]);
mark++;
sum = ele;
}
// otherwise add to current grop
else {
res[mark].push(ele);
sum += ele;
}
});
return res;
}
const test_1 = [10, 100, 20, 50, 20, 50, 70, 120];
const test_2 = [10, 130, 20, 50, 20, 50, 70, 120];
const test_3 = [140, 110, 20, 50, 20, 50, 70, 120];
console.log(groupArray(test_1, 150, 3));
console.log(groupArray(test_2, 150, 3));
console.log(groupArray(test_3, 150, 3));
Note: Since the question did not have any additional rules, this function does not reorder the array or try to look for the best possible length match or best possible sum matches.

Mapping array of integers to an array of integres

I have a slider that has the following raw snap points:
[-100, -200, -300, -400, -500, -600]
And I would like to convert the sliding value to match the following snap points:
[0, 5, 10, 25, 50, 100]
A raw value in [-100, -200) should be mapped to a value in [0, 5)
A raw value in [-200, -300) should be mapped to a value in [5, 10)
A raw value in [-300, -400) should be mapped to a value in [10, 25)
And so on ..
How can I achieve that?
Edit: added my attempt (different raw values though)
// sliderValue is an integer obtained from the slider
const base = -70
const offset = -80
const limits = [
base + offset * 0, // -70
base + offset * 1, // -150
base + offset * 2, // -230
base + offset * 3, // -310
base + offset * 4, // -390
base + offset * 5, // -470
]
const points = [0, 5, 10, 25, 50, 100]
// I can't even begin to make sense of this
// don't know I came up with it, but it works ¯\_(ツ)_/¯
if (sliderValue <= limits[4]) {
percentage = scaleValue(sliderValue, limits[4], limits[5], 50, 100)
} else if (sliderValue <= limits[3]) {
percentage = scaleValue(sliderValue, limits[3], limits[4], 25, 50)
} else if (sliderValue <= limits[2]) {
percentage = scaleValue(sliderValue, limits[2], limits[3], 10, 25)
} else if (sliderValue <= limits[1]) {
percentage = scaleValue(sliderValue, limits[1], limits[2], 5, 10)
} else if (sliderValue <= limits[0]) {
percentage = scaleValue(sliderValue, limits[0], limits[1], 0, 5)
}
console.log(percentage)
// ..
function scaleValue(num, in_min, in_max, out_min, out_max) {
return ((num - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min
}
You could take a function with a look up for the section. Then build the new value, based on the four values as a linear function.
function getValue(x) {
var a = [-100, -200, -300, -400, -500, -600],
b = [0, 5, 10, 25, 50, 100],
i = a.findIndex((v, i, a) => v >= x && x >= a[i + 1]);
return [x, (x -a[i])* (b[i + 1] - b[i]) / (a[i + 1] - a[i]) +b[i]].join(' ');
}
console.log([-100, -150, -200, -250, -300, -350, -400, -450, -500, -550, -600].map(getValue));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Simple linear equation: add 100, divide by 20, then negate.
UPDATE: Due to early-morning eye bleariness, I misread the question. (Sorry!) The general method for mapping linear relations to each other is to figure out the offset of the two sets and the scale factor.
I can't find a smooth relationship between the example points you gave, so I'm not sure how to find a single equation that would neatly and continuously map the points to each other. It looks like your solution (you said it works) might be the best: figure out which range each value maps to, and scale correspondingly.
You can just map the values:
var mapping = {
"-100": 0,
"-200": 5,
"-300": 10,
"-400": 25,
"-500": 50,
"-600": 100
}
function map_values(array){
return [mapping[array[0]], mapping[array[1]]];
}
var input = [-200,-300];
console.log(map_values(input));

Optimal algorithm for segmenting set of integers into labels for a chart axis?

Say you get values anywhere from 0 to 1,000,000,000, and you want to plot 30 days. So one particular chart may have a set like:
[ 1, 465, 123, 9, ... ]
While another chart can have a set with much larger numbers:
[ 761010, 418781, ... ]
Is there an "optimal algorithm" that can take those values and segment them into "clean" numbers? Sorry for the wording, don't know the right terminology, I will try to explain.
By "optimal algorithm", I mean both in terms of minimum number of computational steps, given that it creates labels (say for the y-axis) that are simplest from a human perspective.
For example, say you always want to divide the y-axis into 5 labels. You could do this:
var max = Math.max.apply(Math, values); // 465 (from the first set of values)
var interval = max / 5;
var labels = [ interval * 0, interval * 1, interval * 2, ... ];
But that creates labels like:
[ 0, 93, 186, ... ]
And that would be complex for humans to understand. What would be better (but still not ideal) is to create labels like:
[ 0, 125, 250, 375, 500 ]
But that's still to specific. Somehow it should figure out that a better segmentation is:
[ 0, 200, 400, 600, 800 ]
That way, it's divided into more intuitive chunks.
Is there a standard way to solve this problem? What algorithm works best?
Some maths
var getLabelWidth = function(sep, max_value){
var l = (""+max_value).length;
var av = max_value/sep/Math.pow(10,l-2); // get the length max 2 digit
/// 15.22
var width = (Math.ceil(av)*Math.pow(10,l-2)); // do a ceil on the value retrieved
// and apply it to the width of max_value.
// 16 * 10 000
return width;
}
console.log(getLabelWidth(2,59)); // 30 : [0, 30, 60]
console.log(getLabelWidth(2,100)); // 50 : [0, 50, 100]
console.log(getLabelWidth(2,968)); // 490 : [0, 490, 980]
console.log(getLabelWidth(3,368)); // 130 : [0, 130, 260, 390]
console.log(getLabelWidth(3,859)); // 290 : [0, 290, 580, 870]
console.log(getLabelWidth(3,175)); // 60 : [0, 60, 120, 180]
console.log(getLabelWidth(3,580)); // 200 : [0, 200, 400, 600]
console.log(getLabelWidth(3,74)); // 25 : [0, 25, 50, 75]
console.log(getLabelWidth(4,1111)); // 300 :[0, 300, 600, 900, 1200]
console.log(getLabelWidth(4,761010)); // 200 000: [0, 200000, 400000, 600000, 800000]
It could be improved a little bit i guess,
sorry for my bad english .
For reference, here's what I ended up doing.
function computeLabels(count, max) {
var magnitude = orderOfMagnitude(max);
var multiplier = magnitude * count;
// 1
if (multiplier >= max) return buildLabels(count, multiplier);
// 2
multiplier *= 2;
if (multiplier >= max) return buildLabels(count, multiplier);
// 5
multiplier *= 5;
if (multiplier >= max) return buildLabels(count, multiplier);
// 10, don't think it will ever get here but just in case.
multiplier *= 10;
if (multiplier >= max) return buildLabels(count, multiplier);
}
function buildLabels(count, multiplier) {
var labels = new Array(count);
while (count--) labels[count] = formatLabel(count * multiplier);
return labels;
}
function formatLabel(value) {
if (value > 10e5) return (value / 10e5) + 'M'; // millions
if (value > 10e2) return (value / 10e2) + 'K'; // thousands
return value; // <= hundreds
}
function orderOfMagnitude(val) {
var order = Math.floor(log10(val) + 0.000000001);
return Math.pow(10, order);
}
After drawing it out on paper, the "desirable" labels seemed to follow a simple pattern:
Find the max value in the set.
Get the order of magnitude for it.
Multiply the order of magnitude by the number of ticks.
Iterate: If that previous calculation is greater than the max value, then use it. Otherwise, multiply the value times 2 and check. If not, try times 5. So the pattern is, 1, 2, 5.
This gives you labels that are like:
10, 20 (2 ticks)
20, 40
50, 100
100, 200
200, 400
500, 1000
...
10, 20, 30 (3 ticks)
20, 40, 60
50, 100, 150 (don't like this one too much but oh well)
100, 200, 300
10, 20, 30, 40 (4 ticks)
...
It seems like it can be improved, both in producing better quality "human readable" labels, and in using more optimized functionality, but don't quite see it yet. This works for now.
Would love to know if you find a better way!

Categories

Resources