JavaScript (ES2026) Module 2 – Values, types and operators
Math, rounding and random numbers
Round numbers in JavaScript with Math.round, floor, ceil, trunc and toFixed, avoid the 1.005 trap, make random integers, and sum decimals with Math.sumPrecise.
What you will learn
- Choose the right rounding function (round, floor, ceil, trunc, toFixed) for a task
- Generate random numbers in a range and know when Math.random is not enough
- Add many decimals accurately with Math.sumPrecise where it is available
Before you start
What you’ll learn · Before you start
What you will learn
- Choose the right rounding function (round, floor, ceil, trunc, toFixed) for a task
- Generate random numbers in a range and know when Math.random is not enough
- Add many decimals accurately with Math.sumPrecise where it is available
Before you start
On this page
JavaScript rounds with four functions of the built-in Math object, which differ only in direction:
Math.round(x) goes to the nearest whole number, with a tie going up (so Math.round(-2.5) is -2),
Math.floor always goes down, Math.ceil always up, and Math.trunc drops the fraction. To round to decimal
places, x.toFixed(2) returns a string, rounded from the binary value the number really holds, which is why
(1.005).toFixed(2) is '1.00'. For random numbers, Math.random() gives a value from 0 up to, but not including,
1, which you scale to the range you need; for anything that must stay secret, such as a login code, use
crypto.getRandomValues instead. And to add up many decimals without piling up rounding errors, ECMAScript 2026
adds Math.sumPrecise.
The Math object
Math is a plain object that holds mathematical constants and functions; it is not a class, so you never create
one with new. Every function takes numbers and returns a number.
console.log('Math.PI:', Math.PI, '| Math.E:', Math.E);
console.log('Math.abs(-5.5):', Math.abs(-5.5), '| Math.sign(-3):', Math.sign(-3), '| 2 ** 10:', 2 ** 10);
console.log('Math.sqrt(2):', Math.sqrt(2), '| Math.hypot(3, 4):', Math.hypot(3, 4), '| Math.cbrt(27):', Math.cbrt(27));
const marks = [62, 91, 78];
console.log('Math.max(...marks):', Math.max(...marks), '| Math.max():', Math.max(), '| Math.max(1, NaN):', Math.max(1, NaN));
const clamp = (x, low, high) => Math.min(Math.max(x, low), high); // keep x between low and high
console.log('clamp(15, 0, 10):', clamp(15, 0, 10), '| clamp(-2, 0, 10):', clamp(-2, 0, 10));
try {
// eslint-disable-next-line no-obj-calls -- the TypeError is the point
new Math();
} catch (error) {
console.log('new Math():', error.name, '- Math is a plain object that holds functions, not a constructor');
} Output
Math.PI: 3.141592653589793 | Math.E: 2.718281828459045 Math.abs(-5.5): 5.5 | Math.sign(-3): -1 | 2 ** 10: 1024 Math.sqrt(2): 1.4142135623730951 | Math.hypot(3, 4): 5 | Math.cbrt(27): 3 Math.max(...marks): 91 | Math.max(): -Infinity | Math.max(1, NaN): NaN clamp(15, 0, 10): 10 | clamp(-2, 0, 10): 0 new Math(): TypeError - Math is a plain object that holds functions, not a constructor
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node math-object.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Math.max and Math.min take any number of arguments, so an array is passed with the spread syntax,
Math.max(...marks). Two of their answers surprise people: with no arguments Math.max() is -Infinity, the
starting point that any number beats, and a single NaN among the arguments makes the result NaN. Combining the
two gives a clamp, which keeps a value inside a range, a common need for sliders, scores and percentages.
Rounding to a whole number
// The four ways to round to a whole number, side by side. -0 is shown as -0 (String(-0) would print 0).
const show = (n) => (Object.is(n, -0) ? '-0' : String(n)).padStart(6);
console.log(' x', ' round', ' floor', ' ceil', ' trunc');
for (const x of [2.6, 2.5, 2.4, -2.4, -2.5, -2.6, -0.4]) {
console.log(String(x).padStart(6), show(Math.round(x)), show(Math.floor(x)), show(Math.ceil(x)), show(Math.trunc(x)));
} Output
x round floor ceil trunc 2.6 3 2 3 2 2.5 3 2 3 2 2.4 2 2 3 2 -2.4 -2 -3 -2 -2 -2.5 -2 -3 -2 -2 -2.6 -3 -3 -2 -2 -0.4 -0 -1 -0 -0
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node rounding-table.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The four rounding functions on a tie, in both directions
Text description of the diagram
Each of the four rows is a number line from −3 to 3. An open circle marks the value, 2.5 on the right and −2.5 on the left, and an arrow leads to the filled dot of the whole number it becomes.
- Math.round takes the nearest whole number, and a tie goes up, towards +∞: 2.5 becomes 3 and −2.5 becomes −2.
- Math.floor always goes down, towards −∞: 2.5 becomes 2 and −2.5 becomes −3.
- Math.ceil always goes up, towards +∞: 2.5 becomes 3 and −2.5 becomes −2.
- Math.trunc drops the fraction, which moves the value towards 0: 2.5 becomes 2 and −2.5 becomes −2.
So the four functions agree for positive numbers in pairs (round and ceil give 3, floor and trunc give 2), but for negative numbers round, ceil and trunc give −2 while floor gives −3.
Math.roundreturns the nearest whole number. On a tie, a value exactly halfway, it picks the neighbour closer to +∞, so 2.5 becomes 3 but -2.5 becomes -2. This is not “round half away from zero”, which people often expect, and not the “round half to even” rule of some other languages either.Math.flooralways rounds down, towards -∞: use it to count whole things, such as full boxes from a number of items or full hours from a number of minutes. For negative numbers it moves away from zero:Math.floor(-2.4)is -3.Math.ceilalways rounds up, towards +∞: the number of buses needed for 130 passengers at 50 a bus isMath.ceil(130 / 50), 3.Math.truncremoves the fraction, which moves the value towards zero from either side.
The last row shows a detail of round, ceil and trunc: a negative number that they bring to zero gives -0,
which prints as 0 in most places but still has its sign, as the previous lesson showed.
A shortcut to avoid: x | 0
Older code drops a fraction with x | 0 or ~~x. Both work only by accident: bitwise operators first convert their
operand to a 32-bit integer, which wraps any value outside -2,147,483,648 to 2,147,483,647:
// x | 0 and ~~x look like quick ways to drop a fraction, but they first squeeze x into a 32-bit integer.
console.log('7.9 | 0:', 7.9 | 0, '| ~~-7.9:', ~~-7.9, '| Math.trunc(-7.9):', Math.trunc(-7.9));
const big = 2 ** 31 + 0.5; // 2147483648.5
console.log('big | 0:', big | 0, '| Math.trunc(big):', Math.trunc(big));
console.log('(2 ** 32 + 5.5) | 0:', (2 ** 32 + 5.5) | 0); Output
7.9 | 0: 7 | ~~-7.9: -7 | Math.trunc(-7.9): -7 big | 0: -2147483648 | Math.trunc(big): 2147483648 (2 ** 32 + 5.5) | 0: 5
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node bitwise-trap.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
For small numbers the shortcut agrees with Math.trunc, but 2,147,483,648.5 comes out negative and 4,294,967,301.5
comes out as 5, with no error. Math.trunc says what it does and works for every number.
Rounding to decimal places
toFixed(digits) formats a number with a fixed number of digits after the point. It returns a string, ready for
display, and it rounds the exact binary value the number holds, not the decimal you typed. The number written as
1.005 is really 1.00499999999999989…, so there is no tie to round up:
const price = 1.005;
console.log('price.toFixed(2):', price.toFixed(2), '| typeof:', typeof price.toFixed(2));
console.log('price.toFixed(20):', price.toFixed(20));
console.log('Math.round(price * 100) / 100:', Math.round(price * 100) / 100, '| price * 100:', price * 100);
// Every amount from 0.005 to 999.995 that ends in 5 (100,000 of them), rounded to two places, half up.
const epsilonRound = (x) => Math.round((x + Number.EPSILON) * 100) / 100; // a widely copied repair
const shiftRound = (x) => Number(Math.round(Number(`${x}e2`)) + 'e-2'); // moves the point in the text
const counts = { toFixed: 0, times100: 0, epsilon: 0, shift: 0 };
let firstEpsilonMiss = '';
for (let k = 0; k < 100_000; k++) {
const whole = Math.floor(k / 100);
const cents = String(k % 100).padStart(2, '0');
const x = Number(`${whole}.${cents}5`); // 0.005, 0.015, 0.025 … 999.995
const expected = (k + 1) / 100; // the half-up answer: one hundredth more than the digits before the 5
if (Number(x.toFixed(2)) !== expected) counts.toFixed++;
if (Math.round(x * 100) / 100 !== expected) counts.times100++;
if (epsilonRound(x) !== expected) {
counts.epsilon++;
firstEpsilonMiss ||= String(x);
}
if (shiftRound(x) !== expected) counts.shift++;
}
console.log('wrong results out of 100000:', counts);
console.log('first value the epsilon repair gets wrong:', firstEpsilonMiss); Output
price.toFixed(2): 1.00 | typeof: string
price.toFixed(20): 1.00499999999999989342
Math.round(price * 100) / 100: 1 | price * 100: 100.49999999999999
wrong results out of 100000: { toFixed: 48000, times100: 4588, epsilon: 4573, shift: 0 }
first value the epsilon repair gets wrong: 2.135
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node decimal-places.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The second half of the output checks the four approaches on every amount from 0.005 to 999.995 that ends in a 5, the 100,000 cases where “round half up” has to round up:
toFixed(2)rounds 48,000 of them down, because their stored value lies just below the halfway point. That is correct arithmetic on the stored value, and still wrong for anyone who expects the decimal rule.Math.round(x * 100) / 100gets 4,588 wrong: the multiplication rounds too, sometimes to just below the tie.Math.round((x + Number.EPSILON) * 100) / 100, a repair that is widely copied, fixes only 15 of those 4,588.Number.EPSILONis the gap between neighbouring numbers near 1; from 2 upwards the gaps are wider, so adding it often changes nothing at all, and the first failure is already 2.135.- Moving the decimal point in the text,
Number(x + 'e2'), reads the digits 1.005 as the number 100.5, which is exact, soMath.roundsees a real tie. It gets all 100,000 right. The exercise below builds a complete version of it, with negative numbers.
For display: Intl.NumberFormat
Intl.NumberFormat, the number formatter of the Internationalization API, starts from a different place: the
standard tells it to take the shortest decimal that identifies the number, the digits String(x) prints, and round
those. A tie goes away from zero unless you pick another roundingMode:
// Intl.NumberFormat rounds the shortest decimal that identifies the number (what String(x) prints), not its binary value.
const twoPlaces = new Intl.NumberFormat('en', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
for (const x of [1.005, 2.675, 10000.005, -1.005]) {
console.log(String(x).padEnd(10), 'toFixed(2):', x.toFixed(2).padEnd(9), 'Intl.NumberFormat:', twoPlaces.format(x));
}
// The same 100,000 ties as before: 0.005, 0.015 … 999.995, each expected to round up.
let wrong = 0;
for (let k = 0; k < 100_000; k++) {
const x = Number(`${Math.floor(k / 100)}.${String(k % 100).padStart(2, '0')}5`);
if (Number(twoPlaces.format(x).replaceAll(',', '')) !== (k + 1) / 100) wrong++;
}
console.log('Intl.NumberFormat wrong results out of 100000:', wrong);
// Ties go away from zero by default (roundingMode 'halfExpand'); other rules are an option away.
const halfEven = new Intl.NumberFormat('en', { maximumFractionDigits: 2, roundingMode: 'halfEven' });
console.log("0.125 with roundingMode 'halfEven':", halfEven.format(0.125), '| 0.135:', halfEven.format(0.135)); Output
1.005 toFixed(2): 1.00 Intl.NumberFormat: 1.01 2.675 toFixed(2): 2.67 Intl.NumberFormat: 2.68 10000.005 toFixed(2): 10000.00 Intl.NumberFormat: 10,000.01 -1.005 toFixed(2): -1.00 Intl.NumberFormat: -1.01 Intl.NumberFormat wrong results out of 100000: 0 0.125 with roundingMode 'halfEven': 0.12 | 0.135: 0.14
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node format-for-display.mjs
So Intl.NumberFormat shows 1.005 as 1.01, gets every one of the 100,000 ties right, and adds the thousands
separators of the locale on top. It returns text for people, though, and parsing that text back is fragile, so use
it only at the very end, for display.
Version note
QuickJS, which runs the examples of this track in the browser, has no Intl object, so the formatting example
comes without a Run button, and what you see under it was printed by Node.js 24.21.0. Browsers and Node.js include
Intl.NumberFormat, and its roundingMode option works from Chrome 106, Firefox 116, Safari 15.4 and Node.js 19,
according to MDN’s browser compatibility data.
When the amounts are money, the better fix is the one from the previous lesson: keep them as whole cents or paise, so there is no decimal to round in the first place, and format only for display.
Rounding Calculator Round any number with exact decimal arithmetic and compare every rounding mode side by side.Random numbers
Math.random() returns a number from 0 up to, but not including, 1, spread approximately evenly over that range.
The engine chooses the algorithm and the starting seed; you cannot set the seed, so you cannot make two runs give
the same sequence. To turn it into a whole number from min to max, scale, floor and shift:
min + Math.floor(Math.random() * (max - min + 1)). Using Math.round instead is a common mistake, and the
difference is easy to measure:
// A seeded generator gives the same "random" numbers on every run (Park and Miller's minimal standard generator).
function seededRandom(seed) {
let state = seed;
return () => {
state = (state * 48271) % 2147483647;
return (state - 1) / 2147483646; // a number from 0 up to, but not including, 1
};
}
const right = [0, 0, 0, 0, 0, 0];
const wrong = [0, 0, 0, 0, 0, 0];
const rngA = seededRandom(2026);
const rngB = seededRandom(2026);
for (let i = 0; i < 60_000; i++) {
const good = Math.floor(rngA() * 6) + 1; // each face gets one sixth of the numbers from 0 to 1
const biased = Math.round(rngB() * 5) + 1; // faces 1 and 6 get only a tenth each
right[good - 1]++;
wrong[biased - 1]++;
}
console.log('face: ', [1, 2, 3, 4, 5, 6].map((f) => String(f).padStart(6)).join(''));
console.log('Math.floor(r * 6):', right.map((n) => String(n).padStart(6)).join(''));
console.log('Math.round(r * 5):', wrong.map((n) => String(n).padStart(6)).join('')); Output
face: 1 2 3 4 5 6 Math.floor(r * 6): 9889 10198 10049 9907 10057 9900 Math.round(r * 5): 6069 11963 12104 11833 12054 5977
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node dice.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
With Math.floor, each face gets close to 10,000 of the 60,000 rolls. With Math.round(r * 5) + 1, faces 1 and 6
get about 6,000 each and the others about 12,000: only values below 0.1 round to 0, while values from 0.1 to 0.3
round to 1, so the end faces get half the share of the middle ones.
The example uses its own small seeded generator instead of Math.random, so it prints the same counts on every
run. That is the standard way to test code that uses randomness: pass the random function in as a parameter, give
it Math.random in production and a seeded generator in tests.
When Math.random is not enough
Math.random is not designed to be unpredictable, and anyone who sees enough of its output may be able to predict
what comes next. Values that protect something, such as one-time codes, password-reset links or session IDs, need
the cryptographically secure generator of the Web Crypto API, available in browsers and, as the global crypto, in
Node.js:
// Values that protect something (a login code, a reset link, a session ID) need a cryptographically secure source.
console.log('random UUID:', crypto.randomUUID());
// A whole number from 0 to n - 1, every value equally likely (for n up to 2 ** 32).
function secureInt(n) {
const limit = Math.floor(2 ** 32 / n) * n; // the largest multiple of n that fits in 32 bits
const buffer = new Uint32Array(1);
do {
crypto.getRandomValues(buffer);
} while (buffer[0] >= limit); // drop the few values above it, or small codes would come up more often
return buffer[0] % n;
}
console.log('six-digit code:', String(secureInt(1_000_000)).padStart(6, '0')); Output
random UUID: 652ff08a-4470-4715-b2fe-ef6962389664 six-digit code: 944865
This output changes from run to run: the UUID and the code are new random values on every run
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node secure-random.mjs
crypto.randomUUID() returns a random version 4 UUID; in a browser it is available only on secure (HTTPS) pages.
crypto.getRandomValues fills a typed array with random integers, at most 65,536 bytes per call. Turning those
integers into a number below n needs care: the remainder % n alone would make small values slightly more likely
whenever 2³² is not a multiple of n, so secureInt throws away the few values above the largest multiple and draws
again, a technique called rejection sampling.
Adding many decimals: Math.sumPrecise
Every + rounds its result to the nearest number, and adding many values one by one lets the small errors build
up. Math.sumPrecise(items), new in ECMAScript 2026, takes an array or any other iterable of numbers and returns
their exact mathematical sum, rounded once at the end:
const tenths = Array(10_000).fill(0.1);
const add = (total, x) => total + x;
console.log('10000 tenths with reduce:', tenths.reduce(add, 0));
console.log('[0.1, 0.2, 0.3] with reduce:', [0.1, 0.2, 0.3].reduce(add, 0));
console.log('[1e20, 1, -1e20] with reduce:', [1e20, 1, -1e20].reduce(add, 0));
if (typeof Math.sumPrecise === 'function') {
console.log('10000 tenths with Math.sumPrecise:', Math.sumPrecise(tenths));
console.log('[0.1, 0.2, 0.3] with Math.sumPrecise:', Math.sumPrecise([0.1, 0.2, 0.3]));
console.log('[1e20, 1, -1e20] with Math.sumPrecise:', Math.sumPrecise([1e20, 1, -1e20]));
} else {
console.log('Math.sumPrecise: not available in this engine');
} Output
10000 tenths with reduce: 1000.0000000001588 [0.1, 0.2, 0.3] with reduce: 0.6000000000000001 [1e20, 1, -1e20] with reduce: 0 Math.sumPrecise: not available in this engine
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node sum-precise.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Version note
Node.js 24.21.0, whose output is shown first, does not have Math.sumPrecise yet, so the example prints a note
instead; Node.js 26.10.0 has it only behind the V8 flag --js-sum-precise. The Run button uses QuickJS, which
implements it, and so do Chrome 147, Firefox 137 and Safari 26.2 and later, according to MDN’s browser
compatibility data. Check with typeof Math.sumPrecise === 'function' before you rely on it.
In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:
10000 tenths with reduce: 1000.0000000001588 [0.1, 0.2, 0.3] with reduce: 0.6000000000000001 [1e20, 1, -1e20] with reduce: 0 10000 tenths with Math.sumPrecise: 1000 [0.1, 0.2, 0.3] with Math.sumPrecise: 0.6 [1e20, 1, -1e20] with Math.sumPrecise: 1
The exact sums are what a person would write down: 1000 for ten thousand tenths, where reduce drifts to
1000.0000000001588, and 1 for [1e20, 1, -1e20], where adding from left to right loses the 1 entirely because it is
far smaller than the gap between numbers near 10²⁰. Math.sumPrecise refuses anything that is not a number, a
string or a BigInt included, with a TypeError, and the sum of an empty list is -0. Until it is available
everywhere, add amounts of money as whole cents, where every sum is exact anyway.
Exercise
Exercise · Medium · JavaScript
Round to decimal places and pick a random integer
Write two functions in numbers.mjs.
**roundTo(value, places)** rounds value to places digits after the point, the way people round the decimal they wrote down: a tie goes away from zero, whatever the binary value stored for it. So roundTo(1.005, 2) is 1.01 (not the 1 that Math.round(1.005 * 100) / 100 gives), roundTo(-1.005, 2) is -1.01 and roundTo(2.5, 0) is 3. places is a whole number from 0 to 6, and value is 0 or between 0.000001 and 1,000,000,000,000,000 in size, so String(value) never uses exponent notation.
**randomInt(min, max, rng)** returns a whole number from min to max, both included, every one equally likely. rng is a function that returns a number from 0 up to, but not including, 1, like Math.random; the tests pass in a seeded generator so that their results are repeatable. Throw a RangeError when min or max is not a safe integer, or when min is larger than max.
The sample tests check both functions, including 60,000 seeded draws that should give each face of a die close to 10,000 times.
Starter code · numbers.mjs
/** value rounded to places digits after the point, a tie going away from zero (0 <= places <= 6). */
export function roundTo(value, places) {
// Replace this line with your code.
return Math.round(value * 10 ** places) / 10 ** places;
}
/** A whole number from min to max, both included, each equally likely; rng() returns a number in [0, 1). */
export function randomInt(min, max, rng) {
// Replace this line with your code.
return Math.round(rng() * (max - min)) + min;
} The sample tests · numbers.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { randomInt, roundTo } from './numbers.mjs';
/** Park and Miller's minimal standard generator: the same numbers on every run. */
function seededRandom(seed) {
let state = seed;
return () => {
state = (state * 48271) % 2147483647;
return (state - 1) / 2147483646;
};
}
test('rounds the decimal that was written, not its binary value', () => {
assert.equal(roundTo(1.005, 2), 1.01);
assert.equal(roundTo(2.675, 2), 2.68);
assert.equal(roundTo(10000.005, 2), 10000.01);
assert.equal(roundTo(1.004, 2), 1);
assert.equal(roundTo(1234.5678, 2), 1234.57);
});
test('a tie goes away from zero', () => {
assert.equal(roundTo(2.5, 0), 3);
assert.equal(roundTo(-2.5, 0), -3);
assert.equal(roundTo(-1.005, 2), -1.01);
assert.equal(roundTo(0.125, 2), 0.13);
});
test('other numbers of places, and numbers that need no rounding', () => {
assert.equal(roundTo(3.14159, 3), 3.142);
assert.equal(roundTo(123.456, 0), 123);
assert.equal(roundTo(7, 2), 7);
assert.equal(roundTo(0, 4), 0);
assert.equal(roundTo(0.000001, 6), 0.000001);
});
test('randomInt maps the ends of the rng range to min and max', () => {
assert.equal(randomInt(1, 6, () => 0), 1);
assert.equal(randomInt(1, 6, () => 0.9999999), 6);
assert.equal(randomInt(10, 10, () => 0.5), 10);
assert.equal(randomInt(-3, 3, () => 0.5), 0);
});
test('60,000 seeded rolls of a die: every face close to 10,000 times', () => {
const rng = seededRandom(7);
const counts = [0, 0, 0, 0, 0, 0];
for (let i = 0; i < 60_000; i++) {
const face = randomInt(1, 6, rng);
assert.ok(Number.isInteger(face) && face >= 1 && face <= 6, `got ${face}`);
counts[face - 1]++;
}
for (const n of counts) assert.ok(n > 9_500 && n < 10_500, `counts ${counts.join(', ')}`);
});
test('refuses a range that is not two safe integers in order', () => {
const rng = seededRandom(1);
assert.throws(() => randomInt(6, 1, rng), RangeError);
assert.throws(() => randomInt(1.5, 6, rng), RangeError);
assert.throws(() => randomInt(1, 2 ** 53, rng), RangeError);
}); A hint
For roundTo, do not multiply by 10 ** places: move the point in the text instead. Number('1.005e2') is exactly 100.5, so Math.round sees a real tie. Round the absolute value and put the sign back afterwards, because Math.round sends a negative tie towards +∞. For randomInt, Math.floor(rng() * count) is a whole number from 0 to count - 1.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
What is the difference between Math.round, Math.floor, Math.ceil and Math.trunc? All four return a whole
number, and they differ in direction. Math.floor always rounds down, towards minus infinity, and Math.ceil
always up, towards plus infinity. Math.trunc removes the fraction, which moves the value towards zero.
Math.round goes to the nearest whole number, and a tie goes up, towards plus infinity. The differences show with
negative numbers: for -2.5, floor gives -3, while ceil, trunc and round all give -2, and for -2.6 round gives -3.
None of them rounds to decimal places; for that, toFixed returns a string rounded from the stored binary value, so
money is better kept as whole cents.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid
A quiz app shuffles questions with arr.sort(() => Math.random() - 0.5), and a contest feature picks prize winners with Math.random. What is wrong with each, and what would you use instead?
-
Fresher to mid
Prices must be shown with two decimals and invoices round half up. A tester finds that a price of 1.005 is shown as 1.00. Explain why, and design the rounding: the function, where it is used and how you test it.
Key takeaways
Math.roundgoes to the nearest whole number with ties towards +∞;Math.floorgoes down,Math.ceilup andMath.trunctowards zero.- Avoid
x | 0and~~xfor truncating: they wrap anything outside the 32-bit range. toFixedreturns a string and rounds the stored binary value, so(1.005).toFixed(2)is'1.00'; addingNumber.EPSILONdoes not repair it. Round the decimal text, format withIntl.NumberFormat, which rounds the digits you see, or keep money in whole cents.- A random whole number from
mintomaxismin + Math.floor(Math.random() * (max - min + 1)); inject a seeded generator to test such code. - Use
crypto.getRandomValuesorcrypto.randomUUID, neverMath.random, for codes, tokens and IDs that protect something. Math.sumPreciseadds numbers exactly and rounds once; check that it exists before relying on it.
References
- ECMAScript 2026 Language Specification: Math.round (Ecma International)
- ECMAScript 2026 Language Specification: Math.floor (Ecma International)
- ECMAScript 2026 Language Specification: Math.trunc (Ecma International)
- ECMAScript 2026 Language Specification: Math.max (Ecma International)
- ECMAScript 2026 Language Specification: Math.random (Ecma International)
- ECMAScript 2026 Language Specification: Math.sumPrecise (Ecma International)
- ECMAScript 2026 Language Specification: Number.prototype.toFixed (Ecma International)
- ECMAScript 2026 Language Specification: ToInt32 (Ecma International)
- ECMAScript 2026 Internationalization API Specification: ToIntlMathematicalValue (Ecma International)
- Intl.NumberFormat() constructor (MDN Web Docs)
- Math.random() (MDN Web Docs)
- Math.sumPrecise() (MDN Web Docs)
- Crypto.getRandomValues() (MDN Web Docs)
- Web Cryptography API (W3C)
- Node.js v24 documentation, Web Crypto API (OpenJS Foundation (Node.js))
- TC39 proposal: Math.sumPrecise (Ecma International (TC39))
- MDN browser compatibility data (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress