Reputation: 1058
I am working on a project in which I need to distribute the values of an array and the sum of these values must be 100.
Examples:
[70, 34, 92]
and the total value 100
. The output is [35.71, 17.35, 46.94]
because 35.71 + 17.35 + 46.94 = 100
.[86, 99.5, 100]
and the total value 100
. The output is [30.12, 34.85, 35.03]
.[96, 37]
and the total value 100
. The output is [72.18, 27.82]
.[98, 76.5, 68.5, 63.5, 38.5]
and the total value 100
. The output is [28.41, 22.17, 19.86, 18.41, 11.15]
= 100(sum).My solution so far:
.toFixed(2)
.The "formula/calculation": +(((valueOfArray * 100) / totalSumOfGivenArray).toFixed(2))
My first solution(problems with decimals so not acceptable):
prioritySum = 0;
controlsKey.forEach((value) => {
priorityVal = this.getDistributedValue(+form.controls[formId].value, totalCatSum); // return +(((valueOfArray * 100) / totalSumOfGivenArray).toFixed(2))
prioritySum = prioritySum + priorityVal;
});
This solution sometimes doesn't return exactly the 100 value. Output various such as 99.99, 99.999999, 100(mostly), 100.0000000001, 99.999978, 100.09999999, 99.000009. There are some issues with decimal numbers here.
The other approach I used is this:
let i = 0;
for(i = 0; i < controlsKey.length-1; i++){
let formId = controlsKey[i];
relValue = this.getDistributedValue(+form.controls[formId].value, totalActualSum); // returns formula result
form.controls[formId].setValue(relValue))
relativeSum = relativeSum + relValue;
}
relValue = 100 - relativeSum;
form.controls[controlsKey[i]].setValue(relValue)
This works fine but this solution hmmm...
So my question is there any elegant solution to this problem?
The first solution is ok for me but there are some issues with decimals even I used .toFixed(2)
Upvotes: 1
Views: 144
Reputation: 386610
To prevent unwanted floating point values, you could use integer values and subtract the value from a base value of 10000
(100 and two places) and create an array of strings where the dot is inserted into the value for getting formatted string.
const nice = s => s.toString().replace(/\d\d$/, '.$&');
function get100(array) {
var sum = array.reduce((a, b) => a + b),
offset = 1e4;
return array.map((v, j, { length }) => {
if (j + 1 === length) return nice(offset);
var i = Math.round(v * 1e4 / sum);
offset -= i;
return nice(i);
});
}
console.log(...get100([70, 34, 92])); // [35.71, 17.35, 46.94]
console.log(...get100([86, 99.5, 100])); // [30.12, 34.85, 35.03].
console.log(...get100([96, 37])); // [72.18, 27.82].
console.log(...get100([98, 76.5, 68.5, 63.5, 38.5])); // [28.41, 22.17, 19.86, 18.41, 11.15]
Upvotes: 1
Reputation: 64657
So the big issue is that there is an inherent inaccuracy with floating point numbers. If you don't use toFixed
the issue impossible to resolve - even if you determine the error, and pick an element and increase/decrease it by the amount of the error, you'll simply be off by that amount in the other direction, since a floating point number is not capable of holding the necessary amount of precision across all of the operations:
function getDistribution(values) {
var sum = values.reduce((carry, current) => carry + current, 0)
var distribution = values.map(v => 100 * v / sum)
var err = distribution.reduce((carry, current) => carry+current, 0) - 100;
console.log({distribution, err});
distribution[0] -= err;
err = distribution.reduce((carry, current) => carry+current, 0) - 100;
console.log({distribution, err});
}
getDistribution([98, 76.5, 68.5, 63.5, 38.5]);
Upvotes: 1