Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Matching Operators

Matching operators test if strings match regular expression patterns.

Overview

Suji provides two operators for testing strings against regular expressions, making text validation concise and readable.

The operators are ~ and !~ — there is no =~. Both yield only a boolean. Suji’s regex support is deliberately match-only:

  • no capture groups (::captures(), ::match(), ::find() do not exist)
  • no regex replace — ::replace(old, new) takes strings only
  • no regex split — ::split(sep) takes a string separator
  • regex literals are not interpolated: /${var}/ is passed to the engine literally and fails to compile

Regex values are first class: you can store one in a variable and use it as a match arm pattern.

Match Operator (~)

Tests if a string matches a regex pattern:

import std:println

text = "user@example.com"
pattern = /^[^@]+@[^@]+\.[^@]+$/

# Returns true if matches
is_email = text ~ pattern
println(is_email)  # true

Basic Matching

import std:println

# Check if string contains pattern
println("hello world" ~ /world/)      # true
println("hello world" ~ /goodbye/)    # false

# Case-sensitive by default
println("Hello" ~ /hello/)            # false
println("Hello" ~ /(?i)hello/)        # true (case-insensitive)

Negative Match Operator (!~)

Tests if a string does NOT match a pattern:

import std:println

text = "hello123"

println(text !~ /world/)      # true (doesn't contain "world")
println(text !~ /hello/)      # false (contains "hello")

# Useful for validation
username = "alice_123"
is_valid_username = username !~ /[^a-zA-Z0-9_]/
println(is_valid_username)    # true - contains NO invalid characters

Common Patterns

Email Validation

import std:println

validate_email = |email| {
    email ~ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
}

println(validate_email("user@example.com"))    # true
println(validate_email("invalid.email"))        # false

URL Validation

import std:println

validate_url = |url| {
    url ~ /^https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\/.*)?$/
}

println(validate_url("https://example.com"))       # true
println(validate_url("http://site.co.uk/path"))    # true
println(validate_url("not-a-url"))                 # false

Phone Number

import std:println

validate_phone = |phone| {
    phone ~ /^\d{3}-\d{3}-\d{4}$/
}

println(validate_phone("555-123-4567"))  # true
println(validate_phone("555-1234"))      # false
println(validate_phone("5551234567"))    # false

Contains Digits

import std:println

has_digits = |text| {
    text ~ /\d/
}

println(has_digits("hello123"))  # true
println(has_digits("hello"))     # false

Starts/Ends With

import std:println

# Starts with "http"
println("http://example.com" ~ /^http/)    # true
println("https://example.com" ~ /^http/)   # true
println("ftp://example.com" ~ /^http/)     # false

# Ends with ".com"
println("example.com" ~ /\.com$/)          # true
println("example.org" ~ /\.com$/)          # false

In Conditionals

Matching operators work well with match expressions:

import std:println

username = "alice_123"

match username ~ /^[a-zA-Z0-9_]+$/ {
    true => println("Valid username"),
    false => println("Invalid username: contains special characters"),
}

In Filters

Use matching to filter lists:

import std:println

emails = [
    "valid@example.com",
    "invalid.email",
    "another@test.org",
    "bad@"
]

pattern = /^[^@]+@[^@]+\.[^@]+$/
valid_emails = emails::filter(|e| e ~ pattern)

println(valid_emails)
# [valid@example.com, another@test.org]

Pattern Matching with Match

Combine with match for branching on patterns:

import std:println

text = "Call me at 555-1234"

words = text::split(" ")
phones = words::filter(|w| w ~ /^\d{3}-\d{4}$/)

match {
    phones::length() > 0 => println("Phone: " + phones[0]),
    _ => println("No phone found"),
}

A regex literal can also be used directly as a match arm pattern, which is often tidier than a chain of ~ tests:

import std:println

classify = |token| {
    match token {
        /^\d+$/ => "number",
        /^[a-z]+$/ => "word",
        _ => "mixed",
    }
}

println(classify("42"))       # number
println(classify("hello"))    # word
println(classify("h3llo"))    # mixed

Because regex values are ordinary values, you can name them and reuse them:

import std:println

digits_only = /^\d+$/

println("123" ~ digits_only)  # true
println("12a" ~ digits_only)  # false

What you cannot do is build a pattern dynamically. Regex literals are not interpolated, so /${some_var}/ is handed to the engine verbatim and fails with a Regex error, and there is no function that turns a string into a regex. Patterns must be written out literally in the source.

Negation Patterns

Use !~ to check absence of patterns:

import std:println

# Check password doesn't contain spaces
password = "MyP@ssw0rd"
no_spaces = password !~ /\s/
println(no_spaces)  # true

# Check username has no special chars
username = "alice_123"
only_alphanumeric = username !~ /[^a-zA-Z0-9_]/
println(only_alphanumeric)  # true

Combining with Logical Operators

import std:println

