Regular Expressions

Reviewed & published by Brayan K

A regular expression — "regex" — is a tiny pattern language for describing text. With a handful of symbols you can validate an email, pull every number out of a log line, or clean up messy input. It looks cryptic at first, but the core vocabulary is small, and once it clicks you'll reach for it constantly. This lesson builds that vocabulary from the ground up using C#'s Regex class.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A regex is like a search filter on steroids. A normal "find" box looks for exact text — "find cat". A regex lets you describe a shape instead: "find any 5-digit number", "find any word starting with a capital letter", "find anything that looks like an email". You're not searching for a specific string, you're searching for a kind of string — and that's enormously powerful.

1. Does It Match? Regex.IsMatch

The simplest question is a yes/no: does the text contain something matching the pattern? Regex.IsMatch answers it. Patterns are built from metacharacters like \d (a digit) and quantifiers like + (one or more). Wrap a pattern in ^...$ to require the whole string to match — the key trick for validation.

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        // A regex (regular expression) is a PATTERN that describes text.
        // The simplest question: does the text contain a match? -> IsMatch
        // Tip: use a verbatim string @"..." so backslashes aren't escaped.

        // \d means "a digit", + means "one or more".
        Console.WriteLine(Regex.IsMatch("Order 42", @"\d+"));    // True (finds 42)
        Console.WriteLine(Regex.IsMatch("No numbers", @"\d+"));  // False

        // ^ anchors to the start, $ to the end. So this means "ONLY digits":
        Console.WriteLine(Regex.IsMatch("12345", @"^\d+$"));     // True
        Console.WriteLine(Regex.IsMatch("12a45", @"^\d+$"));     // False
    }
}

// ✅ Expected output:
//    True
//    False
//    True
//    False

2. The Core Metacharacters

The whole language is built from a small set of pieces: character classes (\d, \w, \s), the wildcard ., quantifiers (+, *, ?, {n}), character sets ([abc]), and alternation (x|y). Combine them and you can describe surprisingly specific patterns.

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        // Common building blocks (metacharacters):
        //   \d  digit       \w  word char (letter/digit/_)   \s  whitespace
        //   .   any char     +   1 or more   *   0 or more   ?   0 or 1
        //   {n} exactly n    [abc] any of a,b,c    (x|y) x or y

        // A simple UK-style postcode-ish pattern: 2 letters, 1-2 digits:
        string pattern = @"^[A-Za-z]{2}\d{1,2}$";
        Console.WriteLine(Regex.IsMatch("AB12", pattern)); // True
        Console.WriteLine(Regex.IsMatch("A123", pattern)); // False (1 letter)

        // Quantifiers in action:
        Console.WriteLine(Regex.IsMatch("colour", @"colou?r")); // True
        Console.WriteLine(Regex.IsMatch("color",  @"colou?r")); // True (u optional)
    }
}

// ✅ Expected output:
//    True
//    False
//    True
//    True

3. Finding Matches: Match & Matches

IsMatch only says yes/no. To actually get the matched text, use Regex.Match for the first hit (with .Value and .Index) or Regex.Matches to iterate over every match. Extracting all the numbers from a string is a classic one-liner.

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        string text = "Call me on 020-7946-0018 today";

        // Match finds the FIRST match and gives you details:
        Match m = Regex.Match(text, @"\d{3}-\d{4}-\d{4}");
        if (m.Success)
        {
            Console.WriteLine(m.Value);  // 020-7946-0018
            Console.WriteLine(m.Index);  // 11 (where it starts)
        }

        // Matches finds ALL matches:
        string log = "errors: 5, warnings: 12, info: 230";
        foreach (Match num in Regex.Matches(log, @"\d+"))
            Console.Write(num.Value + " ");  // 5 12 230
        Console.WriteLine();
    }
}

// ✅ Expected output:
//    020-7946-0018
//    11
//    5 12 230

Your turn — write an email check and extract all the words from a sentence.

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".

        // 1) Write a pattern that matches a simple email: something@something.something
        //    Hint: \S+ means "one or more non-space chars".
        string emailPattern = ___;   // 👉  @"^\S+@\S+\.\S+$"

        Console.WriteLine(Regex.IsMatch("hi@site.com", emailPattern)); // True
        Console.WriteLine(Regex.IsMatch("not-an-email", emailPattern));// False

        // 2) Extract all the WORDS from this sentence using \w+
        string sentence = "the quick brown fox";
        foreach (Match w in Regex.Matches(sentence, ___))  // 👉  @"\w+"
            Console.Write(w.Value + "|");
        Console.WriteLine();
        // ✅ Expected (line 2):  the|quick|brown|fox|
    }
}

