JavaScript · Lesson 19

JavaScript Regular Expressions: Patterns · Flags · Characters · Meta Characters · Assertions · Quantifiers · Groups · RegExp Object · Methods

12 phases  ·  Build: 🔵 Stage 1 · Validation Engine

👋 Welcome to Lesson 19

Work through each phase in order. Complete the task before unlocking the next. Your Build It project unlocks when all phases are done.

📚 12 phases🏗️ 🔵 Stage 1 · Validation Engine🌐 GitHub Pages
Phase 1 of 12
1. Background: What Are Regular Expressions?

Imagine you receive a form submission with thousands of email addresses and need to check which ones are valid. Or you have a document full of phone numbers in inconsistent formats · (080) 123-4567, 080-123-4567, 08012345678 · and need to extract and standardise them all.

Doing this with normal string methods (indexOf, includes, slice) would require dozens of lines of complex logic. A regular expression (RegExp or regex) solves it in a single pattern.

javascript
// ❌ Without regex — checking one simple email condition takes many lines
function roughEmailCheck(email) {
  const hasAt    = email.includes("@");
  const hasDot   = email.includes(".");
  const atIndex  = email.indexOf("@");
  const dotAfter = email.lastIndexOf(".") > atIndex;
  return hasAt && hasDot && dotAfter;
}

// ✅ With regex — a full email pattern in one expression
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
console.log(emailPattern.test("alice@example.com")); // true
console.log(emailPattern.test("not-an-email"));       // false

A regular expression is a sequence of characters that defines a search pattern. It describes what to look for in a string · not by spelling out exact text, but by specifying a pattern of characters.

What Can You Do with Regular Expressions?

TaskExample
Test if a string matches a patternIs this a valid email?
Find all matches in a stringFind every phone number in a document
Extract specific parts of a matchGet the area code from each phone number
Replace matched textCensor words, reformat dates
Split a string on a patternSplit CSV respecting quoted commas
Validate user inputForms, APIs, data pipelines

🏢 REAL WORLD: Regex is used by every major programming language and tool. In professional JavaScript, it appears in form validation, search engines, URL routers, log parsers, code editors (search & replace), data cleaning pipelines, and security filters (blocking SQL injection patterns).


Anatomy of a Regular Expression

code
/pattern/flags
code
  /  \d{3}-\d{4}  /  g
  ↑   ↑           ↑  ↑
  |   pattern     |  flag(s)
  delimiter       delimiter
  • The delimiters (/) mark the start and end · like quotes for strings
  • The pattern is what to search for
  • The flags (optional) modify how the search works (e.g. case-insensitive, global)


✏️ Your Task
Practise what you just learned about 1. Background: What Are Regular Expressions?. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 2 of 12
2. Topic 1 · Creating Regular Expressions

Phase 1 · Conceptual Understanding

There are two ways to create a regex in JavaScript. Understanding the difference is important for choosing the right one.


Method 1 · Literal Syntax (Preferred)

Use forward slashes to wrap the pattern. The pattern is compiled at load time · faster and cleaner for fixed patterns.

javascript
const pattern = /hello/;

console.log(pattern.test("say hello world")); // true
console.log(pattern.test("say goodbye"));     // false

💡 TIP: Always prefer literal syntax unless the pattern is built dynamically at runtime. Literals are faster, cleaner, and checked for syntax errors when the script loads.


Method 2 · new RegExp() Constructor

Passes the pattern as a string. The pattern is compiled at runtime · required when the pattern is built from variables.

javascript
// Static pattern — same as /hello/
const pattern1 = new RegExp("hello");

// Dynamic pattern — built from a variable
const word    = "world";
const pattern2 = new RegExp(word);

console.log(pattern1.test("say hello")); // true
console.log(pattern2.test("hello world")); // true

⚠️ WATCH OUT · Escaping in Constructor Strings: In a regex literal, \d means "a digit". In a constructor string, the backslash must be doubled because \d in a JavaScript string is just d (unrecognised escape).

javascript
// These are EQUIVALENT:
const a = /\d+/;             // literal — \d is the regex metachar
const b = new RegExp("\\d+"); // string — \\ is needed to produce \d

console.log(a.test("abc123")); // true
console.log(b.test("abc123")); // true

Adding Flags at Creation

javascript
// Literal — flags after the closing slash
const lit = /hello/gi;

// Constructor — flags as second argument
const con = new RegExp("hello", "gi");

// Both are identical — case-insensitive, global match
console.log("Hello World, hello!".match(lit)); // ["Hello", "hello"]
console.log("Hello World, hello!".match(con)); // ["Hello", "hello"]


✏️ Your Task
Practise what you just learned about 2. Topic 1 · Creating Regular Expressions. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 3 of 12
3. Topic 2 · Flags (Modifiers)

Phase 1 · Conceptual Understanding

Flags change how the entire regex engine behaves. They are placed after the closing / in a literal, or as the second argument to new RegExp().


i · Case-Insensitive

Makes the entire match ignore uppercase vs lowercase.

javascript
const pattern = /hello/i;

console.log(pattern.test("Hello"));    // true
console.log(pattern.test("HELLO"));    // true
console.log(pattern.test("hElLo"));    // true
console.log(pattern.test("goodbye")); // false

Without vs with i:

javascript
console.log(/Cat/.test("I have a cat")); // false — uppercase C doesn't match lowercase c
console.log(/Cat/i.test("I have a cat")); // true  — i flag ignores case

g · Global (Find ALL Matches)

Without g, methods like match() return only the first match. With g, they return all matches.

javascript
const text = "cat and Cat and CAT";

// Without g — only first match
console.log(text.match(/cat/i));  // ["cat"]  (just first)

// With gi — ALL matches
console.log(text.match(/cat/gi)); // ["cat", "Cat", "CAT"]

⚠️ WATCH OUT · g flag and test(): When you use g with a regex stored in a variable and call .test() repeatedly, the regex remembers its last position (lastIndex). This causes alternating true/false results in a loop!

javascript
const re = /cat/g;
console.log(re.test("cat"));   // true  (lastIndex → 3)
console.log(re.test("cat"));   // false (search starts at 3, past end!)
console.log(re.test("cat"));   // true  (lastIndex reset to 0)

// Fix: use .test() without g, or reset lastIndex manually
re.lastIndex = 0; // reset before reuse

m · Multiline

Changes the meaning of ^ (start) and $ (end) anchors. Without m, ^ and $ match the start/end of the entire string. With m, they match the start/end of each line.

javascript
const text = "first line\nsecond line\nthird line";

// Without m — ^ only matches start of entire string
console.log(text.match(/^\w+/));  // ["first"]

// With m — ^ matches start of EACH line
console.log(text.match(/^\w+/gm)); // ["first", "second", "third"]

s · Dot-All (Single-Line)

By default, the . metacharacter matches any character except newlines. The s flag makes . match newlines too.

javascript
const text = "Hello\nWorld";

console.log(/Hello.World/.test(text));  // false — . doesn't match \n
console.log(/Hello.World/s.test(text)); // true  — s flag makes . match \n

d · Generate Indices

Makes exec() and match() include indices · the start/end positions of each match and capture group.

javascript
const match = /(\w+)/.exec("hello world");
console.log(match.indices); // undefined — no d flag

const matchD = /(\w+)/d.exec("hello world");
console.log(matchD.indices); // [[0,5],[0,5]] — [full match range, group 1 range]

u · Unicode Mode

Enables full Unicode support, including matching Unicode code points above U+FFFF and making \p{...} Unicode property escapes available.

javascript
// Without u — \uD83D\uDC4D is a surrogate pair (emoji 👍)
console.log(/^.$/.test("👍"));    // false — emoji is 2 "characters" without u
console.log(/^.$/u.test("👍"));   // true  — u mode treats it as 1 code point

// Unicode property escapes (requires u flag)
console.log(/\p{Letter}/u.test("A"));  // true
console.log(/\p{Emoji}/u.test("👍"));  // true

v · Unicode Sets Mode (ES2024)

An upgrade to u mode · enables set operations inside character classes ([A · B], [A&&B]), Unicode property strings, and improved Unicode handling.

javascript
// Intersection — match characters that are both letters AND ASCII
const asciiLetter = /[\p{Letter}&&\p{ASCII}]/v;
console.log(asciiLetter.test("A")); // true
console.log(asciiLetter.test("é")); // false — not ASCII

y · Sticky

The match must start at exactly lastIndex · it does not search forward. Used for incremental parsing.

javascript
const re = /\d+/y;
re.lastIndex = 4;

console.log(re.exec("abc 123 456")); // ["123"] — found at exactly index 4
re.lastIndex = 0;
console.log(re.exec("abc 123 456")); // null — position 0 is 'a', not a digit

Flags Summary Table

FlagNameEffect
iCase-insensitiveIgnore uppercase/lowercase
gGlobalFind ALL matches (not just first)
mMultiline^ and $ match each line
sDot-all. matches newlines too
dIndicesInclude start/end positions of matches
uUnicodeFull Unicode support + \p{} escapes
vUnicode setsSet operations in [], Unicode strings
yStickyMatch only at exact lastIndex position

Combining Multiple Flags

Flags can be combined in any order:

javascript
// Case-insensitive AND global AND multiline
const pattern = /^hello/gim;

const text = "Hello World\nhello there\nHELLO!";
console.log(text.match(pattern)); // ["Hello", "hello", "HELLO"]


✏️ Your Task
Practise what you just learned about 3. Topic 2 · Flags (Modifiers). Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 4 of 12
4. Topic 3 · Character Classes & Ranges

Phase 1 · Conceptual Understanding

A character class (also called a character set) lets you match any one character from a defined group. They are written inside square brackets [...].


[abc] · Match Any Listed Character

Matches ONE character that is a, b, or c · any single character in the list.

javascript
// [aeiou] matches any single vowel
const vowel = /[aeiou]/;

console.log(vowel.test("apple")); // true  (contains 'a')
console.log(vowel.test("sky"));   // false (no vowels)
console.log(vowel.test("gym"));   // false ('y' not in class)

Find ALL vowels with g:

javascript
const text = "Hello World";
console.log(text.match(/[aeiou]/gi)); // ["e", "o", "o"]

▶ Expected Output: ["e", "o", "o"]


[^abc] · Negated Character Class · Match Anything EXCEPT Listed

The ^ inside [...] negates the class · matches any character NOT in the list.

javascript
// [^aeiou] matches any NON-vowel character
console.log("Hello".match(/[^aeiou]/gi)); // ["H", "l", "l"]

// [^0-9] matches any non-digit character
console.log("a1b2c3".match(/[^0-9]/g));  // ["a", "b", "c"]

⚠️ WATCH OUT: ^ inside [...] means negation. ^ outside [...] means start-of-string anchor. Two completely different meanings!

javascript
/^[abc]/  // Starts with a, b, or c
/[^abc]/  // Any character EXCEPT a, b, c

[a-z] · Character Range

A hyphen - inside [...] defines a range. Matches any character between the two endpoints (inclusive), based on Unicode code points.

javascript
// Lowercase letters
console.log(/[a-z]/.test("hello"));  // true
console.log(/[a-z]/.test("HELLO"));  // false — uppercase

// Uppercase letters
console.log(/[A-Z]/.test("Hello"));  // true

// Digits
console.log(/[0-9]/.test("abc5"));   // true
console.log(/[0-9]/.test("abcde"));  // false

// Letters AND digits combined
console.log(/[a-zA-Z0-9]/.test("a1")); // true

Practical ranges:

javascript
// Extract only letters from a messy string
const messy = "H3ll0 W0rld!";
console.log(messy.match(/[a-zA-Z]/g)); // ["H", "l", "l", "W", "r", "l", "d"]

// Remove non-digit characters from phone number
const phone = "(080) 123-4567";
console.log(phone.replace(/[^0-9]/g, "")); // "08012345678"

▶ Expected Output:

code
["H", "l", "l", "W", "r", "l", "d"]
08012345678

Combining Ranges and Characters

You can mix ranges and individual characters inside one class:

javascript
// Alphanumeric + underscore + hyphen
const slug = /^[a-zA-Z0-9_-]+$/;

console.log(slug.test("my-blog-post"));    // true
console.log(slug.test("my blog post"));    // false (space not allowed)
console.log(slug.test("my_blog_2024"));    // true
console.log(slug.test("my@blog!post"));    // false (@ and ! not allowed)

💡 TIP: The hyphen - is special inside [...]. To include a literal hyphen, place it at the start or end of the class, or escape it with \-:

javascript
/[a-z-]/   // letters a-z OR a literal hyphen (hyphen at end = literal)
/[-a-z]/   // same — hyphen at start = literal
/[a\-z]/   // escaped — also literal hyphen

Common Character Class Patterns

javascript
// Hex digit (0-9 and a-f and A-F)
const hex = /[0-9a-fA-F]/;
console.log(hex.test("F")); // true
console.log(hex.test("G")); // false

// Filename character (no slashes, colons)
const filename = /^[^/\\:*?"<>|]+$/;
console.log(filename.test("report_2024.pdf")); // true
console.log(filename.test("file/path"));        // false

// Valid username: letters, digits, underscore, 3-20 chars
const username = /^[a-zA-Z0-9_]{3,20}$/;
console.log(username.test("alice_99"));   // true
console.log(username.test("a"));          // false (too short)
console.log(username.test("alice-99"));   // false (hyphen not allowed)


✏️ Your Task
Practise what you just learned about 4. Topic 3 · Character Classes & Ranges. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 5 of 12
5. Topic 4 · Meta Characters

Phase 1 · Conceptual Understanding

Meta characters are special characters in regex that have a meaning beyond their literal value. They are the core building blocks of every pattern.


Shorthand Character Classes

These are the most frequently used meta characters · each represents a common character class:

\d · Any Digit [0-9]

javascript
console.log(/\d/.test("abc5def")); // true  — contains a digit
console.log(/\d/.test("abcdef"));  // false — no digits

// Find all digits in a string
console.log("a1b22c333".match(/\d+/g)); // ["1", "22", "333"]

\D · Any NON-Digit [^0-9]

javascript
console.log("a1b22c333".match(/\D+/g)); // ["a", "b", "c"]

\w · Word Character [a-zA-Z0-9_]

Matches letters, digits, and underscore · the characters valid in most identifiers.

javascript
console.log(/\w/.test("hello")); // true
console.log(/\w/.test("!@#$")); // false — no word characters

// Extract words
console.log("Hello, World! 2024".match(/\w+/g)); // ["Hello", "World", "2024"]

\W · Any NON-Word Character [^a-zA-Z0-9_]

javascript
// Find all punctuation/spaces
console.log("Hello, World!".match(/\W+/g)); // [",", " ", "!"]

\s · Any Whitespace [ \t\n\r\f\v]

Matches space, tab, newline, carriage return, form feed, vertical tab.

javascript
console.log(/\s/.test("hello world")); // true  — has a space
console.log(/\s/.test("helloworld")); // false — no whitespace

// Normalise multiple spaces to one
console.log("too   many    spaces".replace(/\s+/g, " ")); // "too many spaces"

\S · Any NON-Whitespace

javascript
// Extract non-space "tokens"
console.log("  hello   world  ".match(/\S+/g)); // ["hello", "world"]

The Dot . · Any Character (Except Newline)

. matches any single character except a newline \n (unless s flag is used).

javascript
console.log(/./.test("a"));   // true
console.log(/./.test("5"));   // true
console.log(/./.test("!"));   // true
console.log(/./.test("\n"));  // false — newline excluded
console.log(/./s.test("\n")); // true  — s flag includes newline

// Pattern: exactly 3 characters starting with 'c'
console.log(/^c..$/. test("cat")); // true
console.log(/^c..$/. test("cart")); // false (4 chars)

🐛 COMMON MISTAKE: Using . when you mean "any character" often creates overly broad patterns that match too much. Prefer [^\n] or specific character classes when you know what you expect. Also, to match a literal dot (e.g., in example.com), you must escape it: \.

javascript
// ❌ Wrong — dot matches ANY character including commas, slashes, etc.
/www.example.com/  // also matches "wwwXexampleYcom"!

// ✅ Correct — escape the dots to match literal periods
/www\.example\.com/

Escape Character \

Backslash \ turns a special character into a literal one.

javascript
// Match a literal dot
console.log(/3\.14/.test("3.14")); // true
console.log(/3\.14/.test("3X14")); // false

// Match a literal dollar sign
console.log(/\$100/.test("$100")); // true
console.log(/\$100/.test("100"));  // false

// Match a literal backslash (needs \\)
console.log(/C:\\Users/.test("C:\\Users")); // true

Characters that MUST be escaped to match literally:

code
. * + ? ^ $ { } [ ] | ( ) \
javascript
// Match a literal question mark
console.log(/Are you sure\?/.test("Are you sure?")); // true

// Match a literal parenthesis
console.log(/\(555\)/.test("(555)")); // true

Alternation | · OR

The pipe | matches either the pattern on the left OR the pattern on the right. It is like an "or" for entire sub-expressions.

javascript
// Match "cat" OR "dog"
const petPattern = /cat|dog/;
console.log(petPattern.test("I have a cat")); // true
console.log(petPattern.test("I have a dog")); // true
console.log(petPattern.test("I have a bird")); // false

// Find all occurrences
const text = "cat and dog and cat";
console.log(text.match(/cat|dog/g)); // ["cat", "dog", "cat"]

💡 TIP: Alternation has the lowest precedence in regex. Use groups (...) to limit its scope:

javascript
/gray|grey/    // "gray" OR "grey" ← correct
/gr(a|e)y/     // "gr" then ("a" OR "e") then "y" ← also correct, different structure
/I love cat|dog/ // "I love cat" OR "dog" ← probably not what you wanted!
/I love (cat|dog)/ // "I love cat" OR "I love dog" ← correct

Meta Characters Reference Table

Meta CharMeaningExampleMatches
.Any char (not newline)a.b"aXb", "a1b"
\dDigit [0-9]\d\d"42", "09"
\DNon-digit\D+"abc", "!!"
\wWord char [a-zA-Z0-9_]\w+"hello_2"
\WNon-word char\W+"!@#", ", "
\sWhitespace\s+" ", "\t\n"
\SNon-whitespace\S+"hello", "42"
\bWord boundary\bcat\b"cat" not "cats"
\BNon-word boundary\Bcat\B"tomcat"
\nNewline literal\nnewline char
\tTab literal\ttab char
\rCarriage return\rCR char
\0Null character\0null
\uXXXXUnicode code point\u0041"A"
\xHHHex character\x41"A"
\\Literal backslashC:\\"C:\"
`\`Literal pipe`a\b`"ab"


✏️ Your Task
Practise what you just learned about 5. Topic 4 · Meta Characters. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 6 of 12
6. Topic 5 · Assertions (Anchors & Lookarounds)

Phase 1 · Conceptual Understanding

Assertions are zero-width matches · they describe a position in the string rather than consuming characters. They are like "conditions" that must be true at a point in the string for the overall match to succeed.


^ · Start of String (or Line with m)

The pattern must match at the very beginning of the string.

javascript
// String must START with "Hello"
console.log(/^Hello/.test("Hello World")); // true
console.log(/^Hello/.test("Say Hello"));   // false — "Hello" not at start

// With m flag — start of each LINE
const text = "apple\nbanana\napricot";
console.log(text.match(/^a\w+/gm)); // ["apple", "apricot"]

$ · End of String (or Line with m)

The pattern must match at the very end of the string.

javascript
// String must END with ".com"
console.log(/\.com$/.test("example.com"));       // true
console.log(/\.com$/.test("example.com/path")); // false

// Validate format: digits only
console.log(/^\d+$/.test("12345")); // true  — all digits
console.log(/^\d+$/.test("123a5")); // false — 'a' breaks the pattern

💡 TIP: ^pattern$ (anchoring both ends) is the cornerstone of validation patterns · it ensures the entire string matches, with nothing extra before or after.


\b · Word Boundary

Matches a position between a word character (\w) and a non-word character (\W) · the invisible boundary at the edge of a word.

javascript
// Match the word "cat" but not "catch", "cats", "tomcat"
const pattern = /\bcat\b/;

console.log(pattern.test("my cat sat"));  // true  — "cat" as a whole word
console.log(pattern.test("my cats"));     // false — "cat" followed by "s"
console.log(pattern.test("tomcat"));      // false — "cat" preceded by "tom"
console.log(pattern.test("the cat."));    // true  — dot is non-word boundary

🏢 REAL WORLD: \b is essential for whole-word search · find the word "apple" but not "pineapple" or "apples". Every modern text editor's "Find Whole Word Only" feature uses \b internally.

Extract whole words only:

javascript
const text = "category: cats and catfish and a cat";
console.log(text.match(/\bcat\b/g)); // ["cat"] — only the standalone word

\B · Non-Word Boundary

Matches a position that is NOT a word boundary · inside a word.

javascript
// Match "cat" only when it's INSIDE a larger word
const pattern = /\Bcat\B/;

console.log(pattern.test("tomcats"));  // true  — "cat" inside "tomcats"
console.log(pattern.test("my cat"));   // false — "cat" is a whole word here

Lookahead (?=...) · "Followed By"

A positive lookahead asserts that what follows the current position matches the given pattern · without consuming those characters.

javascript
// Match "100" only if followed by " dollars"
const pattern = /\d+(?= dollars)/;

console.log(pattern.exec("I have 100 dollars")); // ["100"]
console.log(pattern.exec("I have 100 euros"));   // null — not followed by " dollars"

// The match is just "100" — " dollars" is NOT included in the match
const result = /\d+(?= dollars)/.exec("I have 100 dollars");
console.log(result[0]); // "100"   ← only the digits, not " dollars"

▶ Expected Output:

code
["100"]
null
100

💡 TIP: Lookahead is "peek ahead" · it checks what comes next but doesn't eat it. Think of it as "match X only when it is followed by Y, but don't include Y in the match."


Negative Lookahead (?!...) · "NOT Followed By"

Asserts that what follows does not match the pattern.

javascript
// Match "100" only if NOT followed by " euros"
const pattern = /\d+(?! euros)/g;

const text = "100 dollars and 200 euros and 50 pounds";
console.log(text.match(pattern)); // ["100", "50"] — "200" is followed by " euros"

▶ Expected Output: ["100", "50"]


Lookbehind (?<=...) · "Preceded By"

A positive lookbehind asserts that what precedes the current position matches the pattern · without consuming those characters.

javascript
// Match digits only if preceded by "$"
const pattern = /(?<=\$)\d+/g;

const text = "Price: $100 and €200 and $350";
console.log(text.match(pattern)); // ["100", "350"] — only dollar amounts

▶ Expected Output: ["100", "350"]


Negative Lookbehind (?<!...) · "NOT Preceded By"

Asserts that what precedes does not match.

javascript
// Match digits NOT preceded by "$"
const pattern = /(?<!\$)\d+/g;

const text = "I paid $100 and owed 200 more";
// Careful — this matches parts of numbers. Often combine with \b
const text2 = "I paid $100 and owed 200 more";
console.log(text2.match(/(?<!\$)\b\d+\b/g)); // ["200"]

Lookaround Summary

AssertionSyntaxMeaning
Positive lookaheadX(?=Y)Match X followed by Y
Negative lookaheadX(?!Y)Match X NOT followed by Y
Positive lookbehind(?<=Y)XMatch X preceded by Y
Negative lookbehind(?<!Y)XMatch X NOT preceded by Y
Start of string^Match at string start
End of string$Match at string end
Word boundary\bBetween word and non-word char
Non-word boundary\BNot between word and non-word char


✏️ Your Task
Practise what you just learned about 6. Topic 5 · Assertions (Anchors & Lookarounds). Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 7 of 12
7. Topic 6 · Quantifiers

Phase 1 · Conceptual Understanding

Quantifiers specify how many times the preceding element must occur. Without quantifiers, every character or group matches exactly once.


* · Zero or More

Matches zero or more repetitions of the preceding element. Even zero · the element can be completely absent.

javascript
// "ab*c" — 'a', then zero or more 'b', then 'c'
console.log(/ab*c/.test("ac"));     // true  — zero b's
console.log(/ab*c/.test("abc"));    // true  — one b
console.log(/ab*c/.test("abbbbc")); // true  — four b's
console.log(/ab*c/.test("aXc"));    // false — X is not b

⚠️ WATCH OUT: can match nothing. /a/ matches any string · even an empty one · because "zero or more a's" allows empty. This can make patterns match unexpected things.


+ · One or More

Matches one or more repetitions. The element must appear at least once.

javascript
// "ab+c" — 'a', then ONE or more 'b', then 'c'
console.log(/ab+c/.test("ac"));      // false — no b's (at least 1 required)
console.log(/ab+c/.test("abc"));     // true  — one b
console.log(/ab+c/.test("abbbbc")); // true  — four b's

\d+ · One or more digits · the most common use:

javascript
// Find all numbers in text
const text = "Order 1 has 250 items and order 12 has 3 items";
console.log(text.match(/\d+/g)); // ["1", "250", "12", "3"]

? · Zero or One (Optional)

Makes the preceding element optional · it can appear once or not at all.

javascript
// "colou?r" — optional 'u' (matches British and American English)
console.log(/colou?r/.test("color"));   // true — no u
console.log(/colou?r/.test("colour"));  // true — with u
console.log(/colou?r/.test("colouur")); // false — two u's

// Optional extension
console.log(/https?:\/\//.test("http://"));  // true
console.log(/https?:\/\//.test("https://")); // true

{n} · Exactly n Times

javascript
// Exactly 4 digits
console.log(/^\d{4}$/.test("2024")); // true
console.log(/^\d{4}$/.test("202"));  // false (3 digits)
console.log(/^\d{4}$/.test("20245")); // false (5 digits)

// Exactly 3 letters
console.log(/^[A-Z]{3}$/.test("ABC")); // true
console.log(/^[A-Z]{3}$/.test("AB"));  // false

{n,} · n or More Times

javascript
// 3 or more digits
console.log(/\d{3,}/.test("12"));    // false
console.log(/\d{3,}/.test("123"));   // true
console.log(/\d{3,}/.test("12345")); // true

{n,m} · Between n and m Times (Inclusive)

javascript
// Between 2 and 5 digits
console.log(/^\d{2,5}$/.test("1"));      // false (too few)
console.log(/^\d{2,5}$/.test("12"));     // true
console.log(/^\d{2,5}$/.test("12345")); // true
console.log(/^\d{2,5}$/.test("123456")); // false (too many)

🏢 REAL WORLD: {n,m} is the backbone of all validation patterns · password length, username length, phone number formats, postal codes, credit card numbers.


Greedy vs Lazy Quantifiers

By default, quantifiers are greedy · they match as much as possible while still allowing the overall pattern to succeed.

Adding ? after a quantifier makes it lazy · it matches as little as possible.

javascript
const html = "<div>Hello</div><div>World</div>";

// Greedy — matches from first < to LAST >
console.log(html.match(/<.+>/));
// ["<div>Hello</div><div>World</div>"] ← too much!

// Lazy — matches from first < to FIRST >
console.log(html.match(/<.+?>/));
// ["<div>"] ← just the first tag

▶ Expected Output:

code
["<div>Hello</div><div>World</div>"]
["<div>"]

Full lazy quantifier table:

GreedyLazyDifference
**?Zero or more · as few as possible
++?One or more · as few as possible
???Zero or one · prefers zero
{n,m}{n,m}?n to m · as few as possible
javascript
// Extract content between tags lazily
const text = "<b>Bold</b> and <i>italic</i>";
console.log(text.match(/<[^>]+>[^<]*<\/[^>]+>/g));
// ["<b>Bold</b>", "<i>italic</i>"]

// Or with lazy quantifier
console.log(text.match(/<.+?>.+?<\/.+?>/g));
// ["<b>Bold</b>", "<i>italic</i>"]

Possessive Quantifiers (Atomic Matching)

Adding + after a quantifier (*+, ++, ?+, {n}+) prevents backtracking · once consumed, the engine won't give characters back. Used for performance and avoiding catastrophic backtracking.

⚠️ NOTE: Possessive quantifiers require JavaScript ES2018+ and are not universally supported in all environments. Check compatibility before use.


Quantifiers Summary

QuantifierMeaning
*0 or more (greedy)
+1 or more (greedy)
?0 or 1 (optional)
{n}Exactly n
{n,}n or more
{n,m}Between n and m
*?0 or more (lazy)
+?1 or more (lazy)
??0 or 1 (lazy, prefers 0)
{n,m}?n to m (lazy)


✏️ Your Task
Practise what you just learned about 7. Topic 6 · Quantifiers. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 8 of 12
8. Topic 7 · Groups & Patterns

Phase 1 · Conceptual Understanding

Groups let you treat multiple characters as a single unit · applying quantifiers to them, capturing their matched text, or creating non-capturing groupings for alternation.


(...) · Capturing Group

Wraps part of a pattern to:

  1. Treat multiple characters as one unit (for quantifiers, alternation)
  2. Capture the matched text so you can extract it
javascript
// Without group — + applies only to 'b'
console.log(/ab+/.test("abb")); // true (a then 2 b's)

// With group — + applies to the whole "(ab)"
console.log(/(ab)+/.test("ababab")); // true (ab repeated 3 times)
console.log(/(ab)+/.test("abb"));    // false — "abb" doesn't have repeating "ab"

Capturing · extracting matched substrings:

javascript
// Capture year, month, day from a date string
const datePattern = /(\d{4})-(\d{2})-(\d{2})/;
const match = datePattern.exec("Today is 2024-03-15");

console.log(match[0]); // "2024-03-15" — full match
console.log(match[1]); // "2024"       — group 1 (year)
console.log(match[2]); // "03"         — group 2 (month)
console.log(match[3]); // "15"         — group 3 (day)

▶ Expected Output:

code
2024-03-15
2024
03
15

Named Capturing Groups (?<name>...)

Instead of accessing captures by index (match[1]), name them for clarity using (?<name>...).

javascript
const datePattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = datePattern.exec("Today is 2024-03-15");

console.log(match.groups.year);  // "2024"
console.log(match.groups.month); // "03"
console.log(match.groups.day);   // "15"

// Also usable in replace with $<name>
const reformatted = "2024-03-15".replace(
  /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
  "$<day>/$<month>/$<year>"
);
console.log(reformatted); // "15/03/2024"

▶ Expected Output:

code
2024
03
15
15/03/2024

🏢 REAL WORLD: Named groups make complex patterns self-documenting. A date regex with .groups.year is far clearer than one with [1] · especially when patterns have many groups.


(?:...) · Non-Capturing Group

Groups characters for quantifiers or alternation WITHOUT capturing the match. More efficient when you don't need the captured text.

javascript
// Capturing group — creates a capture (slower, uses memory)
const a = /(\d{3})-(\d{4})/.exec("555-1234");
console.log(a[1], a[2]); // "555" "1234"

// Non-capturing — groups for structure, no capture
const b = /(?:\d{3})-(?:\d{4})/.exec("555-1234");
console.log(b[1]); // undefined — no captures!

// Non-capturing for alternation
const color = /gr(?:a|e)y/; // matches "gray" or "grey"
console.log(color.test("gray")); // true
console.log(color.test("grey")); // true

💡 TIP: Use (?:...) whenever you need grouping but don't need to extract the captured text. It is faster and keeps your capture group numbering clean.


Backreferences \1, \2 · Refer Back to a Captured Group

A backreference matches the same text that was captured by an earlier group. Use \1 for group 1, \2 for group 2, and so on.

javascript
// \1 must match the same word as group 1 captured
const doubled = /(\b\w+\b) \1/;

console.log(doubled.test("the the"));    // true  — "the" repeated
console.log(doubled.test("cat cat"));    // true  — "cat" repeated
console.log(doubled.test("the cat"));    // false — different words

// Find repeated words in text
const text = "I think that that is a problem problem.";
const matches = text.match(/\b(\w+)\b \1\b/g);
console.log(matches); // ["that that", "problem problem"]

▶ Expected Output:

code
true
true
false
["that that", "problem problem"]

Named backreference \k<name>:

javascript
// Match opening and closing HTML tag pairs
const tagPattern = /<(?<tag>\w+)>[^<]*<\/\k<tag>>/;

console.log(tagPattern.test("<div>Hello</div>"));    // true
console.log(tagPattern.test("<b>Bold</b>"));         // true
console.log(tagPattern.test("<div>Hello</span>"));   // false — mismatched tags

Common Validation Patterns

These patterns combine everything covered so far:

javascript
// Email (simplified — RFC 5322 is much more complex)
const email = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
console.log(email.test("alice@example.com"));  // true
console.log(email.test("alice@.com"));          // false
console.log(email.test("aliceexample.com"));   // false

// International phone (E.164 format)
const phone = /^\+?[1-9]\d{7,14}$/;
console.log(phone.test("+2348012345678")); // true
console.log(phone.test("08012345678"));    // true (no + ok)
console.log(phone.test("123"));            // false (too short)

// URL (simplified)
const url = /^https?:\/\/[a-zA-Z0-9.-]+(?:\.[a-zA-Z]{2,})(?:\/[^\s]*)?$/;
console.log(url.test("https://example.com"));          // true
console.log(url.test("http://example.com/path?q=1")); // true
console.log(url.test("ftp://example.com"));            // false

// Strong password — 8+ chars, uppercase, lowercase, digit, special
const password = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/;
console.log(password.test("Hello123!"));   // true
console.log(password.test("hello123!"));   // false (no uppercase)
console.log(password.test("Hello!"));      // false (no digit, too short)

// Nigerian BVN (Bank Verification Number) — 11 digits
const bvn = /^\d{11}$/;
console.log(bvn.test("12345678901")); // true
console.log(bvn.test("1234567890"));  // false (10 digits)

// Postal code — US ZIP (5 digits or ZIP+4)
const zip = /^\d{5}(?:-\d{4})?$/;
console.log(zip.test("90210"));       // true
console.log(zip.test("90210-1234"));  // true
console.log(zip.test("9021"));        // false

// IPv4 address
const ipv4 = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
console.log(ipv4.test("192.168.1.1")); // true
console.log(ipv4.test("256.0.0.1"));   // false
console.log(ipv4.test("10.0.0.1"));    // true


✏️ Your Task
Practise what you just learned about 8. Topic 7 · Groups & Patterns. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 9 of 12
9. Topic 8 · The RegExp Object

Phase 1 · Conceptual Understanding

When you create a regex (either with literal syntax or new RegExp()), you get a RegExp object with properties and methods you can use to inspect and drive the regex engine directly.


RegExp Properties

javascript
const re = /hello\d+/gim;

console.log(re.source);    // "hello\d+" — the pattern text (no slashes)
console.log(re.flags);     // "gim"      — all flags as a string (sorted)
console.log(re.global);    // true       — g flag set?
console.log(re.ignoreCase);// true       — i flag set?
console.log(re.multiline); // true       — m flag set?
console.log(re.sticky);    // false      — y flag set?
console.log(re.unicode);   // false      — u flag set?
console.log(re.dotAll);    // false      — s flag set?
console.log(re.hasIndices);// false      — d flag set?
console.log(re.lastIndex); // 0          — position for next search (g/y only)

▶ Expected Output:

code
hello\d+
gim
true
true
true
false
false
false
false
0

re.lastIndex · Position Tracking

When using the g or y flag, lastIndex tracks where the next search will start. It updates automatically after each match.

javascript
const re   = /\d+/g;
const text = "12 and 34 and 56";

let match;
while ((match = re.exec(text)) !== null) {
  console.log(`Found "${match[0]}" at index ${match.index}, lastIndex now ${re.lastIndex}`);
}

▶ Expected Output:

code
Found "12" at index 0, lastIndex now 2
Found "34" at index 7, lastIndex now 9
Found "56" at index 14, lastIndex now 16

Inspecting a RegExp Object

javascript
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/i;

console.log("Source:", emailRe.source);
console.log("Flags:", emailRe.flags);
console.log("Is global:", emailRe.global);
console.log("Is case-insensitive:", emailRe.ignoreCase);

// Build a new regex from an existing one, adding flags
const withG = new RegExp(emailRe.source, emailRe.flags + "g");
console.log("New flags:", withG.flags);

Modifying lastIndex Manually

javascript
const re = /\d+/g;
const text = "100 200 300 400";

re.lastIndex = 8; // skip past the first two numbers
const match = re.exec(text);
console.log(match[0]); // "300" — started searching from index 8

// Always reset before reusing a global regex
re.lastIndex = 0;
console.log(re.exec(text)[0]); // "100" — starts fresh

RegExp.prototype[Symbol.match] · Used Internally by String.match()

Regex objects implement [Symbol.match], [Symbol.replace], [Symbol.search], and [Symbol.split]. This is how String.prototype.match(re) actually calls the regex's own method.

javascript
// These two are identical:
"hello world".match(/\w+/g);
/\w+/g[Symbol.match]("hello world");
// Both: ["hello", "world"]


✏️ Your Task
Practise what you just learned about 9. Topic 8 · The RegExp Object. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 10 of 12
10. Topic 9 · RegExp & String Methods

Phase 1 · Conceptual Understanding

There are six primary methods for working with regular expressions in JavaScript · four on strings, two on the RegExp object itself.


RegExp.prototype.test(string) · Quick True/False Check

The simplest regex method. Returns true if the pattern matches anywhere in the string, false otherwise. Use it when you just need to know if something matches.

javascript
const hasDigit  = /\d/;
const isEmail   = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

console.log(hasDigit.test("hello5world")); // true
console.log(hasDigit.test("helloworld"));  // false

console.log(isEmail.test("alice@example.com")); // true
console.log(isEmail.test("not-an-email"));       // false

💡 TIP: .test() is the fastest method for pure yes/no checks · use it for form validation, filtering, and conditional logic.


RegExp.prototype.exec(string) · Detailed Match Info

Returns a match array with detailed information, or null if no match. More powerful than test() · gives you the matched text, index, and capture groups.

javascript
const pattern = /(\d{4})-(\d{2})-(\d{2})/;
const result  = pattern.exec("Event date: 2024-03-15");

if (result) {
  console.log(result[0]);     // "2024-03-15" — full match
  console.log(result[1]);     // "2024"        — group 1
  console.log(result[2]);     // "03"           — group 2
  console.log(result[3]);     // "15"           — group 3
  console.log(result.index);  // 12             — position in string
  console.log(result.input);  // "Event date: 2024-03-15"
}

▶ Expected Output:

code
2024-03-15
2024
03
15
12
Event date: 2024-03-15

Iterate ALL matches with exec() in a loop:

javascript
const re   = /\b\w{5}\b/g; // all 5-letter words
const text = "hello world today is a great Monday";
let match;

while ((match = re.exec(text)) !== null) {
  console.log(`"${match[0]}" at index ${match.index}`);
}

▶ Expected Output:

code
"hello" at index 0
"world" at index 6
"today" at index 12
"great" at index 22

String.prototype.match(regexp) · Get All Matches

Without g flag: behaves like exec() · returns first match with groups. With g flag: returns an array of all matched strings (no group info).

javascript
const text = "Call 080-1234-5678 or 081-9876-5432 for details";

// Without g — first match + groups
const first = text.match(/(\d{3})-(\d{4})-(\d{4})/);
console.log(first[0]); // "080-1234-5678"
console.log(first[1]); // "080"
console.log(first[2]); // "1234"

// With g — ALL matches (no group info)
const all = text.match(/\d{3}-\d{4}-\d{4}/g);
console.log(all); // ["080-1234-5678", "081-9876-5432"]

▶ Expected Output:

code
080-1234-5678
080
1234
["080-1234-5678", "081-9876-5432"]

⚠️ WATCH OUT: With the g flag, match() returns an array of strings only · capture groups are ignored. Use matchAll() if you need groups from all matches.


String.prototype.matchAll(regexp) · All Matches WITH Groups

Returns an iterator of all match results · each with full group information. The regex MUST have the g flag.

javascript
const text = "2024-01-15 and 2024-03-22 and 2023-12-01";
const re   = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;

for (const match of text.matchAll(re)) {
  const { year, month, day } = match.groups;
  console.log(`Year: ${year}, Month: ${month}, Day: ${day}`);
}

▶ Expected Output:

code
Year: 2024, Month: 01, Day: 15
Year: 2024, Month: 03, Day: 22
Year: 2023, Month: 12, Day: 01

💡 TIP: matchAll() is the modern replacement for exec() loops. It is cleaner, works with named groups, and returns an iterator you can spread into an array:

javascript
const matches = [...text.matchAll(re)];
console.log(matches.length); // 3
console.log(matches[0].groups.year); // "2024"

String.prototype.search(regexp) · Find Position of First Match

Returns the index of the first match, or -1 if not found. Like indexOf but for regex patterns.

javascript
const text = "Hello World 2024";

console.log(text.search(/\d+/));    // 12 — position of "2024"
console.log(text.search(/xyz/));    // -1  — not found
console.log(text.search(/world/i)); // 6   — case-insensitive

⚠️ WATCH OUT: search() always searches from the beginning · it ignores the g flag and lastIndex. For finding a position with a literal string, indexOf is faster. search() shines when you need a pattern-based position.


String.prototype.replace(regexp, replacement) · Find and Replace

The most powerful string method. Replaces match(es) with a replacement string or the result of a function.

Simple replacement:

javascript
// Replace first match only (no g flag)
console.log("hello hello".replace(/hello/, "goodbye")); // "goodbye hello"

// Replace ALL matches (g flag)
console.log("hello hello".replace(/hello/g, "goodbye")); // "goodbye goodbye"

// Case-insensitive + global
console.log("Hello HELLO hello".replace(/hello/gi, "hi")); // "hi hi hi"

Replacement with capture group references $1, $2, $<name>:

javascript
// Reformat date: YYYY-MM-DD → DD/MM/YYYY
const date = "2024-03-15";
const reformatted = date.replace(/(\d{4})-(\d{2})-(\d{2})/, "$3/$2/$1");
console.log(reformatted); // "15/03/2024"

// Using named groups
const reformatted2 = date.replace(
  /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
  "$<d>/$<m>/$<y>"
);
console.log(reformatted2); // "15/03/2024"

▶ Expected Output:

code
15/03/2024
15/03/2024

Special replacement tokens:

TokenMeaning
$$Literal $
$&Entire matched string
` $ ``Text before match
$'Text after match
$nn-th capture group
$<name>Named capture group
javascript
// Wrap every number in brackets using $&
console.log("I have 5 cats and 3 dogs".replace(/\d+/g, "[$&]"));
// "I have [5] cats and [3] dogs"

Replacement with a function (most powerful):

javascript
// Replace numbers with their doubled values
const result = "Price: 50 and discount: 10".replace(/\d+/g, n => n * 2);
console.log(result); // "Price: 100 and discount: 20"

// Capitalise first letter of each word
const title = "the quick brown fox".replace(/\b\w/g, c => c.toUpperCase());
console.log(title); // "The Quick Brown Fox"

// Censor profanity (replace with asterisks)
const censor = (text, words) => {
  const re = new RegExp(`\\b(${words.join("|")})\\b`, "gi");
  return text.replace(re, w => "*".repeat(w.length));
};
console.log(censor("What the heck is going on", ["heck"]));
// "What the **** is going on"

▶ Expected Output:

code
Price: 100 and discount: 20
The Quick Brown Fox
What the **** is going on

String.prototype.replaceAll(string|regexp, replacement) · Replace All

Like replace() with g, but can also accept a plain string as the first argument (not just regex). When used with a regex, the g flag is required.

javascript
// With plain string
console.log("a.b.c.d".replaceAll(".", "/")); // "a/b/c/d"

// With regex (g flag required)
console.log("hello world".replaceAll(/\w+/g, w => w.toUpperCase()));
// "HELLO WORLD"

String.prototype.split(regexp) · Split on a Pattern

Splits a string into an array, using the regex as the delimiter. More flexible than splitting on a fixed string.

javascript
// Split on any whitespace (including multiple spaces/tabs)
console.log("  hello   world  ".split(/\s+/).filter(Boolean));
// ["hello", "world"]

// Split on comma or semicolon or pipe
console.log("a,b;c|d".split(/[,;|]/));
// ["a", "b", "c", "d"]

// Split on digit runs
console.log("abc123def456ghi".split(/\d+/));
// ["abc", "def", "ghi"]

// Split but KEEP the delimiters (using a capturing group)
console.log("one, two; three".split(/(,|;)\s*/));
// ["one", ",", "two", ";", "three"]

▶ Expected Output:

code
["hello", "world"]
["a", "b", "c", "d"]
["abc", "def", "ghi"]
["one", ",", "two", ";", "three"]

Methods Summary Table

MethodReturnsUse When
re.test(str)BooleanQuick check: does it match?
re.exec(str)Array or nullNeed match position + groups, one at a time
str.match(re)Array or nullGet all matched strings (with g)
str.matchAll(re)IteratorAll matches WITH group info (needs g)
str.search(re)Number (-1 = no match)Find position of first match
str.replace(re, rep)StringSubstitute match(es) with string/function
str.replaceAll(re, rep)StringReplace all (regex needs g)
str.split(re)ArraySplit string on pattern delimiter


✏️ Your Task
Practise what you just learned about 10. Topic 9 · RegExp & String Methods. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 11 of 12
11. Applied Exercises

Phase 2 · Applied Exercises


Exercise 1 · Pattern Detective 🔍

Objective: Read patterns and predict what they match before running them. Build "regex reading" skills.

Scenario: You are reviewing a codebase's input validation regexes. For each, explain what it validates and give a passing and failing example.

Warm-up Micro-Demo:

javascript
const re = /^\d{3}-\d{2}-\d{4}$/;
// Read it: ^ start, 3 digits, literal -, 2 digits, literal -, 4 digits, $ end
// Matches: US Social Security Number format (123-45-6789)

console.log(re.test("123-45-6789")); // true
console.log(re.test("12-345-6789")); // false

Task A · Analyse These Patterns

javascript
// Pattern set to analyse:
const patterns = {
  p1: /^[a-zA-Z][a-zA-Z0-9_]{2,19}$/,
  p2: /^(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$]).{8,}$/,
  p3: /^\+?(\d{1,3})?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/,
  p4: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
  p5: /^([01]?\d|2[0-3]):[0-5]\d$/,
};

// Test and explain each pattern:

// p1 — Username rules
console.log("=== p1: Username ===");
console.log(patterns.p1.test("alice99"));      // true
console.log(patterns.p1.test("9alice"));       // false — must start with letter
console.log(patterns.p1.test("al"));           // false — too short (min 3 total)
console.log(patterns.p1.test("alice!"));       // false — ! not allowed

// p2 — Strong password
console.log("\n=== p2: Password ===");
console.log(patterns.p2.test("Secure1!pass")); // true
console.log(patterns.p2.test("secure1!pass")); // false — no uppercase
console.log(patterns.p2.test("Securepass!"));  // false — no digit
console.log(patterns.p2.test("Se1!"));         // false — too short

// p3 — Flexible phone number
console.log("\n=== p3: Phone ===");
console.log(patterns.p3.test("(080) 123-4567")); // true
console.log(patterns.p3.test("080.123.4567"));    // true
console.log(patterns.p3.test("+1 800 555 1234")); // true
console.log(patterns.p3.test("123"));              // false

// p4 — CSS hex color
console.log("\n=== p4: Hex Color ===");
console.log(patterns.p4.test("#FF5733"));  // true — 6-digit hex
console.log(patterns.p4.test("#F57"));     // true — 3-digit shorthand
console.log(patterns.p4.test("FF5733"));   // false — no #
console.log(patterns.p4.test("#GGGGGG"));  // false — G not hex

// p5 — 24-hour time
console.log("\n=== p5: 24-hour Time ===");
console.log(patterns.p5.test("00:00")); // true — midnight
console.log(patterns.p5.test("23:59")); // true — last minute
console.log(patterns.p5.test("24:00")); // false — 24 invalid
console.log(patterns.p5.test("9:30"));  // true — single digit hour ok

Self-check questions:

  • In p2, why do lookaheads (?=...) not conflict with each other even though they all start at the same position?
  • In p4, why does | inside the group give two alternatives of different lengths?
  • In p5, why does [01]?\d match 09 but 2[0-3] is needed for hours 20 · 23?

Exercise 2 · Text Extractor 📤

Objective: Use match(), matchAll(), and exec() to extract structured data from unstructured text.

Scenario: You receive a raw log file. Extract all relevant data fields using regex.

Warm-up Micro-Demo:

javascript
const log = "[2024-01-15 09:23:11] ERROR: Connection timeout";
const ts  = log.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
console.log("Timestamp:", ts[0]); // "2024-01-15 09:23:11"

Task A · Log Parser

javascript
const logData = `
[2024-03-15 08:00:01] INFO:  Server started on port 3000
[2024-03-15 08:01:22] INFO:  User alice@example.com logged in from 192.168.1.10
[2024-03-15 08:05:44] WARN:  High memory usage: 87%
[2024-03-15 08:10:03] ERROR: Database connection failed (attempt 1 of 3)
[2024-03-15 08:10:15] ERROR: Database connection failed (attempt 2 of 3)
[2024-03-15 08:12:00] INFO:  User bob@test.org logged in from 10.0.0.5
[2024-03-15 08:15:33] ERROR: Disk space critical: 95% used
[2024-03-15 08:20:11] INFO:  Cache cleared — 1250 items removed
`.trim();

