JavaScript · Syntax · Intermediate

Regular expressions

10 tasks

Pattern matching with RegExp. Covers test, match, replace, and named capture groups.

Regex fundamentals in JavaScript

#
A regular expression in JavaScript is an object that describes a text pattern. You create one with a literal `/pattern/flags` or with `new RegExp('pattern', 'flags')`. **The three most-used methods** ```javascript const text = 'User: alice, age 30'; /\d+/.test(text) // true - does the pattern appear anywhere? text.match(/\d+/) // ['30'] - first match as array (or null) text.replace(/\d+/, '??') // 'User: alice, age ??' - replace first match text.replace(/\d+/g, '??')// 'User: alice, age ??' - replace ALL (g flag) ``` **Reading a pattern character by character** Take `/([\w.]+)@([\w.]+\.\w{2,})/` (a simple email pattern): ``` ( open capturing group 1 [\w.]+ one or more: word chars or literal dot ) close group 1 @ literal "@" ( open capturing group 2 [\w.]+ one or more: word chars or literal dot \. literal dot (escaped - plain . means "any char") \w{2,} 2 or more word characters (the TLD) ) close group 2 ``` ```javascript const m = '[email protected]'.match(/([\w.]+)@([\w.]+\.\w{2,})/); m[0] // '[email protected]' - full match m[1] // 'bob' - group 1 m[2] // 'example.com' - group 2 ``` **Regex literal vs new RegExp** Prefer the literal `/pattern/` — it's compiled once and doesn't need string escaping. Use `new RegExp(str)` only when the pattern is built at runtime: ```javascript // Good: literal const valid = /^\d{3}-\d{4}$/; // When pattern comes from a variable: function buildSearch(word) { const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(escaped, 'gi'); } ``` **The `lastIndex` gotcha with stateful regex** When you use the `g` or `y` flag and call `.test()` or `.exec()` on the *same* regex object multiple times, the engine remembers where it left off via `regex.lastIndex`. This causes a surprising alternating true/false pattern: ```javascript const re = /\d+/g; // stateful because of 'g' re.test('abc 42 def'); // true (found '42', lastIndex -> 6) re.test('abc 42 def'); // false (resumes from index 6, finds nothing) re.test('abc 42 def'); // true (lastIndex reset to 0 after miss) // Safe pattern: create a fresh regex each time, or reset manually re.lastIndex = 0; re.test('abc 42 def'); // true (always) ``` The safest rule: use the `g` flag only with `str.match()`, `str.matchAll()`, or `str.replaceAll()`, not with repeated `.test()` calls on the same regex object.

Capturing groups, named groups, and matchAll

#
**Capturing groups** let you extract parts of a match. The result of `.match()` without the `g` flag includes the full match at index 0, then each group. ```javascript const log = '2024-03-15 ERROR disk full'; const m = log.match(/(\d{4}-\d{2}-\d{2}) (\w+) (.+)/); m[1] // '2024-03-15' m[2] // 'ERROR' m[3] // 'disk full' ``` **Named groups** `(?<name>...)` — access via `m.groups.name`: ```javascript const m = log.match(/(?<date>\d{4}-\d{2}-\d{2}) (?<level>\w+) (?<msg>.+)/); m.groups.date // '2024-03-15' m.groups.level // 'ERROR' m.groups.msg // 'disk full' ``` Named groups are also available in `replace` via `$<name>`: ```javascript '2024-03-15'.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, '$<d>/$<m>/$<y>') // '15/03/2024' ``` **replace with a function** When the replacement is a function, it receives `(fullMatch, ...groups, offset, originalStr)`: ```javascript 'hello world'.replace(/(\w+)/g, (match) => match.toUpperCase()) // 'HELLO WORLD' 'a=3, b=7'.replace(/(\w+)=(\d+)/g, (_, key, val) => `${key}=${+val * 2}`) // 'a=6, b=14' ``` **matchAll — iterate over all group matches** `.match()` with `g` flag returns only the full matches (groups are dropped). Use `str.matchAll(regex)` to get all matches *with* their groups: ```javascript const text = 'alice=30, bob=25'; const re = /(\w+)=(\d+)/g; for (const m of text.matchAll(re)) { console.log(m[1], m[2]); // 'alice' '30' then 'bob' '25' } // or collect results: const pairs = [...text.matchAll(re)].map(m => ({ name: m[1], age: +m[2] })); // [{ name: 'alice', age: 30 }, { name: 'bob', age: 25 }] ``` `matchAll` requires the `g` flag and returns an iterator, so spread it or iterate with `for...of`. **Non-capturing groups** `(?:...)` group without a capture index — useful for alternation: ```javascript // Match 'colour' or 'color' — the 'u?' is just quantified, not captured 'colour and color'.match(/colou?r/g) // ['colour', 'color'] // Alternation without a capture group 'cats and dogs'.match(/(?:cat|dog)s?/g) // ['cats', 'dogs'] ```

Lookahead, lookbehind, greedy/lazy, and dynamic regex

#
**Lookahead** asserts what comes *after* the current position without consuming it. ```javascript // Positive lookahead (?=...) - match X only when followed by Y '100px 200em 300px'.match(/\d+(?=px)/g) // ['100', '300'] (numbers before 'px') // Negative lookahead (?!...) - match X only when NOT followed by Y '100px 200em'.match(/\d+(?!px)/g) // ['200'] (numbers not before 'px') ``` **Lookbehind** (ES2018) asserts what comes *before*: ```javascript // Positive lookbehind (?<=...) - match X only when preceded by Y '$100 EUR200'.match(/(?<=\$)\d+/g) // ['100'] (numbers after '$') // Negative lookbehind (?<!...) - match X only when NOT preceded by Y '$100 EUR200'.match(/(?<!\$)\d+/g) // ['200'] (numbers not after '$') ``` **Greedy vs lazy** Quantifiers are greedy by default. Add `?` to make them lazy: ```javascript const html = '<b>bold</b> and <i>italic</i>'; html.match(/<.+>/g) // ['<b>bold</b> and <i>italic</i>'] greedy - one big match html.match(/<.+?>/g) // ['<b>', '</b>', '<i>', '</i>'] lazy - each tag ``` **Escaping user input for dynamic regex** If you build a regex from a user-supplied string, you must escape all special regex characters first — otherwise a user typing `.` or `*` breaks your pattern: ```javascript function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function highlight(text, query) { const re = new RegExp(escapeRegex(query), 'gi'); return text.replace(re, '<mark>$&</mark>'); } highlight('Price: 2.50', '2.50') // 'Price: <mark>2.50</mark>' // without escaping, '.' would match any char and could over-match ``` **Common pitfalls** - Forgetting the `g` flag: `'aaa'.replace(/a/, 'b')` gives `'baa'`, not `'bbb'` - `.` does not match newlines by default — use `[\s\S]` or the `s` flag (`/pattern/s`) - `^` and `$` match string boundaries, not line boundaries, unless you use the `m` flag - Using `.test()` in a loop with the same `g` regex causes `lastIndex` drift (see Block 1) - Regex cannot parse HTML — use the DOM instead

Common regex patterns cookbook (JavaScript)

#
A ready-to-use collection of patterns for common tasks in JavaScript. **Email (simple)** ```javascript const EMAIL = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g; '[email protected], [email protected]'.match(EMAIL) // ['[email protected]', '[email protected]'] ``` **ISO date** ```javascript const ISO = /\b(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])\b/g; 'Event on 2024-03-15, deadline 2024-12-31'.match(ISO) // ['2024-03-15', '2024-12-31'] ``` **URL** ```javascript const URL_RE = /https?:\/\/[^\s<>'"]+/g; 'See https://developer.mozilla.org for more'.match(URL_RE) // ['https://developer.mozilla.org'] ``` **Slug (lowercase-with-dashes)** ```javascript function toSlug(str) { return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } toSlug('Hello World! 42') // 'hello-world-42' ``` **Camel to snake** ```javascript function camelToSnake(str) { return str.replace(/([A-Z])/g, '_$1').toLowerCase(); } camelToSnake('myVariableName') // 'my_variable_name' ``` **Mask credit card (show last 4)** ```javascript '4111 1111 1111 1234'.replace(/\d(?=\d{4})/g, '*') // '**** **** **** 1234' ``` **Quick flags reference** ``` g global - find all matches (not just first) i case-insensitive m multiline - ^ and $ match line boundaries s dotAll - . matches newlines too u unicode - enables \u{...} escapes and Unicode property classes d hasIndices - match.indices[n] gives [start, end] per group ``` **When NOT to use regex in JavaScript** - Parsing HTML - use `document.querySelector`, `DOMParser`, or a library - Parsing URLs - use `new URL(str)` - Parsing JSON - use `JSON.parse` - Validating email strictly - regex cannot enforce RFC 5322; use a library Regex shines for scanning and transforming plain text. For structured formats, always prefer the dedicated parser.
01

#

Write a function that tests whether a string is a valid email address (contains @, a domain, and a dot in the domain part).

function isValidEmail(str) {
    // your code here
}

console.log(isValidEmail('[email protected]'));   // true
console.log(isValidEmail('user@example'));        // false
console.log(isValidEmail('notanemail'));          // false
console.log(isValidEmail('[email protected]'));              // true
Solution
function isValidEmail(str) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}
02

#

Extract all words from a string that start with a capital letter. Return them as an array.

function capitalWords(str) {
    // your code here
}

console.log(capitalWords('Hello world, My Name is Alice'));  // ['Hello', 'My', 'Name', 'Alice']
console.log(capitalWords('no capitals here'));               // []
Solution
function capitalWords(str) {
    return str.match(/\b[A-Z][a-zA-Z]*/g) || [];
}
03

#

Replace all sequences of whitespace (spaces, tabs, newlines) in a string with a single space, and trim the result.

function normalizeSpaces(str) {
    // your code here
}

console.log(normalizeSpaces('  hello   world  '));         // 'hello world'
console.log(normalizeSpaces('line1\n\nline2\t  line3')); // 'line1 line2 line3'
Solution
function normalizeSpaces(str) {
    return str.replace(/\s+/g, ' ').trim();
}
04

#

Extract all numbers (integers and decimals) from a string and return them as an array of numbers.

function extractNumbers(str) {
    // your code here
}

console.log(extractNumbers('Price: 12.5, qty: 3, discount: 0.15'));  // [12.5, 3, 0.15]
console.log(extractNumbers('no numbers here'));                       // []
Solution
function extractNumbers(str) {
    const matches = str.match(/\d+\.?\d*/g);
    return matches ? matches.map(Number) : [];
}
05

#

Use a capturing group to extract the username and domain from an email address string.

function parseEmail(email) {
    // Return { username, domain } or null if not valid
}

console.log(parseEmail('[email protected]'));   // { username: 'user', domain: 'example.com' }
console.log(parseEmail('notanemail'));          // null
Solution
function parseEmail(email) {
    const match = email.match(/^([^\s@]+)@([^\s@]+)$/);
    if (!match) return null;
    return { username: match[1], domain: match[2] };
}
06

#

Write a function that converts a camelCase string to snake_case (e.g., 'myVariableName' → 'my_variable_name').

function camelToSnake(str) {
    // your code here
}

console.log(camelToSnake('myVariableName'));    // 'my_variable_name'
console.log(camelToSnake('helloWorld'));        // 'hello_world'
console.log(camelToSnake('alreadylower'));      // 'alreadylower'
Solution
function camelToSnake(str) {
    return str.replace(/([A-Z])/g, '_$1').toLowerCase();
}
07

#

Count how many times a word appears in a string (case-insensitive, whole words only).

function countWord(str, word) {
    // your code here
}

console.log(countWord('The cat sat on the mat. The cat is fat.', 'the'));  // 3
console.log(countWord('cats and cat and cat', 'cat'));                      // 3 (not 'cats')
Solution
function countWord(str, word) {
    const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const re = new RegExp(`\\b${escaped}\\b`, 'gi');
    return (str.match(re) || []).length;
}
08

#

Split a string of comma/semicolon/space-separated values into an array of trimmed non-empty tokens.

function splitTokens(str) {
    // your code here
}

console.log(splitTokens('a, b;  c , d'));    // ['a', 'b', 'c', 'd']
console.log(splitTokens('one;two,three'));   // ['one', 'two', 'three']
console.log(splitTokens('  , ; '));          // []
Solution
function splitTokens(str) {
    return str.split(/[,;\s]+/).map(t => t.trim()).filter(Boolean);
}
09

#

Use regex to mask all but the last four digits of a credit card number (replace with '*').

function maskCard(number) {
    // your code here
}

console.log(maskCard('1234567890123456'));  // '************3456'
console.log(maskCard('4111 1111 1111 1111')); // '**** **** **** 1111'
Solution
function maskCard(number) {
    return number.replace(/\d(?=\d{4})/g, '*');
}
10

#

Extract all URLs from a string. A URL starts with http:// or https:// and ends at whitespace or end of string.

function extractUrls(str) {
    // your code here
}

const text = 'Visit https://example.com and http://foo.org/path?q=1 for details.';
console.log(extractUrls(text));
// ['https://example.com', 'http://foo.org/path?q=1']
Solution
function extractUrls(str) {
    return str.match(/https?:\/\/[^\s]+/g) || [];
}