GUIDE · JavaScript

Numbers & Math

lesson8 sectionsruns in browser90 asserts136 lines

At a glance

DIVIDE
Math.floor(a / b) for ints >= 0; Math.trunc toward zero; % keeps the dividend's sign (use ((a % n) + n) % n for a positive modulo).
PARSE
Number(s) strict ('' -> 0!), parseInt(s, 10) lenient prefix, parseFloat, +s. Number.isNaN not isNaN. Number.isInteger.
FORMAT
toFixed(n) returns a STRING. Math.round rounds .5 toward +Infinity.
LIMITS
Number.MAX_SAFE_INTEGER ~9e15; BigInt (123n) beyond; bitwise ops are 32-bit signed.
SAY
IEEE-754 doublefloating-point errorsafe integerBigIntinteger cents for moneyoverflow doesn't throw — it goes to Infinity

The idea#

Think of every JavaScript number as one measuring tape. There is no second tape for whole numbers and a third for fractions. The tape is an IEEE-754 double, a 64-bit binary stick, and its tick marks are binary fractions. One tenth is a repeating mark on that stick, so 0.1 + 0.2 lands a hair past 0.3. === sees the hair. That miss is floating-point error.

The tape has a reliable stretch for whole numbers and a fuzzy stretch past it. Number.MAX_SAFE_INTEGER is 2^53 - 1, about nine quadrillion. Past that safe integer, two different whole numbers can share a tick. 9007199254740992 === 9007199254740993 is true. When the problem says the values reach 10^18, or you are computing a large factorial, switch to BigInt. You write it with an n: 123n. BigInt division truncates. 7n / 2n is 3n. You add BigInt to BigInt. 1n + 1 throws. Convert on purpose with BigInt(x) or Number(b).

Ordinary division is real division. 7 / 2 is 3.5. You ask for an integer result. Math.floor moves toward negative infinity, so the floor of -3.5 is -4. Math.trunc chops toward zero. The % operator keeps the dividend's sign, so -7 % 3 is -1. A positive modulo is ((a % n) + n) % n. Dividing by zero reports Infinity. 0 / 0 is NaN. Overflow climbs to Infinity the same way. Nothing is thrown.

Why it matters#

Money, scores, and "is this input a number" all fail on this tape if you treat it like a calculator from school.

Store money as integer cents and divide only when you display. (10 + 20) / 100 is 0.3 on the nose, because the addition happened on whole ticks. Compare real fractions by distance: Math.abs(a - b) < Number.EPSILON. toFixed(n) returns a string, and it is a display tool. (1.005).toFixed(2) is '1.00', because 1.005 is already stored as a slightly smaller tick. Math.round sends halves toward positive infinity. Math.round(2.5) is 3. Math.round(-2.5) is -2. That is toward +Infinity, not "away from zero."

Parsing is three tools. Number(s) is strict: '42abc' becomes NaN, while '' and null become 0. parseInt(s, 10) reads a prefix and stops, so '42abc' is 42. Always pass the radix. parseFloat does that for a decimal prefix. A unary + is a short Number. With +, a string partner concatenates: '3' + 4 is '34'. Other operators coerce: '3' * 4 is 12.

Number.isNaN checks the value as it is. The global isNaN('x') converts first and then answers true. Number.isInteger('5') is false. Number.isFinite rejects Infinity. Math.max() with no arguments is -Infinity, and Math.min() with none is Infinity, so guard an empty list before you spread it. JSON stores both Infinity and NaN as null.

Bitwise operators use a smaller tape: 32-bit signed integers. >> keeps the sign. >>> is the unsigned view. ~n is -(n + 1). A positive power of two satisfies (n & (n - 1)) === 0. n & -n isolates the lowest set bit. A shift that walks off that 32-bit end wraps.

A small example#

JS
const tape = 0.1 + 0.2;const sameTick = tape === 0.3;const closeEnough = Math.abs(tape - 0.3) < Number.EPSILON; function totalDollars(cents) {  const sum = cents.reduce((total, c) => total + c, 0);  return sum / 100;} const bill = totalDollars([10, 20]);const rawMod = -7 % 3;const positiveMod = ((-7 % 3) + 3) % 3;

sameTick is false. The binary tape cannot mark 0.1 exactly, so the sum is a hair over 0.3. closeEnough is true. bill is 0.3, because 10 cents and 20 cents were added as integers and divided once at the end. rawMod is -1. positiveMod is 2.