// 1. Extract all timestamps
const timestamps = logData.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/g);
console.log("Timestamps:", timestamps.length, "entries");

// 2. Count log levels
const levels = { INFO: 0, WARN: 0, ERROR: 0 };
for (const [, level] of logData.matchAll(/\] (INFO|WARN|ERROR):/g)) {
  levels[level]++;
}
console.log("\nLog level counts:", levels);

// 3. Extract all email addresses
const emails = logData.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g);
console.log("\nEmails found:", emails);

// 4. Extract all IP addresses
const ips = logData.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g);
console.log("IPs found:", ips);

// 5. Extract ERROR entries with full details using named groups
const errorRe = /\[(?<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] ERROR: (?<msg>.+)/g;
console.log("\nERROR entries:");
for (const match of logData.matchAll(errorRe)) {
  console.log(`  [${match.groups.ts}] ${match.groups.msg}`);
}

// 6. Extract percentage values
const percentages = logData.match(/\d+(?=%)/g);
console.log("\nPercentages mentioned:", percentages);

Expected Output:

code
Timestamps: 8 entries

Log level counts: { INFO: 4, WARN: 1, ERROR: 3 }

Emails found: ["alice@example.com", "bob@test.org"]
IPs found: ["192.168.1.10", "10.0.0.5"]

ERROR entries:
  [2024-03-15 08:10:03] Database connection failed (attempt 1 of 3)
  [2024-03-15 08:10:15] Database connection failed (attempt 2 of 3)
  [2024-03-15 08:15:33] Disk space critical: 95% used

Percentages mentioned: ["87", "95"]

Self-check questions:

  • Why is matchAll() better than exec() in a loop for extracting multiple groups?
  • Why does /\d+(?=%)/g use a lookahead for % instead of /\d+%/g?
  • What would happen if you forgot the g flag when using matchAll()?

Exercise 3 · Text Transformer ✏️

Objective: Use replace() with functions to transform text programmatically.

Scenario: You're building a markdown-to-HTML converter for a blogging platform.

Warm-up Micro-Demo:

javascript
// Bold: **text** → <strong>text</strong>
const bold = "This is **important** text".replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
console.log(bold); // "This is <strong>important</strong> text"

Task A · Markdown Converter

javascript
function markdownToHtml(md) {
  return md
    // Headings: ### → h3, ## → h2, # → h1
    .replace(/^### (.+)$/gm, "<h3>$1</h3>")
    .replace(/^## (.+)$/gm,  "<h2>$1</h2>")
    .replace(/^# (.+)$/gm,   "<h1>$1</h1>")

    // Bold: **text** → <strong>text</strong>
    .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")

    // Italic: *text* → <em>text</em>
    .replace(/\*(.+?)\*/g, "<em>$1</em>")

    // Inline code: `code` → <code>code</code>
    .replace(/`([^`]+)`/g, "<code>$1</code>")

    // Links: [text](url) → <a href="url">text</a>
    .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')

    // Unordered list items: - item → <li>item</li>
    .replace(/^- (.+)$/gm, "<li>$1</li>")

    // Auto-link URLs not already in anchor tags
    .replace(/(?<!href=")https?:\/\/[\w.-]+(?:\/[\w./?=&%-]*)*/g, '<a href="$&">$&</a>')

    // Blank lines → paragraph breaks
    .replace(/\n{2,}/g, "\n\n<hr>\n\n");
}

const markdown = `
# My Blog Post

This is a **bold statement** and this is *italic*.
✏️ Your Task
· ### Exercise 1 · Pattern Detective 🔍 Objective: Read patterns and predict what they match before running them. Build "regex reading" skills. Scenario: You are reviewing a codebase's input validation regexes. For each, explain what it validates and give a passing and failing example.
Phase 12 of 12
Features
  • Fast and reliable
  • Supports \inline code\
  • Links like Google

Note

Visit https://example.com for more info. `.trim();

console.log(markdownToHtml(markdown));

code

**Expected Output (sample):**

<h1>My Blog Post</h1> ... <strong>bold statement</strong> ... <em>italic</em> ... <h2>Features</h2> <li>Fast and reliable</li> <li>Supports <code>inline code</code></li> <li>Links like <a href="https://google.com">Google</a></li> ... <a href="https://example.com">https://example.com</a>

code

**Self-check questions:**
- Why must `###` be replaced before `##` and `##` before `#`?
- Why does the link pattern use `[^\]]+` inside `[...]` instead of `.+`?
- Why does the URL auto-linker use a negative lookbehind `(?<!href=")`?

---
---
✏️ Your Task
Practise what you just learned about Features. Open your editor, type the examples above by hand, modify them, and observe what changes.
🏗️ Build It — Mini Project
🔵 Stage 1 · Validation Engine

Phase 3 · Project Simulation

Real-world scenario: You're building a registration form validator and data sanitiser for a multi-country web application. The system must:

  • Validate fields in real time (email, phone, password, username, URL)
  • Extract and structure data from a pasted contact list
  • Sanitise and redact sensitive information from text
  • Generate a validation report

🔵 Stage 1 · Validation Engine

Goal: Build a reusable validation system using regex patterns.

Simple stage preview:

starter.html
const isEmail = str => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
console.log(isEmail("a@b.com")); // true
console.log(isEmail("bad"));     // false

Lesson 19 complete! 🎉

You covered: