Numbers, floating point and formatting rupees
Module 1 promised an explanation for ₹0.30000000000000004. Here it is, along
with the reason a bill for three items at ₹80.10 comes to ₹240.29999999999998 —
and what to do so it does not.
The problem
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);
console.log(80.10 * 3);
0.30000000000000004
false
240.29999999999998
Not a bug in JavaScript. JavaScript has one number type, and it stores numbers in binary, where 0.1 has no exact representation — just as one-third has no exact decimal form. You write 0.1, the machine stores the closest binary value it can, and small errors accumulate.
Every language using the same standard behaves identically. Python, Java and C all print something similar.
So never compare decimals with ===:
const total = 0.1 + 0.2;
console.log(Math.abs(total - 0.3) < 0.0001);
true
Compare within a tolerance, or — far better for money — do not use decimals at all.
The fix for money: count in paise
Store money as a whole number of the smallest unit. Rupees become paise, integers are exact, and the arithmetic is reliable:
const toPaise = (rupees) => Math.round(rupees * 100);
const fromPaise = (paise) => paise / 100;
console.log(80.10 * 3);
console.log(fromPaise(toPaise(80.10) * 3));
240.29999999999998
240.3
A whole bill:
const lines = [
{ rate: 80.10, qty: 3 },
{ rate: 44.50, qty: 2 },
];
const totalPaise = lines.reduce(
(total, line) => total + toPaise(line.rate) * line.qty,
0,
);
console.log(totalPaise);
console.log(fromPaise(totalPaise));
32930
329.3
Convert to paise at the boundary, do every calculation in integers, convert back
only to display. This is what payment systems do, and it is why an API returns
"amount": 32930 rather than 329.30.
Integers are exact up to Number.MAX_SAFE_INTEGER, which is 9,007,199,254,740,991
— about ₹90,000 crore in paise. Comfortable.
Rounding, and its surprises
console.log(Math.round(2.5));
console.log(Math.round(-2.5));
console.log(Math.round(2.4));
3
-2
2
Math.round(-2.5) is -2, not -3. It rounds toward positive infinity on a
tie, rather than away from zero. For a refund of ₹2.50 that is a paisa in the
wrong direction, every time.
| Function | On 2.5 | On -2.5 | Does |
|---|---|---|---|
Math.round |
3 |
-2 |
Nearest; ties go up |
Math.floor |
2 |
-3 |
Down, always |
Math.ceil |
3 |
-2 |
Up, always |
Math.trunc |
2 |
-2 |
Drops the fraction |
For money, decide the rule deliberately and write it down. Math.round on
positive amounts is usually fine; the moment negatives appear, check.
toFixed is for display only
const value = (0.1 + 0.2).toFixed(2);
console.log(value);
console.log(typeof value);
0.30
string
It returns a string, so toFixed(2) + 10 joins text — module 2's coercion
bug, arriving through a helpful-looking method.
And it does not round the way you expect:
console.log((1.005).toFixed(2));
console.log((2.675).toFixed(2));
1.00
2.67
Both look wrong and both are correct: 1.005 is not exactly 1.005 in binary, it is
a hair below, so it rounds down. toFixed is for formatting a number you have
already calculated correctly. It is not a way to fix arithmetic.
Formatting for an Indian reader
This is the part people hand-roll and get wrong. Indian digit grouping is not groups of three — it is the last three, then twos: 12,34,567, not 1,234,567.
Intl.NumberFormat knows:
const inr = new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
});
console.log(inr.format(1234567.5));
console.log(inr.format(240));
₹12,34,567.50
₹240.00
Compare the grouping:
console.log(new Intl.NumberFormat('en-IN').format(1234567));
console.log(new Intl.NumberFormat('en-US').format(1234567));
12,34,567
1,234,567
A hand-written formatter that inserts a comma every three digits produces the
American form, which looks wrong to every Indian reader. Use Intl.
Common options:
| Option | Effect |
|---|---|
style: 'currency', currency: 'INR' |
Adds ₹ |
maximumFractionDigits: 0 |
Whole rupees — ₹12,34,567 |
minimumFractionDigits: 2 |
Always two decimals |
style: 'percent' |
0.125 becomes 13% |
(1234567).toLocaleString('en-IN') is the quick form. Create the formatter
once and reuse it when formatting many values — building one per row of a table
is measurably slower.
Note the percent row rounds: 0.125 displays as 13%, not 12.5%. Add
maximumFractionDigits if you need the detail.
Converting text to numbers, once more
From module 2, now with the money angle:
console.log(Number('80.10'));
console.log(Number(''));
console.log(parseInt('80.99', 10));
console.log(parseFloat('80.99'));
80.1
0
80
80.99
parseInt truncates rather than rounding — parseInt('80.99', 10) is 80,
losing 99 paise silently. For money, Number() then convert to paise.
And Number('') is 0, so an empty amount field becomes a free order rather
than an error. Check for emptiness before converting.
Useful number methods
| Call | Gives |
|---|---|
Number.isInteger(x) |
Whole number? |
Number.isNaN(x) |
Genuinely NaN |
Number.isFinite(x) |
Not Infinity or NaN |
Math.abs(x) |
Distance from zero |
Math.min(...a) / Math.max(...a) |
Smallest / largest |
Math.random() |
0 to just under 1 |
(x).toFixed(n) |
String with n decimals |
Math.random() never returns 1. For a whole number from 1 to 6:
Math.floor(Math.random() * 6) + 1.
Check your work
0.1 + 0.2 is 0.30000000000000004 and 80.10 * 3 is 240.29999999999998.
Binary cannot represent these decimals exactly. Every language with the same
number standard does this.
Never compare decimals with ===. Compare within a tolerance, or work in
integers.
Counting in paise fixes it. fromPaise(toPaise(80.10) * 3) is 240.3
exactly. The bill example totals 32930 paise, or ₹329.30.
Math.round(-2.5) is -2, not -3. Ties round toward positive infinity.
toFixed returns a string, so adding a number to it joins text.
(1.005).toFixed(2) is 1.00 and (2.675).toFixed(2) is 2.67, because
neither value is exactly what it looks like. toFixed formats; it does not fix
arithmetic.
Intl.NumberFormat('en-IN') groups as 12,34,567, not 1,234,567. With
style: 'currency' and currency: 'INR' it gives ₹12,34,567.50.
style: 'percent' on 0.125 gives 13% — it rounds by default.
parseInt('80.99', 10) is 80. It truncates, losing paise.
Number('') is 0.
The Bill exercise. Two lines at ₹80.10 × 3 and ₹44.50 × 2 give:
Toor dal x3 = ₹240.30
Sugar x2 = ₹89.00
Total: ₹329.30
For the discount: ten units at ₹65.50 is 65,500 paise, over the ₹500 threshold. Five per cent is 3,275 paise exactly, leaving 62,225 paise — ₹622.25.
The ₹16.415 question has no single right answer, which is the point. In paise
it is 1641.5, and Math.round gives 1642 — rounding a half up, so the
customer pays the extra paisa. Rounding the discount up instead would favour
the customer. Either is defensible; what is not defensible is leaving it to
whatever toFixed happens to do, because that depends on binary representation
and will differ between values that look alike. Decide the rule, write it in a
comment, and test the halfway case — that is the whole exercise.
Practice
- Print
0.1 + 0.2and80.10 * 3. Then check0.1 + 0.2 === 0.3and write a tolerance comparison that givestrue. - Write
toPaiseandfromPaiseand redo80.10 * 3with them. - Build the two-line bill and total it in paise. Confirm
32930. - Run all four rounding functions on
2.5and-2.5and fill in the table yourself before checking. - Prove
toFixedreturns a string. Print itstypeof, then add10to it and explain the result. - Run
(1.005).toFixed(2)and account for the answer. - Format
1234567.5as Indian currency. Then format the same number withen-USand note the different grouping. - Format a number as whole rupees with no decimals, and another as a percentage.
parseInt('80.99', 10)andNumber('80.99')— explain the difference and say which belongs in a billing calculation.- Harder. Write a
Billmodule withaddLine(item, rate, qty),total()returning paise, andformat()returning a printable string with each line and the total in Indian currency. Apply a 5% discount on totals over ₹500, and decide — and write down — whether a discount of ₹16.415 rounds up or down, and why. Then prove your rule with a test.
Next: dates and times — including the reason a delivery booked for the 27th gets stored as the 26th.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship