JavaScript Essentials

Regular Expressions in JavaScript

45 min Lesson 31 of 60

What Are Regular Expressions?

Regular expressions, often abbreviated as regex or RegExp, are powerful patterns used to match, search, and manipulate text. They provide a concise and flexible way to identify strings of text such as particular characters, words, or patterns of characters. In JavaScript, regular expressions are objects that describe a pattern of characters, and they are used with string methods and the RegExp object to perform pattern matching and text replacement operations. Whether you are validating user input, parsing log files, extracting data from strings, or performing complex find-and-replace operations, regular expressions are an indispensable tool in your JavaScript toolkit.

Regular expressions have been part of computing since the 1950s when mathematician Stephen Cole Kleene formalized them. Today, nearly every programming language supports regular expressions, and JavaScript provides first-class support through the built-in RegExp object and regex literal syntax. Understanding regular expressions will dramatically improve your ability to work with text data in any application.

Creating Regular Expressions: Literal vs Constructor

JavaScript provides two ways to create a regular expression. The first is the regex literal, which consists of a pattern enclosed between two forward slashes. The second is the RegExp constructor, which takes a string pattern as its first argument. Both approaches create a RegExp object, but they differ in when the expression is compiled and how special characters are handled.

Example: Creating Regular Expressions

// Method 1: Regex Literal (compiled at script load time)
const pattern1 = /hello/;
const pattern2 = /hello/gi;

// Method 2: RegExp Constructor (compiled at runtime)
const pattern3 = new RegExp('hello');
const pattern4 = new RegExp('hello', 'gi');

// The constructor is useful when the pattern is dynamic
const userInput = 'search term';
const dynamicPattern = new RegExp(userInput, 'i');

// Both create the same type of object
console.log(typeof pattern1); // "object"
console.log(pattern1 instanceof RegExp); // true
console.log(pattern3 instanceof RegExp); // true
Note: When using the RegExp constructor, you must double-escape backslashes because the string itself interprets one level of escaping. For example, to match a digit with \d, you write new RegExp('\\d') in the constructor, but simply /\d/ with the literal syntax.

The regex literal is preferred when you know the pattern at development time because it offers better readability and the engine compiles it when the script is loaded. The RegExp constructor is essential when the pattern must be built dynamically -- for instance, when you want to include a user-provided search term or a variable within the pattern.

Example: Dynamic Patterns with the Constructor

function highlightWord(text, word) {
    // Escape special regex characters in the user's word
    const escaped = word.replace(/[.*+?^${}()|[\]\]/g, '\\$&');
    const regex = new RegExp(`(${escaped})`, 'gi');
    return text.replace(regex, '<mark>$1</mark>');
}

console.log(highlightWord('Hello world! Hello again!', 'hello'));
// "<mark>Hello</mark> world! <mark>Hello</mark> again!"

Regex Flags

Flags modify how the regular expression engine processes the pattern. They are placed after the closing slash in a literal or passed as the second argument to the RegExp constructor. JavaScript supports several flags that control matching behavior.

Example: All JavaScript Regex Flags

// g - Global: find all matches, not just the first
const globalMatch = 'cat bat sat'.match(/[a-z]at/g);
console.log(globalMatch); // ["cat", "bat", "sat"]

// i - Case-Insensitive: ignore case when matching
const caseInsensitive = 'Hello HELLO hello'.match(/hello/gi);
console.log(caseInsensitive); // ["Hello", "HELLO", "hello"]

// m - Multiline: ^ and $ match line boundaries, not just string boundaries
const multiline = `Line 1
Line 2
Line 3`;
const lineStarts = multiline.match(/^Line/gm);
console.log(lineStarts); // ["Line", "Line", "Line"]

// s - DotAll: makes . match newline characters as well
const dotAll = 'Hello\nWorld'.match(/Hello.World/s);
console.log(dotAll[0]); // "Hello\nWorld"

// u - Unicode: enables full Unicode matching
const unicode = '\u{1F600}'.match(/./u);
console.log(unicode[0]); // "\u{1F600}" (emoji character)

// d - HasIndices: includes start and end indices for captured groups
const indices = 'hello world'.match(/(?<greeting>hello)/d);
console.log(indices.indices.groups.greeting); // [0, 5]

