GUIDE · JavaScript
Numbers & Math
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#
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.
'use strict';const assert = require('node:assert/strict');Division & modulo#
const mod = (a, n) => ((a % n) + n) % n;Verified by 10 asserts
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);Parsing#
Verified by 19 asserts
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);Formatting#
const round2 = x => Math.round((x + Number.EPSILON) * 100) / 100;Verified by 12 asserts
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);Floating point#
Verified by 6 asserts
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);BigInt#
when the problem says "up to 10^18" or factorials
Verified by 8 asserts
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);Math grab-bag#
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
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);Bitwise#
32-bit SIGNED; >>> is unsigned
Verified by 11 asserts
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 wayInfinity / sentinels#
let best = -Infinity;for (const x of [3, 9, 2]) best = Math.max(best, x);Verified by 3 asserts
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 themSee also