What the code below is doing#

The code below is eight sections on one tape.

Division and modulo cover real division, floor versus trunc, a 32-bit bitwise trunc, the sign of %, a positive mod helper, and Infinity and NaN from dividing by zero. Parsing runs Number across strings, '', null, undefined, and booleans, then parseInt with a radix, parseFloat, unary plus, string + versus *, and Number.isNaN beside the global isNaN. Formatting shows toFixed as a string, the 1.005 case, Math.round toward positive infinity, a two-place helper that adds Number.EPSILON, and base conversion. Floating point checks 0.1 + 0.2, an epsilon comparison, integer cents, and the two integers past MAX_SAFE_INTEGER that compare equal.

BigInt covers powers, truncating division, the throw on 1n + 1, and == versus === with an ordinary 1. The Math grab-bag is abs, sign, empty max and min, roots, logs, digit counts, gcd, lcm, a prime check, digits, clamp, and a note that Math.random is the wrong tool for secrets. Bitwise covers and, or, xor, not, both shifts, 32-bit wrap, a power-of-two test, and the lowest set bit. The last section starts a running max at -Infinity and shows JSON turning Infinity and NaN into null.

The header's warning: everything is a double, so integer division does not exist, and overflow does not throw. It goes to Infinity. Keep money in integer cents. Past the safe integer, use BigInt. Say IEEE-754 double, floating-point error, safe integer, BigInt, and integer cents.

Everything below is the demonstration. Same idea, written as code you can run.

JSjavascript/numbers-math.js · setup
'use strict';const assert = require('node:assert/strict');
01

Division & modulo#

JS
const mod = (a, n) => ((a % n) + n) % n;
Verified by 10 asserts
JS
assert.equal(7 / 2, 3.5);assert.equal(Math.floor(7 / 2), 3);assert.equal(Math.trunc(-7 / 2), -3);                        // floor(-3.5) = -4assert.equal((7 / 2) | 0, 3);                                // bitwise trunc — 32-bit onlyassert.equal(-7 % 3, -1);assert.equal(mod(-7, 3), 2);assert.equal(5 % 1, 0);assert.equal(5.5 % 1, 0.5);                                  // fractional partassert.equal(1 / 0, Infinity);                               // no exceptionassert.equal(Number.isNaN(0 / 0), true);
02

Parsing#

Verified by 19 asserts
JS
assert.equal(Number('42'), 42);assert.equal(Number('42abc'), NaN);assert.equal(Number(''), 0);                                 // gotchaassert.equal(Number(' 12 '), 12);assert.equal(Number(null), 0);assert.equal(Number(undefined), NaN);assert.equal(Number(true), 1);assert.equal(Number('0x1f'), 31);assert.equal(parseInt('42abc', 10), 42);                     // ALWAYS pass radixassert.equal(parseInt('abc', 10), NaN);assert.equal(parseFloat('3.14xyz'), 3.14);assert.equal(+'3' + +'4', 7);                                // unary plusassert.equal('3' + 4, '34');                                 // string wins with +assert.equal('3' * 4, 12);                                   // other operators coerce to numberassert.equal(Number.isNaN('x'), false);                      // Number.isNaN: no coercionassert.equal(isNaN('x'), true);                              // global isNaN coerces — avoidassert.equal(Number.isInteger(5.0), true);assert.equal(Number.isInteger('5'), false);assert.equal(Number.isFinite(1 / 0), false);
03

Formatting#

JS
const round2 = x => Math.round((x + Number.EPSILON) * 100) / 100;
Verified by 12 asserts
JS
assert.equal((2.5).toFixed(2), '2.50');                      // STRINGassert.equal((1234.5678).toFixed(1), '1234.6');assert.equal((1.005).toFixed(2), '1.00');                    // FP: 1.005 is really 1.00499999...assert.equal(Math.round(2.5), 3);assert.equal(Math.round(-2.5), -2);                          // toward +Infinity, not away from zeroassert.equal(Math.round(1.005 * 100) / 100, 1);              // "round to 2 places" — still FP-proneassert.equal(round2(1.005), 1.01);assert.equal((255).toString(16), 'ff');assert.equal((255).toString(2), '11111111');assert.equal((5).toString().padStart(3, '0'), '005');assert.equal((1234567.891).toLocaleString('en-US'), '1,234,567.891');assert.equal(Number((0.1 + 0.2).toFixed(10)), 0.3);
04

