Rob
Rob

Reputation: 7217

JavaScript - converting 3 numbers to percentage, doesn't yield 100% total

I have a scenario where I have three numbers:

  1. 17
  2. 10
  3. 90

I need to convert those into whole percentage values (so that when added, total 100% as you'd expect). I have this function:

function roundPercentageTotals(num1, num2, num3) {
    var total = num1 + num2 + num3;  // 117

    var num1Total = (num1 / total) * 100;  // 14.529914529914531
    var num2Total = (num2 / total) * 100;  //  8.547008547008546
    var num3Total = (num3 / total) * 100;  // 76.92307692307693

    var num1ToDecimal = num1Total.toFixed(1); // 14.5
    var num2ToDecimal = num2Total.toFixed(1); //  8.5
    var num3ToDecimal = num3Total.toFixed(1); // 76.9

    var totalPercentage = parseInt(num1ToDecimal) + parseInt(num2ToDecimal) + parseInt(num3ToDecimal); // 98

    return { percentage1: Math.round(num1ToDecimal, percentage2: Math.round(num2ToDecimal), percentage3: Math.round(num3ToDecimal) };
}

In my example, the total percentage calculated is 98%. Followed by:

  1. Percentage1 = 15
  2. Percentage2 = 9
  3. Percentage3 = 77

Which adds up to 101%, where am I going wrong?

Thanks for any help in advance!

Upvotes: 4

Views: 5033

Answers (5)

Simran Singh
Simran Singh

Reputation: 2869

Here is a simple solution with some checks, you can remove the checks if you want. In my case I'm not expecting a huge difference, So I throw an error if the difference is greater or lower than 0.9:

export function adjustPercentagesTo100(percentages: number[]) {
  const total = percentages.reduce((acc, curr) => acc + curr, 0)
  // If the sum is already 100, the original values is returned.
  if (total === 100) return percentages

  const diff = 100 - total
  // Error if the difference between the total and 100 is greater than 0.9.
  if (Math.abs(diff) > 0.9)
     throw new Error('The difference between the total and 100 is greater than 0.9. Please check the data.')

  // Find the highest value in the array.
  const highestValue = Math.max(...percentages)

  // Find the number of decimal values in the highest value, so the modified value can be adjusted to same number of decimal values
  const numberOfDecimalValuesInMax = highestValue.toString().split('.')[1]?.length || 1

  // Find the index of the highest value in the array.
  const index = percentages.indexOf(highestValue)

  // Adjust the highest value by adding or subtracting the difference
  percentages[index] = Number((highestValue + diff).toFixed(numberOfDecimalValuesInMax))

  return percentages
}

Upvotes: 1

Arootin Aghazaryan
Arootin Aghazaryan

Reputation: 848

percent-round is doing just that. did the job for me. just pass values and get percentages back that always add up to 100%.

const percentRound = require ('percent-round');
percentRound([16, 56, 18]); // [18, 62, 20]

Upvotes: 5

Rob
Rob

Reputation: 7217

Ok, so it looks like mathematically, I cannot achieve exactly what I was looking for. However, I needed to round figures up so it equalled 100% in the end (all be in that some of the figures where rounded, so not totally accurate).

Here's my solution, just in case this is useful to someone else:

function roundPercentageTotals(numArr) {

    // Total of all numbers passed.
    var total = numArr[0] + numArr[1] + numArr[2];

    // Percentage representations of each number (out of 100).
    var num1Percent = Math.round((numArr[0] / total) * 100);
    var num2Percent = Math.round((numArr[1] / total) * 100);
    var num3Percent = Math.round((numArr[2] / total) * 100);

    // Total percent of the 3 numbers combined (doesnt always equal 100%).
    var totalPercentage = num1Percent + num2Percent + num3Percent;

    // If not 100%, then we need to work around it by subtracting from the largest number (not as accurate but works out).
    if (totalPercentage != 100) {
        // Get the index of the largest number in the array.
        var index = getLargestNumInArrayIndex(numArr);

        // Take the difference away from the largest number.
        numArr[index] = numArr[index] - (totalPercentage - 100);

        // Re-run this method recursively, until we get a total percentage of 100%.
        return roundPercentageTotals(numArr);
    }

    // Return the percentage version of the array passed in.
    return [num1Percent, num2Percent, num3Percent];
}

function getLargestNumInArrayIndex(array) {
    return array.indexOf(Math.max.apply(Math, array));
}

Pass an array of the numbers into roundPercentageTotals, such as roundPercentageTotals([13,54,38]) and it will return the whole percentage (or nearest percentage I should say) figures in an array.

Upvotes: 6

Tyler Roper
Tyler Roper

Reputation: 21672

You're getting 98% in the first calculation because you're rounding the values down, and then getting 101% in your second because you're rounding them up.

Change your parseInt() to parseFloat() to get your totals to be closer to 100% instead of 98%. parseInt() floors decimals, it does not round them.

In regards to your second calculation totaling 101%: By rounding up 14.5 to 15, and 8.5 to 9, you've just added a full 1%. This leaves you with 101% instead of 100%.

The bottom line is that you cannot consistently achieve an even 100% if you're going to round the exact values, unless you fudge your percentages to fit somewhere along the way.

Upvotes: 6

Dion Zac
Dion Zac

Reputation: 69

You cannot convert those numbers in percentage without decimals. It will work only if the numbers are divided by 100. So the answere here must be (1. 14.5 , 2. 8.5 , 3. 76.9). And as you can see there is a "0.1" percent missing for the same reason of the decimals you threw (i.e by converting 14.529914529914531 to 14.5).

Upvotes: 1

Related Questions