Percentage Calculator: JavaScript Floating-Point Precision Traps and Solutions#

Last week a colleague asked me: “Why doesn’t 0.1 + 0.2 equal 0.3?” I smiled, thinking it was just that classic floating-point issue. But when I started building a percentage calculator, I found way more pitfalls than expected.

The Root Cause of Floating-Point Issues#

JavaScript uses IEEE 754 double-precision floating-point numbers, storing numbers in 64 bits:

Sign bit (1) + Exponent bits (11) + Mantissa bits (52)

The problem is that many decimal fractions can’t be precisely represented in binary. Take 0.1:

0.1.toString(2)
// "0.0001100110011001100110011001100110011001100110011001101"

It’s an infinitely repeating binary fraction. When stored, it gets truncated, causing:

0.1 + 0.2  // 0.30000000000000004
0.1 * 3    // 0.30000000000000004
0.1 * 0.2  // 0.020000000000000004

Five Common Percentage Calculations#

A complete percentage calculator needs to handle five scenarios:

1. Percentage Of#

“What is 25% of 200?”

function percentageOf(percent, value) {
  return (percent / 100) * value
}

percentageOf(25, 200)  // 50

Looks fine? Try this:

percentageOf(33.33, 300)  // 99.99000000000001

Users expect 99.99, not 99.99000000000001.

2. What Percent#

“50 is what percent of 200?”

function whatPercent(part, total) {
  return (part / total) * 100
}

whatPercent(50, 200)  // 25
whatPercent(1, 3)     // 33.33333333333333

Now we have infinite repeating decimals.

3. Increase#

“What is 100 increased by 20%?”

function increase(value, percent) {
  return value * (1 + percent / 100)
}

increase(100, 20)    // 120
increase(70, 33.33)  // 93.33100000000002

4. Decrease#

“What is 100 decreased by 20%?”

function decrease(value, percent) {
  return value * (1 - percent / 100)
}

decrease(100, 20)    // 80
decrease(100, 33.33) // Precision issues again

5. Percent Change#

“What’s the percentage change from 100 to 120?”

function changeRate(oldValue, newValue) {
  return ((newValue - oldValue) / oldValue) * 100
}

changeRate(100, 120)  // 20
changeRate(80, 100)   // 25

Solution Attempt: toFixed() Pitfalls#

First instinct is to use toFixed():

(0.1 + 0.2).toFixed(2)  // "0.30"

But there are two issues:

Issue 1: Returns a String#

typeof (0.1).toFixed(2)  // "string"

Need to convert back:

parseFloat((0.1 + 0.2).toFixed(2))  // 0.3

Issue 2: Banker’s Rounding#

toFixed() uses banker’s rounding (round half to even):

(1.005).toFixed(2)  // "1.00" not "1.01"!
(2.5).toFixed(0)    // "2" not "3"

This is incorrect for financial calculations.

Better Solution: Scale to Integers#

Scale decimals to integers, perform the operation, then scale back:

function preciseMultiply(a, b) {
  const decimalsA = (a.toString().split('.')[1] || '').length
  const decimalsB = (b.toString().split('.')[1] || '').length
  const multiplier = Math.pow(10, Math.max(decimalsA, decimalsB))
  
  return (a * multiplier * b * multiplier) / (multiplier * multiplier)
}

preciseMultiply(0.1, 0.2)     // 0.02 ✓
preciseMultiply(33.33, 300)   // 9999 ✓

Precise Percentage Calculations#

function precisePercentageOf(percent, value) {
  // percent / 100 * value
  // Avoid division before multiplication for better precision
  return (percent * value) / 100
}

precisePercentageOf(33.33, 300)  // 99.99 ✓

A more complete solution:

function safeRound(num, decimals = 4) {
  const multiplier = Math.pow(10, decimals)
  // Add Number.EPSILON to correct floating-point errors
  return Math.round(num * multiplier + Number.EPSILON) / multiplier
}

const PercentageCalculator = {
  percentageOf(percent, value) {
    return safeRound((percent * value) / 100)
  },
  
  whatPercent(part, total) {
    if (total === 0) return 0
    return safeRound((part / total) * 100)
  },
  
  increase(value, percent) {
    return safeRound(value * (1 + percent / 100))
  },
  
  decrease(value, percent) {
    return safeRound(value * (1 - percent / 100))
  },
  
  changeRate(oldValue, newValue) {
    if (oldValue === 0) return newValue > 0 ? Infinity : 0
    return safeRound(((newValue - oldValue) / oldValue) * 100)
  }
}

Edge Cases#

Division by Zero#

whatPercent(50, 0)    // Infinity or NaN?
changeRate(0, 100)    // Infinity
changeRate(0, 0)      // NaN

Handle gracefully:

function safeWhatPercent(part, total) {
  if (total === 0) {
    return part === 0 ? 0 : Infinity
  }
  return safeRound((part / total) * 100)
}

Negative Numbers#

decrease(100, 150)      // -50, is this reasonable?
whatPercent(-50, 200)   // -25%

Decide based on business requirements.

Large Numbers#

9999999999999999 + 1  // Still 9999999999999999

JavaScript’s maximum safe integer is 2^53 - 1 (Number.MAX_SAFE_INTEGER). Beyond this, use BigInt.

React Implementation#

function usePercentageCalculator() {
  const [calcType, setCalcType] = useState<'percentage_of' | 'what_percent' | 'increase' | 'decrease' | 'change'>('percentage_of')
  const [value1, setValue1] = useState('')
  const [value2, setValue2] = useState('')
  
  const result = useMemo(() => {
    const num1 = parseFloat(value1)
    const num2 = parseFloat(value2)
    
    if (isNaN(num1) || isNaN(num2)) return null
    
    switch (calcType) {
      case 'percentage_of':
        return PercentageCalculator.percentageOf(num1, num2)
      case 'what_percent':
        return PercentageCalculator.whatPercent(num1, num2)
      case 'increase':
        return PercentageCalculator.increase(num1, num2)
      case 'decrease':
        return PercentageCalculator.decrease(num1, num2)
      case 'change':
        return PercentageCalculator.changeRate(num1, num2)
    }
  }, [calcType, value1, value2])
  
  return { calcType, setCalcType, value1, setValue1, value2, setValue2, result }
}

Professional Solution: decimal.js#

For financial applications, use decimal.js:

import Decimal from 'decimal.js'

Decimal.set({ precision: 20 })

function percentageOf(percent, value) {
  return new Decimal(percent).div(100).times(value).toNumber()
}

percentageOf(33.33, 300)  // 99.99 ✓

// Correct rounding with decimal.js
new Decimal(1.005).toFixed(2)  // "1.01" ✓

Practical Application#

Based on these principles, I built an online tool: Percentage Calculator

Features:

  • Five percentage calculation scenarios
  • Correct floating-point precision handling
  • Real-time calculation
  • Copy results support

Percentage calculations seem simple, but handling every edge case properly takes real effort.


Related Tools: Unit Converter | Number Base Converter