// Combining flags
const combined = /pattern/gims;
Pro Tip: The u flag is essential when working with text that may contain emoji or characters outside the Basic Multilingual Plane. Without it, JavaScript treats such characters as two separate code units, leading to unexpected matching results.

Core RegExp and String Methods

JavaScript provides several methods on both the RegExp object and String object that work with regular expressions. Understanding when to use each method is crucial for writing efficient and correct pattern-matching code.

Example: test() -- Check If a Pattern Matches

// test() returns true or false
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

console.log(emailPattern.test('user@example.com'));  // true
console.log(emailPattern.test('invalid-email'));      // false
console.log(emailPattern.test('user@.com'));           // false

// Useful in conditional statements
const input = 'user@example.com';
if (emailPattern.test(input)) {
    console.log('Valid email format');
} else {
    console.log('Invalid email format');
}

Example: match() -- Find Matches in a String

const text = 'The price is $45.99 and the tax is $3.50';

// Without g flag: returns first match with details
const firstMatch = text.match(/\$[\d.]+/);
console.log(firstMatch[0]);     // "$45.99"
console.log(firstMatch.index);  // 13
console.log(firstMatch.input);  // original string

// With g flag: returns array of all matches
const allMatches = text.match(/\$[\d.]+/g);
console.log(allMatches); // ["$45.99", "$3.50"]

// No match returns null
const noMatch = text.match(/\u20AC[\d.]+/g);
console.log(noMatch); // null (no euro amounts)

Example: matchAll() -- Iterate Over All Matches with Details

const text = 'Date: 2024-01-15, Updated: 2024-03-20';
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/g;

// matchAll() returns an iterator of match objects
const matches = [...text.matchAll(dateRegex)];

for (const match of matches) {
    console.log('Full match:', match[0]);
    console.log('Year:', match[1]);
    console.log('Month:', match[2]);
    console.log('Day:', match[3]);
    console.log('Index:', match.index);
    console.log('---');
}
// Full match: 2024-01-15, Year: 2024, Month: 01, Day: 15, Index: 6
// Full match: 2024-03-20, Year: 2024, Month: 03, Day: 20, Index: 32

Example: search() -- Find the Index of the First Match

const text = 'JavaScript is awesome!';

// search() returns the index of the first match, or -1
console.log(text.search(/awesome/));    // 14
console.log(text.search(/python/i));    // -1
console.log(text.search(/java/i));      // 0
console.log(text.search(/script/i));    // 4

Example: replace() and replaceAll() with Regex

const text = 'Hello World! Hello JavaScript!';

// replace() with regex -- replaces first match (or all with g flag)
console.log(text.replace(/Hello/, 'Hi'));
// "Hi World! Hello JavaScript!"

console.log(text.replace(/Hello/g, 'Hi'));
// "Hi World! Hi JavaScript!"

// Using capture groups in replacement
const date = '2024-01-15';
const formatted = date.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1');
console.log(formatted); // "01/15/2024"

// Using a function as the replacer
const prices = 'Items cost $10 and $25';
const doubled = prices.replace(/\$(\d+)/g, (match, amount) => {
    return '$' + (parseInt(amount) * 2);
});
console.log(doubled); // "Items cost $20 and $50"

Character Classes

Character classes allow you to match any one character from a specific set. JavaScript provides both shorthand character classes for common patterns and custom character classes using square brackets. These are the building blocks of nearly every regular expression pattern.

Example: Built-in and Custom Character Classes

// \d -- matches any digit (0-9)
console.log('abc123'.match(/\d+/g));  // ["123"]

// \D -- matches any non-digit
console.log('abc123'.match(/\D+/g));  // ["abc"]

// \w -- matches word characters (letters, digits, underscore)
console.log('hello_world 123!'.match(/\w+/g));  // ["hello_world", "123"]

// \W -- matches non-word characters
console.log('hello world!'.match(/\W+/g));  // [" ", "!"]

// \s -- matches whitespace (space, tab, newline, etc.)
console.log('hello\tworld\n!'.match(/\s/g));  // ["\t", "\n"]

// \S -- matches non-whitespace
console.log('hello world'.match(/\S+/g));  // ["hello", "world"]

// . -- matches any character except newline (unless s flag is used)
console.log('hat hot hit'.match(/h.t/g));  // ["hat", "hot", "hit"]

// Custom character classes with []
console.log('gray grey'.match(/gr[ae]y/g));  // ["gray", "grey"]

// Negated character class with [^]
console.log('abc123'.match(/[^a-z]+/g));  // ["123"]

// Range in character class
console.log('A1b2C3'.match(/[A-Za-z]/g)); // ["A", "b", "C"]

Quantifiers

Quantifiers specify how many times a character or group must appear for a match. JavaScript supports several quantifier types, from simple repetition to precise range controls. By default, quantifiers are greedy, meaning they match as much text as possible. You can make them lazy by adding a question mark after them, causing them to match as little text as possible.

Example: Quantifiers in Action

// + matches one or more
console.log('aabbb'.match(/b+/));  // ["bbb"]

// * matches zero or more
console.log('aac'.match(/ab*/));   // ["a"] (zero b's is fine)

// ? matches zero or one
console.log('color colour'.match(/colou?r/g));  // ["color", "colour"]

// {n} matches exactly n times
console.log('1234567'.match(/\d{3}/g));  // ["123", "456"]

// {n,} matches n or more times
console.log('aabbbcccc'.match(/c{2,}/g));  // ["cccc"]

// {n,m} matches between n and m times
console.log('aaabbbcccc'.match(/a{1,2}/g));  // ["aa", "a"]

// Greedy vs Lazy quantifiers
const html = '<div>Hello</div><div>World</div>';

// Greedy (default) -- matches as much as possible
console.log(html.match(/<div>.*<\/div>/)[0]);
// "<div>Hello</div><div>World</div>" (entire string)

// Lazy (with ?) -- matches as little as possible
console.log(html.match(/<div>.*?<\/div>/)[0]);
// "<div>Hello</div>" (first match only)
Common Mistake: Using greedy quantifiers with .* inside HTML-like patterns often matches far more text than intended. Always consider using lazy quantifiers .*? or more specific patterns like [^<]* to avoid matching across element boundaries.

Anchors and Boundaries

Anchors do not match characters themselves; instead, they match positions in the string. They are used to assert that a match occurs at a specific location, such as the beginning of the string, end of the string, or at a word boundary. Anchors are essential for ensuring your patterns match exactly where you expect them to.

Example: Anchors and Boundary Matchers

// ^ matches the start of the string (or line with m flag)
console.log(/^Hello/.test('Hello World'));  // true
console.log(/^Hello/.test('Say Hello'));    // false

// $ matches the end of the string (or line with m flag)
console.log(/World$/.test('Hello World'));  // true
console.log(/World$/.test('World Cup'));    // false

// \b matches a word boundary
console.log('cat concatenate'.match(/\bcat\b/g));  // ["cat"]
// Only matches "cat" as a whole word, not inside "concatenate"

// \B matches a non-word boundary
console.log('cat concatenate'.match(/\Bcat/g));  // ["cat"] (from concatenate)

// Combining anchors for full string matching
const isExactMatch = /^hello$/i;
console.log(isExactMatch.test('hello'));       // true
console.log(isExactMatch.test('hello world')); // false
console.log(isExactMatch.test('say hello'));    // false

// Multiline anchor behavior
const text = `first line
second line
third line`;
console.log(text.match(/^\w+ line$/gm));
// ["first line", "second line", "third line"]

Groups and Capturing

Grouping allows you to treat multiple characters as a single unit. Capturing groups store the matched text for later use, while non-capturing groups provide grouping without storing the result. Groups are essential for extracting specific parts of a match, applying quantifiers to sub-patterns, and creating alternation patterns.

Example: Capturing Groups and Alternation

// Basic capturing group
const dateMatch = '2024-01-15'.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(dateMatch[0]); // "2024-01-15" (full match)
console.log(dateMatch[1]); // "2024" (first group)
console.log(dateMatch[2]); // "01" (second group)
console.log(dateMatch[3]); // "15" (third group)

// Non-capturing group (?:...)
const nonCapturing = 'http://example.com'.match(/(?:http|https):\/\/(.+)/);
console.log(nonCapturing[1]); // "example.com" (only the domain is captured)

// Alternation with |
const pets = 'I have a cat and a dog';
console.log(pets.match(/cat|dog/g)); // ["cat", "dog"]

// Grouping with quantifiers
const repeated = 'abcabcabc'.match(/(abc){2,}/);
console.log(repeated[0]); // "abcabcabc"

Example: Named Groups

// Named capturing groups use (?<name>...)
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2024-01-15'.match(dateRegex);

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

// Named groups in replace()
const formatted = '2024-01-15'.replace(
    /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
    '$<day>/$<month>/$<year>'
);
console.log(formatted); // "15/01/2024"

// Named groups with matchAll()
const text = 'From 2024-01-15 to 2024-12-31';
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;

for (const m of text.matchAll(regex)) {
    console.log(`${m.groups.month}/${m.groups.day}/${m.groups.year}`);
}
// "01/15/2024"
// "12/31/2024"

Backreferences

Backreferences allow you to refer back to a previously captured group within the same regular expression. This is useful for matching repeated patterns, finding duplicated words, or ensuring that opening and closing delimiters match. Numbered backreferences use \1, \2, etc., while named backreferences use \k<name>.

Example: Backreferences

// Numbered backreference: \1 refers to the first captured group
const duplicateWords = /\b(\w+)\s+\1\b/gi;
const text = 'The the quick brown fox fox jumped';
console.log(text.match(duplicateWords));
// ["The the", "fox fox"]

// Named backreference: \k<name>
const matchQuoted = /(?<quote>['"]).*?\k<quote>/g;
const code = `She said "hello" and 'goodbye' today`;
console.log(code.match(matchQuoted));
// ["\"hello\"", "'goodbye'"]

// Backreference for matching HTML tags
const tagMatch = /<(\w+)>.*?<\/\1>/g;
const html = '<b>bold</b> and <i>italic</i>';
console.log(html.match(tagMatch));
// ["<b>bold</b>", "<i>italic</i>"]

Lookahead and Lookbehind Assertions

Lookahead and lookbehind assertions are zero-width assertions -- they check whether a pattern exists ahead of or behind the current position without including it in the match. These assertions are powerful for complex pattern matching where you need context around the match without consuming characters.

Example: Lookahead and Lookbehind

// Positive lookahead (?=...) -- matches if followed by pattern
const prices = '100 USD 200 EUR 300 USD';
const usdPrices = prices.match(/\d+(?= USD)/g);
console.log(usdPrices); // ["100", "300"]

// Negative lookahead (?!...) -- matches if NOT followed by pattern
const nonUsd = prices.match(/\d+(?! USD)(?= \w+)/g);
console.log(nonUsd); // ["200"]

// Positive lookbehind (?<=...) -- matches if preceded by pattern
const text = 'Price: $100, Cost: $200';
const amounts = text.match(/(?<=\$)\d+/g);
console.log(amounts); // ["100", "200"]

// Negative lookbehind (?<!...) -- matches if NOT preceded by pattern
const words = 'unhappy happy'.match(/(?<!un)happy/g);
console.log(words); // ["happy"]

// Practical: password strength check with multiple lookaheads
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%]).{8,}$/;
console.log(strongPassword.test('Passw0rd!'));  // true
console.log(strongPassword.test('password'));    // false
console.log(strongPassword.test('Pass1!'));      // false (too short)
Pro Tip: Lookahead and lookbehind assertions are perfect for password validation because you can check multiple conditions simultaneously. Each (?=.*condition) independently verifies one requirement, and they all must pass for the overall pattern to match.

Common Validation Patterns

Regular expressions are most commonly used for input validation. Here are well-tested patterns for validating common data formats. Remember that regex-based validation should complement server-side validation, never replace it.

Example: Email Validation

// Basic email validation pattern
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

console.log(emailRegex.test('user@example.com'));      // true
console.log(emailRegex.test('user.name@domain.co.uk')); // true
console.log(emailRegex.test('user@.com'));              // false
console.log(emailRegex.test('@example.com'));           // false
console.log(emailRegex.test('user@com'));               // false

// Breakdown:
// ^[a-zA-Z0-9._%+-]+   -- one or more valid local part characters
// @                      -- literal @ symbol
// [a-zA-Z0-9.-]+        -- one or more valid domain characters
// \.[a-zA-Z]{2,}$       -- dot followed by 2+ letter TLD

Example: Phone Number Validation

// US phone number: (123) 456-7890 or 123-456-7890 or 1234567890
const usPhoneRegex = /^(\+1\s?)?(\(\d{3}\)|\d{3})[\s.-]?\d{3}[\s.-]?\d{4}$/;

console.log(usPhoneRegex.test('(123) 456-7890')); // true
console.log(usPhoneRegex.test('123-456-7890'));    // true
console.log(usPhoneRegex.test('1234567890'));      // true
console.log(usPhoneRegex.test('+1 123-456-7890')); // true
console.log(usPhoneRegex.test('12345'));            // false

// International phone with country code
const intlPhone = /^\+\d{1,3}\s?\d{4,14}$/;
console.log(intlPhone.test('+44 7911123456'));  // true
console.log(intlPhone.test('+966 501234567'));  // true

Example: URL Validation

// URL validation pattern
const urlRegex = /^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&\/=]*)$/;

console.log(urlRegex.test('https://example.com'));           // true
console.log(urlRegex.test('http://www.example.com/path'));   // true
console.log(urlRegex.test('www.example.com'));               // true
console.log(urlRegex.test('example.com/page?id=1'));         // true
console.log(urlRegex.test('not a url'));                     // false

// Extract URL components with named groups
const urlParts = /^(?<protocol>https?:\/\/)?(?<domain>[^\/\s]+)(?<path>\/[^\s]*)?$/;
const parsed = 'https://example.com/path/page'.match(urlParts);
console.log(parsed.groups.protocol); // "https://"
console.log(parsed.groups.domain);   // "example.com"
console.log(parsed.groups.path);     // "/path/page"

Real-World Practical Examples

Let us examine several real-world scenarios where regular expressions solve common development challenges. These examples demonstrate how regex patterns combine the concepts we have covered into practical solutions.

Example: Extracting Data from Strings

