0

Assuming we have an array of values similar to [-0.6,-1.3,0.4,7.4,6.1,4.4,-0.1,0]; -1.3 being the lowest value, and 7.4 being the highest.

If we want to convert all the numbers in the array so that the lowest value then becomes 0, and the highest value becomes 140 (along with all the other numbers becoming relative to the smallest and largest). I.e:

[-0.6,-1.3,0.4,7.4]

would become

[... , 0, ... , 140]

How would we accomplish something like this?

Cheers.

Walter Tross
  • 12,237
  • 2
  • 40
  • 64
GROVER.
  • 4,071
  • 2
  • 19
  • 66

3 Answers3

2

Using vanilla JS.

First find the min and max values.

Each value will be subtracted from the min so that the values start at 0 i.e. -1.3 - (-1.3) = 0 To find the scale factor take the scaled max (140) and divide that by the difference between the max value and the min value. 140 / (7.4 - (-1.3)) = 16.091954...

You can then map each value using (x - minVal) * scale so that each value is mapped to a value between 0 and 140.

let src = [-0.6, -1.3, 0.4, 7.4];

const minMaxReducer = (acc, currVal) => {
  // check min value
  if (acc.min == null || currVal < acc.min)
    acc.min = currVal;

  // check max value
  if (acc.max == null || currVal > acc.max)
    acc.max = currVal;

  return acc;
}

// find the min and max values
var range = src.reduce(minMaxReducer, {
  min: null,
  max: null
});


let scale = 140 / (range.max - range.min);

// scale all values
let result = src.map(x => (x - range.min) * scale);

console.log(result);
phuzi
  • 12,078
  • 3
  • 26
  • 50
  • Your range could be simplified a bit, using reduce here seems overkill.. It is a one pass, but I've a feeling any performance advantage here would be removed with the extra branch instructions. So -> `var range = {min: Math.min(...src), max: Math.max(...src)}` would simplify things a tad. – Keith Aug 15 '18 at 12:33
  • @keith Would `Math.min()` and `Math.max()` not also branch in some way? – phuzi Aug 15 '18 at 12:35
  • @Keith Would be interesting to figure out which was more performant, single pass reducer or two pass using `Math.min` and `Math.max`. – phuzi Aug 15 '18 at 12:42
  • 1
    The single pass reducer is faster, by a fair bit. I've a feeling it's more likely the spread operator that is the performance killer here. So your minMaxReducer would be a useful addition to users libs.. :) Upvoting.. – Keith Aug 15 '18 at 13:19
  • @Keith however `Math.min.apply` and `Math.max.apply` seem to be significantly faster... https://jsperf.com/array-min-max-spread-apply – phuzi Aug 15 '18 at 13:43
  • 1
    according to the accepted answer to [this question](https://stackoverflow.com/questions/22747068/is-there-a-max-number-of-arguments-javascript-functions-can-accept), `Math.min.apply` and `Math.max.apply` can safely be passed arrays of 65535 elements. – Walter Tross Aug 15 '18 at 14:24
  • 1
    @phuzi Your JsPerf is slightly wrong -> `min: Math.min.apply(src),` should be -> `min: Math.min.apply(Math, src),` That might explain the speed difference, you able to update? – Keith Aug 15 '18 at 14:43
  • @Keith I'd love to but there appears to be a bug. Just getting "Not all tests inserted" – phuzi Aug 15 '18 at 17:44
  • 1
    @Keith Updated on JS bench http://jsben.ch/ISs2S running multiple times give different results, but looks like the single pass reduce is quicker. :) – phuzi Aug 15 '18 at 17:51
1

Do you want something like this?

const logic = (arr, from, to) => {
  let min = arr[0],
    max = arr[0];
  if (arr.length > 1)
    for (let i = 1; i < arr.length; i++) {
      let val = arr[i];
      if (val < min) min = val;
      if (val > max) max = val;
    }
  let diff = max - min;
  let scale = to / diff;
  return arr.map(x => (x - min) * scale + from)
}

let arr = [-0.6, -1.3, 0.4, 7.4];
let result = logic(arr, 0, 140);
console.log(result);

You can also add Math.floor to get rid of the float.

Clujio
  • 332
  • 1
  • 6
  • Yes, but i just want to give a algorithm example. Important part is the logic. – Clujio Aug 15 '18 at 12:23
  • Dangerous to use the initial 0 values for min and max.What if the range of values supplied are all greater than (or less than) 0? – phuzi Aug 15 '18 at 12:39
1

What you want is a linear function of the array values

  • new = a * old + b,

with the constraint that

  • newMax = a * oldMax + b and
  • newMin = a * oldMin + b

where oldMin and oldMax come from the array, while newMin and newMax are given (0 and 140 in your case.)

If you subtract these 2 equations, you get

  • newMax - newMin = a * (oldMax - oldMin)

i.e.

  • a = (newMax - newMin) / (oldMax - oldMin)

Replacing this in the second equation you get

  • newMin = (newMax - newMin) / (oldMax - oldMin) * oldMin + b

i.e.

  • b = newMin - (newMax - newMin) / (oldMax - oldMin) * oldMin

If we call oldRange = oldMax - oldMin and newRange = newMax - newMin, we can rewrite these as:

  • a = newRange / oldRange
  • b = newMin - newRange / oldRange * oldMin

and if we call scale = newRange / oldRange we can further simplify the notation:

  • a = scale
  • b = newMin - scale * oldMin

This for the algebra. Now the JavaScript:

let oldArr = [-0.6, -1.3, 0.4, 7.4, 6.1, 4.4, -0.1, 0];
let oldMin = Math.min.apply(null, oldArr);    
let oldMax = Math.max.apply(null, oldArr);    
let oldRange = oldMax - oldMin;
let newMin = 0;
let newMax = 140;
let newRange = newMax - newMin;
let scale = newRange / oldRange;
let a = scale;
let b = newMin - scale * oldMin;
let newArr = oldArr.map(x => a * x + b);
console.log(newArr);

Never mind that the new max is calculated as 139.99999999999997, you cannot have total precision with floating point.


Math.min.apply and Math.max.apply “should only be used for arrays with relatively few elements”, according to this (but according to the accepted answer to this question, you can safely pass arrays of up to 65535 elements). The alternative, without resorting to a library, is

let oldMin = oldArr.reduce((upToNow, current) => Math.min(upToNow, current));
let oldMax = oldArr.reduce((upToNow, current) => Math.max(upToNow, current));

The reduce method is only passed a 2-argument “reducer” function here (no initial value), so that on the first call the reducer will receive the first and second element of the array, instead of an undefined initial value and the first element.


Since some people are very keen on efficiency here (which I would be too, if I had evidence of huge arrays entering this algorithm), I'll also write down the fastest way of calculating the min and the max of an array at the same time. I'll write it as a function, for general use.

function arrayMinMax(arr) {
    return arr.reduce((minMax, cur) => cur < minMax[0] ? [cur, minMax[1]] :
                                      (cur > minMax[1] ? [minMax[0], cur] : minMax),
        [arr[0], arr[0]]);
}

let oldArr = [-0.6, -1.3, 0.4, 7.4, 6.1, 4.4, -0.1, 0];
let [oldMin, oldMax] = arrayMinMax(oldArr);
let oldRange = oldMax - oldMin;
let newMin = 0;
let newMax = 140;
let newRange = newMax - newMin;
let scale = newRange / oldRange;
let a = scale;
let b = newMin - scale * oldMin;
let newArr = oldArr.map(x => a * x + b);
console.log(newArr);

BTW, the kind of mapping you need often involves rounding to integer values. That would best be done at the same time as the linear transformation:

let newArr = oldArr.map(x => Math.round(a * x + b));
Walter Tross
  • 12,237
  • 2
  • 40
  • 64