RizTech Academy logo
RizTech Academy
Text, Numbers and DatesLesson 2 of 325 min

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

  1. Print 0.1 + 0.2 and 80.10 * 3. Then check 0.1 + 0.2 === 0.3 and write a tolerance comparison that gives true.
  2. Write toPaise and fromPaise and redo 80.10 * 3 with them.
  3. Build the two-line bill and total it in paise. Confirm 32930.
  4. Run all four rounding functions on 2.5 and -2.5 and fill in the table yourself before checking.
  5. Prove toFixed returns a string. Print its typeof, then add 10 to it and explain the result.
  6. Run (1.005).toFixed(2) and account for the answer.
  7. Format 1234567.5 as Indian currency. Then format the same number with en-US and note the different grouping.
  8. Format a number as whole rupees with no decimals, and another as a percentage.
  9. parseInt('80.99', 10) and Number('80.99') — explain the difference and say which belongs in a billing calculation.
  10. Harder. Write a Bill module with addLine(item, rate, qty), total() returning paise, and format() 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