// Extract all hashtags from a social media post
const post = 'Learning #JavaScript and #RegExp is fun! #WebDev';
const hashtags = post.match(/#\w+/g);
console.log(hashtags); // ["#JavaScript", "#RegExp", "#WebDev"]

// Extract all numbers (including decimals) from text
const report = 'Sales grew 15.5% to $1,234.56 million in Q3';
const numbers = report.match(/\d+\.?\d*/g);
console.log(numbers); // ["15.5", "1", "234.56", "3"]

// Parse CSV line respecting quoted fields
const csvLine = 'John,"Doe, Jr.",30,"New York"';
const fields = [...csvLine.matchAll(/(?:"([^"]*)"|([^,]+))/g)]
    .map(m => m[1] !== undefined ? m[1] : m[2]);
console.log(fields); // ["John", "Doe, Jr.", "30", "New York"]

Example: Text Transformation and Cleanup

// Convert camelCase to kebab-case
function toKebabCase(str) {
    return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
console.log(toKebabCase('backgroundColor')); // "background-color"
console.log(toKebabCase('fontSize'));         // "font-size"

// Remove HTML tags from a string
function stripHtml(html) {
    return html.replace(/<[^>]*>/g, '');
}
console.log(stripHtml('<p>Hello <b>world</b></p>'));
// "Hello world"

// Normalize whitespace
function normalizeSpaces(text) {
    return text.replace(/\s+/g, ' ').trim();
}
console.log(normalizeSpaces('  Hello   world  \n  !'));
// "Hello world !"

// Mask sensitive data
function maskEmail(email) {
    return email.replace(/^(.).+(@.+)$/, '$1****$2');
}
console.log(maskEmail('john.doe@example.com')); // "j****@example.com"

// Mask credit card number
function maskCard(number) {
    return number.replace(/\d(?=\d{4})/g, '*');
}
console.log(maskCard('4111222233334444')); // "************4444"

Example: Input Sanitization and Formatting

// Format phone number as you type
function formatPhone(input) {
    const digits = input.replace(/\D/g, '');
    if (digits.length <= 3) return digits;
    if (digits.length <= 6) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
    return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
}
console.log(formatPhone('1234567890')); // "(123) 456-7890"

// Validate and format a slug
function createSlug(title) {
    return title
        .toLowerCase()
        .replace(/[^\w\s-]/g, '')   // remove non-word characters
        .replace(/\s+/g, '-')       // replace spaces with hyphens
        .replace(/-+/g, '-')        // collapse multiple hyphens
        .replace(/^-|-$/g, '');     // trim leading/trailing hyphens
}
console.log(createSlug('Hello World! This is a Test.'));
// "hello-world-this-is-a-test"

// Syntax highlighting for code snippets (simplified)
function highlightSyntax(code) {
    return code
        .replace(/\b(const|let|var|function|return|if|else)\b/g,
            '<span class="keyword">$1</span>')
        .replace(/(\/\/.*$)/gm,
            '<span class="comment">$1</span>')
        .replace(/('[^']*'|"[^"]*")/g,
            '<span class="string">$1</span>');
}
console.log(highlightSyntax('const name = "world"; // greeting'));
Common Mistake: Never use regex to fully parse HTML. Regular expressions cannot handle nested structures or the full complexity of HTML. Use the DOM parser (DOMParser) for HTML parsing. Regex is fine for simple tag stripping or extracting data from known, controlled HTML fragments.

Example: Working with the split() Method

// Split by multiple delimiters
const data = 'apple, banana; cherry  grape';
const fruits = data.split(/[,;\s]+/);
console.log(fruits); // ["apple", "banana", "cherry", "grape"]

// Split and keep the delimiter
const sentence = 'Hello! How are you? I am fine.';
const parts = sentence.split(/([!?.])\s*/);
console.log(parts);
// ["Hello", "!", "How are you", "?", "I am fine", ".", ""]

// Split camelCase into words
const camel = 'backgroundColor';
const words = camel.split(/(?=[A-Z])/);
console.log(words); // ["background", "Color"]

Performance Considerations

While regular expressions are powerful, poorly written patterns can cause severe performance issues, including catastrophic backtracking that can freeze your application. Understanding these pitfalls helps you write efficient regex patterns.

Example: Avoiding Performance Pitfalls

// BAD: Catastrophic backtracking with nested quantifiers
// This pattern can take exponential time on non-matching strings
// const bad = /^(a+)+$/;  // DO NOT USE

// GOOD: Simplified equivalent without nested quantifiers
const good = /^a+$/;

// BAD: Greedy matching over large strings
// const badHtml = /<div>.*<\/div>/;  // scans entire string

// GOOD: Use specific negated character class or lazy quantifier
const goodHtml = /<div>[^<]*<\/div>/;

// Compile regex outside loops for better performance
const regex = /\d+/g;  // compiled once
const data = ['abc123', 'def456', 'ghi789'];

// GOOD: reusing compiled regex
const results = data.map(str => str.match(regex));

// Tip: Reset lastIndex when reusing a global regex
const globalRegex = /test/g;
console.log(globalRegex.test('test')); // true
console.log(globalRegex.lastIndex);    // 4
globalRegex.lastIndex = 0;             // reset before reuse
console.log(globalRegex.test('test')); // true
Note: When using a regex with the g flag in a loop with test() or exec(), the lastIndex property advances with each call. Always reset lastIndex to 0 before reusing the regex on a new string, or you may get unexpected results.

Practice Exercise

Build a complete form validation module using regular expressions. Create validation functions for the following fields: (1) a username that must be 3-20 characters containing only letters, numbers, and underscores, and must start with a letter; (2) a password that requires at least 8 characters with at least one uppercase letter, one lowercase letter, one digit, and one special character; (3) an email address with a standard format; (4) a URL that must start with http or https; (5) a date in YYYY-MM-DD format where the month is between 01 and 12 and the day is between 01 and 31. Write a function that takes any input string and identifies all embedded dates, extracting them into an array of objects with year, month, and day properties using named groups. Also write a text transformation function that converts a block of text into title case (capitalize the first letter of each word) while keeping common prepositions lowercase. Test all your functions with both valid and invalid inputs, and log the results to the console.