Percentage Calculator: JavaScript Floating-Point Precision Traps and Solutions
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