B8 · Part 1 · Beginner
Strings, numbers & Math
The everyday toolbox every other chapter quietly assumes you already have.
07On this page
String methods beyond the basics
The types chapter already covered
slice, trim, includes and
split. These come up just as often and didn't fit there.
"Hello World".startsWith("Hello"); // true
"Hello World".endsWith("World"); // true
"Hello World".startsWith("World", 6); // true — search starts at index 6, not the very start
"7".padStart(3, "0"); // "007" — pad on the LEFT to a minimum length
"7".padEnd(3, "."); // "7.." — pad on the RIGHT
"ha".repeat(3); // "hahaha"
padStart is the standard fix for "always show two
digits" — String(minutes).padStart(2, "0") turns
5 into "05" — without hand-rolling an
if (x < 10) check.
Case, and the string methods everyone assumes you know
"Hello".toUpperCase(); // "HELLO"
"Hello".toLowerCase(); // "hello"
"Hello".at(-1); // "o" — negative indices count from the end, same idea as array .at()
"Hello"[-1]; // undefined — plain bracket access does NOT support negative indices
.at() exists on strings for exactly the reason it exists
on arrays: str[str.length - 1] for "the last character"
works, but it's a mouthful next to str.at(-1), and plain
bracket indexing has no concept of counting from the end at all — it
just returns undefined for any negative index.
⚠ toUpperCase/toLowerCase are not always safe for comparison
A handful of characters change length when case-folded — the
German
ß becomes "SS" under
.toUpperCase(). Comparing user input case-insensitively
is usually safer with
a.localeCompare(b, undefined, { sensitivity: "base" }),
covered next, than with a manual .toLowerCase() === .toLowerCase().
Comparing text properly
console.log(["résumé", "resume", "zebra"].sort());
console.log(["résumé", "resume", "zebra"].sort((a, b) => a.localeCompare(b)));
A plain .sort() with no comparator compares strings by
raw UTF-16 code unit — accented characters and non-Latin scripts
routinely land in the wrong place. localeCompare sorts
the way a human reading that language actually expects, and takes an
optional locale and options argument
(a.localeCompare(b, "en", { sensitivity: "base" })) for
case-insensitive, accent-insensitive comparison in one call instead
of lowercasing both sides by hand.
Math — the methods that actually get used
| Call | Does |
|---|---|
Math.round(x) | nearest integer — .5 rounds up |
Math.floor(x) | rounds down, always — toward negative infinity |
Math.ceil(x) | rounds up, always |
Math.trunc(x) | drops the decimal part — rounds toward zero |
Math.abs(x) | absolute value |
Math.min(...) / Math.max(...) | smallest/largest of the arguments — not an array; spread one in |
Math.random() | a float, from 0 up to (never including) 1 |
console.log(Math.floor(-4.5));
console.log(Math.trunc(-4.5));
-5, then -4 — the difference only shows up
on negative numbers. floor always moves toward negative
infinity; trunc always moves toward zero. Reaching for
the wrong one is a real, if rare, off-by-one source once negative
values are involved.
A random integer in a range
function randomInt(min, max) { // inclusive of both min and max
return Math.floor(Math.random() * (max - min + 1)) + min;
}
randomInt(1, 6); // a dice roll — 1 through 6, inclusive
Math.random() alone gives a float between 0 and just
under 1 — never useful on its own. Multiplying by the size of the
range and flooring turns that float into one of exactly
max - min + 1 whole numbers; the final + min
shifts the whole range to start at the right place instead of at 0.
This exact formula is worth memorizing — it gets asked for cold often
enough that deriving it live, under pressure, is its own small trap.
Dry run: randomInt(1, 6) for four sample Math.random() values
| Math.random() returns | × (max − min + 1) = × 6 | Math.floor(...) | + min (1) | Result |
|---|---|---|---|---|
| 0 | 0 | 0 | +1 | 1 |
| 0.166... | 0.996... | 0 | +1 | 1 |
| 0.5 | 3.0 | 3 | +1 | 4 |
| 0.999999 | 5.999994 | 5 | +1 | 6 |
Math.random() gets arbitrarily close to 1 but never
reaches it, so Math.floor(random * 6) only ever lands on
0 through 5 — six whole numbers — and + min shifts that
range to land on 1 through 6. Drop the + 1 inside the
multiply and 6 becomes unreachable; drop the Math.floor
and almost every roll is a decimal.
Math.sign, Math.hypot, and checking a number is actually safe
Math.sign(-5); // -1
Math.sign(0); // 0
Math.sign(7); // 1
Math.hypot(3, 4); // 5 — the hypotenuse: sqrt(3**2 + 4**2), without writing the formula out
Math.sign is the direct answer to "is this positive,
negative, or zero" without a chain of > 0 /
< 0 comparisons. Math.hypot shows up
constantly in anything involving distance — two points on a canvas,
a drag gesture's total movement — as the built-in alternative to
Math.sqrt(a ** 2 + b ** 2).
console.log(Number.isInteger(5), Number.isInteger(5.5), Number.isInteger("5"));
console.log(isFinite("123"), Number.isFinite("123"));
true false false, then true false. The
global isFinite/isNaN coerce their argument
to a number first — "123" passes. The Number.
versions refuse to coerce at all: a string is never a number to
Number.isInteger or Number.isFinite, full
stop, no matter what it contains. The Number. versions
are the safer default for exactly that reason — they answer "is this
actually a number with this property," not "would this convert into
one."
Number.isSafeInteger(n) goes one step further, checking
against the exact-representable limit
covered in Types & data — useful the moment a value might have
come from parsing JSON that originated outside JavaScript, where a
huge integer id can silently lose precision on the way in.
Formatting numbers for display
(1234.5).toFixed(2); // "1234.50" — a STRING, fixed decimal places, no thousands separator
(1234.5).toLocaleString("en-IN"); // "1,234.5" — locale-correct digit grouping
(1234.5).toLocaleString("en-IN", { style: "currency", currency: "INR" });
// "₹1,234.50"
⚠ toFixed rounds, and always returns a string
(1.005).toFixed(2) is "1.00", not
"1.01" — the same floating-point imprecision from
the types chapter means
1.005 isn't quite exactly representable to begin with.
And the return value is always a string —
(5).toFixed(2) + 1 is "5.001",
concatenation, not addition, unless it's converted back with
Number(...) first.
toFixed and toLocaleString solve different
problems: toFixed guarantees an exact number of decimal
places with no locale awareness at all; toLocaleString
formats for a specific reader's language and region (digit grouping,
currency symbol placement) but won't force a fixed decimal count
unless told to. For money specifically,
Intl.NumberFormat (covered in
Regex & dates) is the same
formatting engine toLocaleString calls internally —
worth reaching for directly when formatting many values with the
same options at once.
Interview answer, one sentence
"Strings are immutable UTF-16 sequences, so their methods return new strings; numbers are IEEE 754 doubles, so 0.1 + 0.2 is not 0.3 and money belongs in integers; and Intl formats numbers, dates and currency for the user's locale without hand-rolled code."
Before you move on
- Write
String(minutes).padStart(2, "0")from memory, and explain what problem it solves. - Explain why
Math.floor(-4.5)andMath.trunc(-4.5)give different answers, and state the rule each one follows. - Derive the
randomInt(min, max)formula from scratch, and say what breaks if you drop the+ 1or theMath.floor. - Explain why
Number.isInteger("5")isfalsewhile the globalisFinite("123")istrue— what's different about how each one treats a string. - Explain why
(1.005).toFixed(2)is"1.00", and why the return value is always a string.
Opens in the editor: write it, run it, and check it against real tests.
All 35 for this level →Finished reading?
0 of 41 chapters read in this series