Floating point#

Verified by 6 asserts
JS
assert.equal(0.1 + 0.2 === 0.3, false);assert.equal(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON, true);assert.equal((10 + 20) / 100, 0.3);                          // money: integer cents, divide at the endassert.equal(Number.MAX_SAFE_INTEGER, 9007199254740991);     // 2^53 - 1assert.equal(9007199254740992 === 9007199254740993, true);   // precision lost past MAX_SAFE_INTEGERassert.equal(Number.isSafeInteger(2 ** 53), false);
05

BigInt#

when the problem says "up to 10^18" or factorials

Verified by 8 asserts
JS
assert.equal(2n ** 64n, 18446744073709551616n);assert.equal(BigInt(10) ** 18n > BigInt(Number.MAX_SAFE_INTEGER), true);assert.equal(typeof 1n, 'bigint');assert.equal(7n / 2n, 3n);                                   // integer division built inassert.throws(() => 1n + 1);                                 // can't mix; convert with BigInt(x) / Number(b)assert.equal(String(123n), '123');assert.equal(1n == 1, true);                                 // loose equality works; strict doesn'tassert.equal(1n === 1, false);
06

Math grab-bag#

JS
const gcd = (a, b) => (b ? gcd(b, a % b) : Math.abs(a));const lcm = (a, b) => (a / gcd(a, b)) * b;const isPrime = n => { if (n < 2) return false; for (let i = 2; i * i <= n; i++) if (n % i === 0) return false; return true; };const digits = n => [...String(Math.abs(n))].map(Number);const digitSum = n => digits(n).reduce((a, b) => a + b, 0);const reverseNum = n => Math.sign(n) * Number([...String(Math.abs(n))].reverse().join(''));const clamp = (x, lo, hi) => Math.min(hi, Math.max(lo, x));// Random (not for crypto): Math.floor(Math.random() * n) in [0, n). Tokens -> node:crypto randomBytes.
Verified by 21 asserts
JS
assert.equal(Math.abs(-3), 3);assert.equal(Math.sign(-3), -1);assert.equal(Math.max(), -Infinity);                         // of nothing — guard empty arraysassert.equal(Math.min(), Infinity);assert.equal(2 ** 10, 1024);assert.equal(Math.pow(2, 10), 1024);assert.equal(Math.sqrt(16), 4);assert.equal(Math.cbrt(27), 3);assert.equal(Math.hypot(3, 4), 5);assert.equal(Math.ceil(4.1), 5);assert.equal(Math.log2(8), 3);assert.equal(Math.log10(1000), 3);assert.equal(Math.floor(Math.log10(12345)) + 1, 5);          // digit countassert.equal(String(12345).length, 5);                       // digit count, simplerassert.equal(gcd(12, 18), 6);assert.equal(lcm(4, 6), 12);assert.equal(isPrime(97), true);assert.deepEqual(digits(-402), [4, 0, 2]);assert.equal(digitSum(29), 11);assert.equal(reverseNum(-120), -21);assert.equal(clamp(15, 0, 10), 10);
07

Bitwise#

32-bit SIGNED; >>> is unsigned

Verified by 11 asserts
JS
assert.equal(5 & 3, 1);assert.equal(5 | 3, 7);assert.equal(5 ^ 3, 6);assert.equal(~5, -6);                                        // -(n+1)assert.equal(1 << 3, 8);assert.equal(-8 >> 1, -4);                                   // arithmetic shift keeps signassert.equal(-1 >>> 0, 4294967295);                          // unsigned viewassert.equal(2 ** 31 | 0, -2147483648);                      // overflow wraps in bitwise opsassert.equal((8 & (8 - 1)) === 0, true);                     // power of twoassert.equal(6 & -6, 2);                                     // lowest set bitassert.equal((13).toString(2).split('1').length - 1, 3);     // popcount the lazy way
08

Infinity / sentinels#

JS
let best = -Infinity;for (const x of [3, 9, 2]) best = Math.max(best, x);
Verified by 3 asserts
JS
assert.equal(best, 9);assert.equal(Infinity > Number.MAX_VALUE, true);assert.equal(JSON.stringify({ a: Infinity, b: NaN }), '{"a":null,"b":null}');   // JSON can't carry them

See also