4. Groups & Replacement

Parentheses (...) capture parts of a match into groups. Named groups (?<year>\d{4}) are the clearest way to pull structured data out of text. And Regex.Replace can rewrite text using those groups via $1, $2 — perfect for reformatting.

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        // Parentheses create GROUPS you can pull out of a match.
        // NAMED groups (?<name>...) are clearest.
        string date = "2025-03-09";
        var m = Regex.Match(date, @"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})");

        if (m.Success)
        {
            Console.WriteLine(m.Groups["year"].Value);  // 2025
            Console.WriteLine(m.Groups["month"].Value); // 03
            Console.WriteLine(m.Groups["day"].Value);   // 09
        }

        // Replace using a captured group: turn 2025-03-09 into 09/03/2025
        string uk = Regex.Replace(date,
            @"(\d{4})-(\d{2})-(\d{2})",
            "$3/$2/$1");                 // $1,$2,$3 refer to the groups
        Console.WriteLine(uk);          // 09/03/2025
    }
}

// ✅ Expected output:
//    2025
//    03
//    09
//    09/03/2025

🧩 Reorder Challenge

These lines validate a 4-digit PIN, but they're scrambled. Order them so the program compiles and prints whether the PIN is valid.

A. string pin = "1234";
B. bool ok = Regex.IsMatch(pin, pattern);
C. string pattern = @"^\d{4}$";
D. Console.WriteLine($"Valid PIN: {ok}");
A. string pin = "1234";
C. string pattern = @"^\d{4}$";
B. bool ok = Regex.IsMatch(pin, pattern);
D. Console.WriteLine($"Valid PIN: {ok}");

// Output:  Valid PIN: True

The input and the pattern must both exist before IsMatch uses them; the result ok must exist before it's printed.

🧠 Quick Recall

1. What does this print?

Console.WriteLine(Regex.IsMatch("abc123", @"\d+"));

True — the string contains digits (123), so \d+ finds a match.

2. What does this print?

Console.WriteLine(Regex.IsMatch("abc123", @"^\d+$"));

False — the anchors require the WHOLE string to be digits, but it starts with letters.

3. What does this print?

Console.WriteLine(Regex.Replace("a1b2c3", @"\d", "*"));

a*b*c* — every digit is replaced by an asterisk.

Putting It Together: an Input Toolkit

Cleaning and validating user input is regex's daily bread. Here it strips non-digits from a phone number, validates a username, and collapses messy whitespace.

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        // === A small input-cleaning & validation toolkit ===

        // 1) Strip every non-digit from a messy phone number:
        string raw = "+44 (020) 7946-0018";
        string digits = Regex.Replace(raw, @"\D", "");  // \D = NOT a digit
        Console.WriteLine(digits);   // 4402079460018

        // 2) Validate a username: 3-16 chars, letters/digits/underscore only:
        string userPattern = @"^\w{3,16}$";
        Console.WriteLine(Regex.IsMatch("ada_lovelace", userPattern)); // True
        Console.WriteLine(Regex.IsMatch("ab", userPattern));           // False (too short)

        // 3) Collapse runs of whitespace into single spaces (tidy user text):
        string messy = "too    many     spaces";
        string tidy = Regex.Replace(messy, @"\s+", " ");
        Console.WriteLine(tidy);     // too many spaces
    }
}

// ✅ Expected output:
//    4402079460018
//    True
//    False
//    too many spaces

📋 Cheat Sheet

TokenMatchesExample
\dA digit 0-9\d{3} → 123
\wLetter/digit/_\w+ → hello_1
\sWhitespace\s+ → spaces
.Any charactera.c → abc, a9c
+ * ?1+, 0+, 0-1colou?r
^ $Start / end^\d+$
[abc]Any one of a,b,c[A-Z]
(x|y)x or y(cat|dog)

Pro Tips

Common Errors (and the fix)

Mini-Challenge: Extract Hashtags

Pull every hashtag out of a social-media post. A clean, real use of Regex.Matches with a simple pattern.

using System;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        // 🎯 MINI-CHALLENGE: extract hashtags
        // 1. Given:  string post = "Loving #csharp and #dotnet today! #100DaysOfCode";
        // 2. Use Regex.Matches with a pattern for a hashtag:
        //    a # followed by one or more word chars  ->  @"#\w+"
        // 3. Print each hashtag on its own line.
        //
        // ✅ Example output:
        //    #csharp
        //    #dotnet
        //    #100DaysOfCode

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

What does the metacharacter \d match?

  • Any whitespace character
  • Any word character
  • A single digit 0-9
  • The end of a line

Answer: A single digit 0-9. \d matches a single digit; \w is a word char and \s is whitespace.

What does the quantifier + mean in a regex?

  • One or more
  • Zero or one
  • Zero or more
  • Exactly one

Answer: One or more. + means one or more; * means zero or more, and ? means zero or one.

What do the anchors ^ and $ do?

  • Match any character
  • Escape special characters
  • Create a capture group
  • Anchor to the start and end of the string

Answer: Anchor to the start and end of the string. ^ anchors to the start and $ to the end, so ^...$ requires the whole string to match.

Which Regex method returns a simple bool 'is there a match anywhere?'

  • Regex.Match
  • Regex.IsMatch
  • Regex.Matches
  • Regex.Replace

Answer: Regex.IsMatch. IsMatch returns a bool; Match returns the first match's details and Matches returns all matches.

What does the quantifier ? mean (as in colou?r)?

  • The preceding item is optional (zero or one)
  • One or more of the preceding
  • Any single character
  • Start of a group

Answer: The preceding item is optional (zero or one). ? makes the preceding item optional - zero or one occurrence - so colou?r matches both color and colour.

In Regex.Replace, how do you refer to the first captured group in the replacement string?

  • \1
  • {1}
  • $1
  • %1

Answer: $1. Numbered groups are referenced as $1, $2, etc. in the replacement string.

How do you read a named group from a match, e.g. (?<year>\d{4})?

  • m.Groups[0].Value
  • m.Groups["year"].Value
  • m.Named["year"]
  • m.year

Answer: m.Groups["year"].Value. Named groups are read with m.Groups["year"].Value.

Why are verbatim strings @"..." recommended for regex patterns?

  • They run faster
  • They enable named groups
  • They anchor the pattern automatically
  • Backslashes stay literal, so \d doesn't need doubling

Answer: Backslashes stay literal, so \d doesn't need doubling. In a verbatim string backslashes are literal, so @"\d+" stays readable instead of "\\d+".

What does \D match?

  • A digit
  • Any character that is NOT a digit
  • A word boundary
  • A literal D

Answer: Any character that is NOT a digit. Uppercase \D is the negation - it matches any non-digit character.

Why does \d+ match the string "abc123" while ^\d+$ does not?

  • \d+ is case-insensitive
  • \d+ matches letters too
  • ^\d+$ requires the WHOLE string to be digits; \d+ only needs some digits somewhere
  • They behave identically

Answer: ^\d+$ requires the WHOLE string to be digits; \d+ only needs some digits somewhere. Without anchors \d+ matches any digits within the string; ^\d+$ requires the entire string to be digits.

Continue this course

Frequently asked questions

Why should I use @"..." (verbatim strings) for regex?

Regex patterns are full of backslashes (\d, \w, \s). In a normal C# string you'd have to double every backslash. A verbatim string @"..." treats backslashes literally, so @"\d+" stays readable instead of becoming "\\d+".

What's the difference between IsMatch, Match, and Matches?

IsMatch returns a bool — 'is there a match anywhere?'. Match returns the first match with details (Value, Index, Groups). Matches returns a collection of every match in the text. Pick the one for the question you're asking.

What do ^ and $ do?

^ anchors the pattern to the start of the string and $ to the end. Wrapping a pattern in ^...$ means 'the WHOLE string must match', not just contain a match — essential for validation like ^\d+$ ('only digits').

How do I pull out parts of a match?

Use groups. Parentheses (...) capture a part, and named groups (?<year>\d{4}) let you read them by name via m.Groups["year"].Value. In replacements, refer to groups with $1, $2 or ${name}.

Is regex the right tool for everything?

No. Regex is great for patterns in text — validation, extraction, find-and-replace. But for structured formats like HTML, JSON, or full email RFC validation, use a proper parser or library. Reach for regex when the pattern is simple and local.

Related lessons