Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 22 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 22 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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.

A few constants and functions of Math JavaScript · math-object.mjs
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

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

round, floor, ceil and trunc for seven values JavaScript · rounding-table.mjs
// 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

Four number lines show where 2.5 and -2.5 go under Math.round, Math.floor, Math.ceil and Math.trunc.Where 2.5 and −2.5 goMath.roundnearest; a tie goes up, towards +∞−3−2−10123Math.flooralways down, towards −∞−3−2−10123Math.ceilalways up, towards +∞−3−2−10123Math.truncdrops the fraction: towards 0−3−2−10123the value: 2.5 or −2.5the whole number it becomes

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.round returns 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.floor always 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.ceil always rounds up, towards +∞: the number of buses needed for 130 passengers at 50 a bus is Math.ceil(130 / 50), 3.
  • Math.trunc removes 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:

Truncating with bitwise operators JavaScript · bitwise-trap.mjs
// 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

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:

toFixed, a common repair, and a reliable one, on 100,000 ties JavaScript · decimal-places.mjs
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

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) / 100 gets 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.EPSILON is 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, so Math.round sees 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:

toFixed against Intl.NumberFormat on the same ties JavaScript · format-for-display.mjs
// 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:

60,000 seeded rolls of a die, with floor and with round JavaScript · dice.mjs
// 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

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:

A UUID and a six-digit code from the secure generator JavaScript · secure-random.mjs
// 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.

Random Generator Random numbers, picks from a list and dice from the secure generator, with rejection sampling so every outcome is equally likely.

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:

reduce against Math.sumPrecise JavaScript · sum-precise.mjs
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

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.

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.

  1. Question 1 of 6 What does Math.round(-2.5) return?

    Choose one answer.

    Show the answer to question 1

    Answer: -2

    Math.round sends a tie to the neighbour closer to +∞, and -2 is closer to +∞ than -3. So Math.round(2.5) is 3, but Math.round(-2.5) is -2.

  2. Question 2 of 6 A school needs buses for 130 students, 50 to a bus. Which expression gives the number of buses?

    Choose one answer.

    Show the answer to question 2

    Answer: Math.ceil(130 / 50)

    130 / 50 is 2.6, and any part of a bus still needs a whole bus, so the count must round up: Math.ceil gives 3. Math.round also gives 3 here, but it would give 2 for 120 students, 2.4 buses.

  3. Question 3 of 6 What does (1.005).toFixed(2) return?

    Choose one answer.

    Show the answer to question 3

    Answer: The string '1.00'

    toFixed returns a string, and it rounds the stored binary value. 1.005 is stored as 1.00499999999999989…, which is below the halfway point, so it rounds down to '1.00'.

  4. Question 4 of 6 Which expression gives every whole number from 1 to 6 with the same chance?

    Choose one answer.

    Show the answer to question 4

    Answer: Math.floor(Math.random() * 6) + 1

    Math.random() * 6 lies from 0 up to, but not including, 6, and floor turns each sixth of that range into one of 0 to 5. round gives 1 and 6 half the share of the other faces, ceil can return 0 when Math.random() returns 0, and the last one gives 0 to 6.

  5. Question 5 of 6 A site sends a six-digit login code by SMS. Where should the random digits come from?

    Choose one answer.

    Show the answer to question 5

    Answer: crypto.getRandomValues, with rejection sampling to keep every code equally likely

    A login code protects an account, so it must be unpredictable. Math.random is not designed for that, the time is easy to guess and a seeded generator repeats. crypto.getRandomValues is the secure source, and rejection sampling avoids the small bias that % on its own would add.

  6. Question 6 of 6 What does this program print on Node.js 24.21.0?

    What does this program print? Choose one answer.

    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');
    }
    Show the answer to question 6

    Answer: it prints

    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

    Adding one value at a time rounds after every step, so ten thousand tenths drift above 1000 and the 1 between 1e20 and -1e20 is lost. Node.js 24.21.0 has no Math.sumPrecise, so the last branch prints the note.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Math.round goes to the nearest whole number with ties towards +∞; Math.floor goes down, Math.ceil up and Math.trunc towards zero.
  • Avoid x | 0 and ~~x for truncating: they wrap anything outside the 32-bit range.
  • toFixed returns a string and rounds the stored binary value, so (1.005).toFixed(2) is '1.00'; adding Number.EPSILON does not repair it. Round the decimal text, format with Intl.NumberFormat, which rounds the digits you see, or keep money in whole cents.
  • A random whole number from min to max is min + Math.floor(Math.random() * (max - min + 1)); inject a seeded generator to test such code.
  • Use crypto.getRandomValues or crypto.randomUUID, never Math.random, for codes, tokens and IDs that protect something.
  • Math.sumPrecise adds numbers exactly and rounds once; check that it exists before relying on it.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.