Most regex pieces match characters. Assertions match a position instead: the start of the text, the edge of a word, "right before a digit." They check a condition without consuming any characters, which lets you say where a match is allowed to happen.
/^Hello/.test("Hello there") // true starts with Hello
/^Hello/.test("Oh, Hello") // false
/\.pdf$/.test("notes.pdf") // true ends with .pdf
Using both anchors means "the whole text must match," which is what you want for validation:
/\d{3}/.test("abc12345") // true (3 digits appear somewhere)
/^\d{3}$/.test("abc12345") // false (the whole thing is not 3 digits)
/^\d{3}$/.test("123") // true
^ and $. Without them, /\d{5}/ happily accepts "my zip is 212019999 thanks" because five digits appear somewhere inside.
\b matches the edge between a word character and a non-word character (or the start/end of the text). It is how you match whole words only:
const text = "cat concatenate bobcat cat.";
text.match(/cat/g).length // 4 (inside other words too)
text.match(/\bcat\b/g).length // 2 (only the standalone word)
// Replace the word "is", but not the "is" inside "This" or "island"
"This is an island".replace(/\bis\b/g, "IS"); // "This IS an island"
\B is the opposite: a position that is not a word edge.
"cat concatenate".match(/\Bcat\B/g) // ["cat"] only the one buried inside a word
\b works well with search() to find where a word starts or ends:
const text = "HELLO, LOOK AT YOU!";
console.log(text.search(/\bLO/)); // LO at the START of a word: LOOK
console.log(text.search(/LO\b/)); // LO at the END of a word: HELLO
\b is defined by \w, so accented letters count as non-word characters. /\bcafé\b/.test("café") is false, even with the u flag, because there is no "edge" between é and the end of the text. For text in other languages, use lookarounds with \p{L} instead: /(?<!\p{L})café(?!\p{L})/u.
With m, the anchors work on every line (see RegExp Flags):
const poem = "Is this\nall there\nis";
console.log(poem.match(/^is/gim));
A lookahead checks what comes next without including it in the match:
// (?=...) positive: must be followed by
"100px 20em 5px".match(/\d+(?=px)/g) // ["100", "5"] numbers followed by px
// (?!...) negative: must NOT be followed by
"100px 20em 5px".match(/\b\d+(?!\d|px)/g) // ["20"]
Notice the matches do not include "px". The lookahead only peeked.
The same idea, checking what came before:
// (?<=...) must be preceded by
"Cost: $45, tax $3, qty 7".match(/(?<=\$)\d+/g) // ["45", "3"] amounts after $
// (?<!...) must NOT be preceded by
"Cost: $45, tax $3, qty 7".match(/(?<![\$\d])\d+/g) // ["7"]
| ^ $ | Start / end of text (of each line with the m flag) |
| \b \B | Word edge / not a word edge |
| X(?=Y) | X, only if followed by Y |
| X(?!Y) | X, only if NOT followed by Y |
| (?<=Y)X | X, only if preceded by Y |
| (?<!Y)X | X, only if NOT preceded by Y |
Lookaheads can stack at the start of a pattern, each checking one rule without moving forward:
const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
// has lower has upper has digit 8+ chars total
strong.test("password") // false (no capital, no digit)
strong.test("Password1") // true
strong.test("Pass1") // false (too short)
const withCommas = n => String(n).replace(/\B(?=(\d{3})+$)/g, ",");
withCommas(1234567) // "1,234,567"
Read it as: "a position inside the number, where the digits after it come in groups of exactly three up to the end." Insert a comma at each such position. (For real apps, toLocaleString() does this for you.)
What does this log?
console.log("$5 or 5 euros or $50".match(/(?<=\$)\d+/g));
["5", "50"]. The lookbehind requires a $ right before the digits, but does not include it in the match.Why should a zip code check use /^\d{5}$/ instead of /\d{5}/?
\d{5} is a SyntaxError without anchors/\d{5}/ accepts "abc123456xyz". The anchors force the whole text to be exactly 5 digits.How many characters does (?=px) add to a match?
!@#$