validate_password = |password| {
    # All these conditions must be true
    long_enough = password::length() >= 8
    has_upper = password ~ /[A-Z]/
    has_lower = password ~ /[a-z]/
    has_digit = password ~ /[0-9]/
    has_special = password ~ /[!@#$%^&*]/
    
    long_enough && has_upper && has_lower && has_digit && has_special
}

println(validate_password("Passw0rd!"))  # true
println(validate_password("password"))   # false

Inline Regex Modes

The regex engine supports inline mode modifiers like (?i) (case-insensitive) and (?m) (multiline).

import std:println

text = "Hello World"

# Case-insensitive
println(text ~ /(?i)hello/)       # true

# Multiline
multiline = "line1\nline2"
println(multiline ~ /(?m)^line2/) # true

Common Pitfalls

Pitfall 1: Not Escaping Special Characters

import std:println

# Unescaped dot matches any character
println("axb" ~ /a.b/)   # true  - the dot matched "x"
println("a.b" ~ /a.b/)   # true  - and it matches a literal dot too

# Escape the dot to match only a literal dot
println("axb" ~ /a\.b/)  # false
println("a.b" ~ /a\.b/)  # true

Pitfall 2: Forgetting Anchors

import std:println

# Partial match (finds "123" anywhere)
println("hello123world" ~ /\d+/)  # true

# Anchored (entire string must be digits)
println("hello123world" ~ /^\d+$/)  # false
println("123" ~ /^\d+$/)            # true

Pitfall 3: Case Sensitivity

import std:println

# Case-sensitive by default
println("Hello" ~ /hello/)   # false

# Use inline case-insensitive mode
println("Hello" ~ /(?i)hello/)  # true

Pitfall 4: Expecting Greediness to Matter

Greedy and non-greedy quantifiers are both accepted, but since ~ returns only “did it match”, the difference is invisible — you never get to see what matched:

import std:println

text = "<tag>content</tag>"

println(text ~ /<.*>/)   # true
println(text ~ /<.*?>/)  # true - same answer

If you need the matched text, regex will not help you: use string methods such as ::index_of(), ::split() and slicing (s[1;4]) to carve the value out yourself.

Best Practices

DO:

  • Use anchors (^, $) for exact matches
  • Escape special regex characters
  • Reach for ::contains(), ::starts_with() and ::ends_with() when a plain substring test will do
  • Test patterns thoroughly
  • Add comments for complex regex

DON’T:

  • Expect captured groups, regex replace or regex split — none exist
  • Write =~; the operator is ~
  • Try to interpolate a pattern (/${var}/ is a regex error)
  • Forget case sensitivity
  • Use regex to parse HTML/XML

Examples

Username Validation

import std:println

validate_username = |username| {
    # 3-20 characters, alphanumeric and underscore only
    valid_format = username ~ /^[a-zA-Z0-9_]{3,20}$/
    no_double_underscore = username !~ /__/
    not_starts_with_number = username !~ /^[0-9]/
    
    valid_format && no_double_underscore && not_starts_with_number
}

println(validate_username("alice_123"))    # true
println(validate_username("ab"))           # false (too short)
println(validate_username("alice__bob"))   # false (double underscore)
println(validate_username("123alice"))     # false (starts with number)

Extract Domain from URL

import std:println

get_domain = |url| {
    url !~ /^https?:\/\// && return nil

    parts = url::split("://")
    parts::length() < 2 && return nil

    host_and_path = parts[1]
    host_parts = host_and_path::split("/")
    host_parts[0]
}

println(get_domain("https://example.com/path"))  # example.com
println(get_domain("http://site.org"))           # site.org
println(get_domain("ftp://site.org"))            # nil

Sanitize Input

There is no regex replace, so filtering has to be done character by character. Note that strings are not iterableloop through text is a runtime error, so convert with ::to_list() first:

import std:println

sanitize = |text| {
    # Keep only alphanumerics, spaces, hyphens and underscores
    result = ""
    loop through text::to_list() with char {
        match { char ~ /[a-zA-Z0-9 _-]/ => { result = result + char } }
    }
    result
}

println(sanitize("Hello, World!"))      # Hello World
println(sanitize("Test@#$%123"))        # Test123

Password Strength Checker

import std:println

check_password_strength = |password| {
    score = 0
    
    match { password::length() >= 8 => { score = score + 1 } }
    match { password::length() >= 12 => { score = score + 1 } }
    match { password ~ /[A-Z]/ => { score = score + 1 } }
    match { password ~ /[a-z]/ => { score = score + 1 } }
    match { password ~ /[0-9]/ => { score = score + 1 } }
    match { password ~ /[!@#$%^&*]/ => { score = score + 1 } }
    
    match {
        score >= 5 => "Strong",
        score >= 3 => "Medium",
        _ => "Weak",
    }
}

println(check_password_strength("abc123"))          # Weak
println(check_password_strength("Abc123"))          # Medium
println(check_password_strength("Abc123!@"))        # Strong

File Extension Check

import std:println

is_image = |filename| {
    filename ~ /(?i)\.(jpg|jpeg|png|gif|webp)$/
}

is_document = |filename| {
    filename ~ /(?i)\.(pdf|doc|docx|txt)$/
}

println(is_image("photo.jpg"))        # true
println(is_image("document.pdf"))     # false
println(is_document("report.pdf"))    # true

Next Steps

See Also