Topic 4.4
Math and Rounding
In one line
The built-in Math object gives you rounding (round, floor, ceil, trunc), min/max, powers, roots and random numbers. Rounding has rules that surprise people, toFixed returns a string, and money is safest stored as whole paise or cents.
Think of it like this
Paying at a shop. The bill says ₹199.50 and you only have whole rupees. Rounding decides what you pay: the shop may round to the nearest rupee (₹200), always down in your favour (₹199), or always up in theirs (₹200). A careful shopkeeper doesn't write half-paise in the book at all: everything is counted in whole paise, so ₹199.50 is written as 19950 paise. JavaScript gives you all these choices; you must pick the right one.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Math object
- A built-in object that holds maths functions like
Math.roundand constants likeMath.PI. - Round
- Change a number to a nearby simpler one, such as the nearest whole number.
- Floor / ceil
- Floor goes down to the whole number below; ceil goes up to the whole number above.
- Truncate
- Cut off the decimal part without rounding, so 4.9 becomes 4 and -4.9 becomes -4.
- Significant digits
- The digits that carry meaning, counted from the first non-zero digit.
- Smallest unit
- The smallest coin of a currency, like paise for rupees or cents for dollars.
- Radian
- The unit
Math.sinand friends use for angles. A full circle is 2π radians (360 degrees). - Pseudo-random
- Numbers that look random but come from a formula, so in theory they can be predicted.
- Clamp
- Keep a number inside a range: anything too small becomes the minimum, too big becomes the maximum.
Step by step
01Pick the rounding that matches the rule
Ask what the real-world rule is. "Nearest" is round. "How many full boxes can I fill?" is floor. "How many boxes do I need?" is ceil.
const items = 53;
const perBox = 10;
console.log(Math.floor(items / perBox)); // 5 full boxes
console.log(Math.ceil(items / perBox)); // 6 boxes needed
console.log(items % perBox); // 3 items in the last box02Display with toFixed, but keep numbers as numbers
toFixed is for the last step, showing a value. If you call it early, you get a string, and + then joins text instead of adding.
const price = (19.5).toFixed(2); // "19.50", a string
console.log(price + 1); // "19.501" (joined!)
console.log(Number(price) + 1); // 20.503Count money in paise
Turn rupees into paise once, with Math.round. After that every sum is a sum of whole numbers, which is always exact.
const pricesInRupees = [4.35, 0.1, 0.2];
const paise = pricesInRupees.map((r) => Math.round(r * 100)); // [435, 10, 20]
const total = paise.reduce((sum, p) => sum + p, 0); // 465
console.log((total / 100).toFixed(2)); // "4.65"04Split fairly
Dividing 10000 paise by 3 gives 3333.33... Give everyone the floor, then hand out the leftover paise one by one, so the shares add up to exactly the total.
function split(totalPaise, people) {
const base = Math.floor(totalPaise / people);
const extra = totalPaise % people;
return Array.from({ length: people }, (_, i) => base + (i < extra ? 1 : 0));
}
console.log(split(10000, 3)); // [ 3334, 3333, 3333 ]05Make a random whole number
Math.random() gives 0 <= r < 1. Multiply to stretch the range, floor to make whole numbers, and add min to shift it.
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(randomInt(1, 6)); // 1, 2, 3, 4, 5 or 6Try it yourself
- 1
Predict the table
In the first example, add
-0.5and0.5tovalues. Predict all four columns for both before you run it. (Watch for-0.) - 2
Change the tax rate
In the second example, change the tax to 5%. Predict the paise of tax and the final amount, then run it.
- 3
Check the formula's edges
In the third example, change
randomInt(1, 6)torandomInt(0, 1)in the test. What shouldnew Set(rolls).sizeprint now? Then remove+ 1from the formula and see which value disappears.
Code & diagrams
Look at the `-2.5` row: `Math.round` sends halves towards positive infinity, so it gives `-2`, not `-3`. The last value clamps 140 into the range 0 to 100.
Preview
Console
Nothing logged yet.Expected console output
2.4 2 2 3 2
2.5 3 2 3 2
2.6 3 2 3 2
-2.4 -2 -3 -2 -2
-2.5 -2 -3 -2 -2
-2.6 -3 -3 -2 -2
9 9 NaN -Infinity
7.5 -1 1024 9 5
1 100`toFixed` rounds halves away from zero (`-1.5` becomes `-2`) while `Math.round` rounds them up (`-1`). `1.005` is stored as slightly less than 1.005, so both ways give 1.00.
Preview
Console
Nothing logged yet.Expected console output
0.30000000000000004 0.30
10.00 string
1.00 1
-2 -1
1234.57 0.00012
434.99999999999994 434 435
[ 435, 10, 20, 9999 ] 10464
1884 123.48Ten thousand rolls all land between 1 and 6 and all six faces appear. Press the button to roll for real; that part isn't logged, because random values can't have a fixed expected output.
Preview
Console
Nothing logged yet.Expected console output
true
6
4 trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Add after toFixed
Write const subtotal = (19.5).toFixed(2); console.log(subtotal + 10);.
Break #2
Convert rupees to paise with floor
Write const paise = Math.floor(4.35 * 100);.
Break #3
Pass an array to Math.max
Write const best = Math.max([72, 95, 88]);.
Myth vs fact
Myth
Math.round rounds .5 away from zero.
Fact
It rounds .5 towards positive infinity: Math.round(-2.5) is -2. toFixed does round halves away from zero, which is why the two can disagree on negatives.
Myth
toFixed(2) always rounds correctly.
Fact
It rounds the number actually stored in binary, which may be slightly below what you typed: (1.005).toFixed(2) is "1.00". For display, Intl.NumberFormat rounds the decimal value you'd expect.
Myth
Using toFixed(2) everywhere fixes money bugs.
Fact
It hides them in display while the stored values still drift. Store integer paise or cents and round once, at defined points.
Myth
Math.random() is fine for generating passwords or tokens.
Fact
It's a fast pseudo-random generator, not a secure one. Use crypto.getRandomValues or crypto.randomUUID for anything secret.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The spec rounds
toFixedusing the exact binary value: it picks thenfor whichn / 10^f - xis closest to zero, choosing the largernon an exact tie. That's why results depend on how the decimal was stored.Intl.NumberFormat(ES2023 edition of the Intl spec) addsroundingModeoptions like"halfEven"(banker's rounding, which sends.5to the even neighbour) and"halfExpand"(its default). - ▸
V8's
Math.randomuses the xorshift128+ algorithm, seeded per page. It's statistically good and fast, but anyone who sees a few outputs can predict the rest, which is why it must never protect secrets. There is no built-in way to seed it for repeatable tests; use a small seeded generator in test code. - ▸
Most
Mathfunctions are allowed to be approximate (the spec says "implementation-approximated"), soMath.sinorMath.powmay differ in the last digit between engines.Math.sin(Math.PI)is about1.2e-16, not0, becauseMath.PIitself is rounded. Compare floating results with a tolerance, not===. - ▸
For money beyond
Number.MAX_SAFE_INTEGERpaise, or for exact decimal rates, useBigInt(Topic 1.3) for whole units, or a decimal library. A nativeDecimaltype has been discussed at TC39 but is not part of the language.
Remember this
- 1
**
Math** is a built-in object full of maths functions and constants. You don't create it; you call its methods directly:Math.round(2.6). Constants includeMath.PI(3.14159...) andMath.E. All its functions take numbers and turn other values into numbers first, soMath.max(1, "7")is7and anything that can't become a number givesNaN(Phase 1, Topic 1.3). - 2
The four roundings.
Math.round(x)goes to the nearest whole number, and a.5always goes up towards positive infinity:Math.round(2.5)is3butMath.round(-2.5)is-2.Math.floor(x)always goes down (towards minus infinity):Math.floor(-4.2)is-5.Math.ceil(x)always goes up:Math.ceil(4.1)is5.Math.trunc(x)(ES2015) chops off the decimals, so it goes towards zero:Math.trunc(-4.7)is-4. For positive numbersfloorandtruncagree; for negatives they don't. - 3
Other everyday methods.
Math.min(a, b, ...)andMath.max(...)take separate arguments, so use spread for an array:Math.max(...scores). With no arguments,Math.max()is-Infinity.Math.abs(x)removes the minus sign,Math.sign(x)gives-1,0or1. Powers:x ** y(ES2016) orMath.pow(x, y); roots:Math.sqrt,Math.cbrt;Math.hypot(3, 4)is5(the distance formula). The trig functions (Math.sin,Math.cos,Math.atan2) use radians, not degrees:degrees * Math.PI / 180. A common helper is clamping:Math.min(Math.max(x, low), high)keepsxinside a range. - 4
Rounding to decimal places.
num.toFixed(2)rounds to 2 decimals and returns a string ((10).toFixed(2)is"10.00"), which is what you want for display but not for more maths. Because numbers are stored in binary (Topic 1.3), some decimals can't be stored exactly:1.005is really1.00499999999999989..., so(1.005).toFixed(2)is"1.00", andMath.round(1.005 * 100) / 100is1.toPrecision(n)rounds tonsignificant digits. For display,Intl.NumberFormat(Topic 4.5) rounds the way people expect and adds separators. - 5
Money. Never add up money as decimal rupees or dollars:
0.1 + 0.2is0.30000000000000004, and the errors pile up across a cart. Store money as an integer number of the smallest unit (paise, cents): ₹4.35 is435. Integers up toNumber.MAX_SAFE_INTEGER(about 9 quadrillion) are exact. Convert typed input withMath.round(rupees * 100)(neverfloororparseInt:4.35 * 100is434.99999999999994), do all maths in paise, decide rounding once (for tax or discount), and divide by 100 only to display. When you split ₹100 three ways, give the leftover paise to someone so the parts still add up to the total. - 6
Random numbers.
Math.random()returns a decimal from0(included) up to1(not included). A random whole number frommintomaxinclusive isMath.floor(Math.random() * (max - min + 1)) + min.Math.random()is not secure: its output can be predicted, so never use it for passwords, tokens or anything an attacker might guess. Usecrypto.getRandomValues(new Uint32Array(1))orcrypto.randomUUID()(in secure HTTPS pages) instead. To shuffle an array fairly, use the Fisher–Yates algorithm (Hectal's/dsacourse covers it), notsort(() => Math.random() - 0.5), which is biased.
Explain it without notes
What is the difference between Math.floor and Math.trunc?
Why does (1.005).toFixed(2) give "1.00"?
How should an app store and add up prices?
How do you make a random integer between two values, and when is Math.random the wrong tool?
Practice
Write roundTo(x, places) that rounds a number to a given number of decimal places and returns a number (not a string), so roundTo(3.14159, 2) is 3.14.
Write toPaise(text) that turns a typed amount like "4.35" or "12" into whole paise (435, 1200).
Write percentOff(pricePaise, percent) that returns the discounted price in paise, rounded to the nearest paisa.
Trade-offs
- ↔
Integer paise make every sum exact but need converting at the edges (input and display); decimal numbers are simpler to write and quietly wrong in totals.
- ↔
toFixedis quick and built in but returns a string and rounds the binary value;Intl.NumberFormathandles rounding, separators and currency symbols but is more code to set up (create the formatter once and reuse it).
Done when you can
I can choose between
round,floor,ceilandtrunc, including for negative numbers.I can use
Math.min/Math.maxwith spread, and clamp a value into a range.I know
toFixedreturns a string and why(1.005).toFixed(2)is"1.00".I store money as whole paise or cents and convert with
Math.round.I can make a random integer in a range and I know when to use
cryptoinstead.