Skip to content

Regex Explainer — Swift source

Translate a regular expression into plain English, step by step. Explains anchors, character classes, quantifiers, groups, escapes, alternation, and flags.

This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.

// regex-explainer — Swift port: tokenize a regex into labeled tokens + describe JS flags.
// Mirrors src/lib/regexExplain.ts (canonical TS). Validation compiles with
// NSRegularExpression (ICU) — near-JS syntax; JS-only constructs report ok=false.
import Foundation

struct RegexToken { let token: String; let description: String }
struct FlagInfo { let flag: String; let description: String }
struct ExplainResult {
    let ok: Bool; let tokens: [RegexToken]; let flags: [FlagInfo]; let error: String?
}

let flagDesc = ["g": "global - find all matches", "i": "case-insensitive",
    "m": "multiline (^ and $ match line boundaries)", "s": "dotAll - \".\" matches newlines",
    "u": "unicode", "y": "sticky - match at lastIndex", "d": "indices - expose match boundaries"]

let escapeDesc = ["d": "a digit [0-9]", "D": "a non-digit", "w": "a word character [A-Za-z0-9_]",
    "W": "a non-word character", "s": "a whitespace character", "S": "a non-whitespace character",
    "b": "a word boundary", "B": "a non-word boundary", "n": "a newline", "t": "a tab",
    "r": "a carriage return"]

/// Index of the "]" closing a class opened at start; a leading "]" is a literal member.
func findClassEnd(_ p: [Character], _ start: Int) -> Int {
    var i = start + 1
    if i < p.count, p[i] == "^" { i += 1 }
    if i < p.count, p[i] == "]" { i += 1 }
    while i < p.count, p[i] != "]" { if p[i] == "\\" { i += 1 }; i += 1 }
    return i < p.count ? i : p.count - 1
}

/// Index of the ")" matching the group opened at start; skips classes + escapes.
func findGroupEnd(_ p: [Character], _ start: Int) -> Int {
    var depth = 1, i = start + 1
    while i < p.count, depth > 0 {
        if p[i] == "\\" { i += 2; continue }
        if p[i] == "[" { i = findClassEnd(p, i) + 1; continue }
        if p[i] == "(" { depth += 1 } else if p[i] == ")" { depth -= 1 }
        i += 1
    }
    return i - 1
}

func describeGroup(_ grp: String) -> String {
    for (pre, label) in [("?:", "non-capturing group"), ("?=", "lookahead assertion (positive)"),
                         ("?!", "lookahead assertion (negative)"), ("?<=", "lookbehind assertion (positive)"),
                         ("?<!", "lookbehind assertion (negative)")] where grp.hasPrefix("(" + pre) {
        return label
    }
    return "capturing group"
}

func describeClass(_ inner: String) -> String {
    inner.isEmpty ? "(empty)" : inner.replacingOccurrences(of: "\\", with: "\\\\")
}

/// Explain a regex pattern + flags into tokens. Never throws.
func explainRegex(_ pattern: String, _ flags: String = "") -> ExplainResult {
    var opts = NSRegularExpression.Options() // JS i/m/s map onto ICU options
    for f in flags {
        if f == "i" { opts.insert(.caseInsensitive) }
        if f == "m" { opts.insert(.anchorsMatchLines) }
        if f == "s" { opts.insert(.dotMatchesLineSeparators) }
    }
    do { // validate with the native engine first
        _ = try NSRegularExpression(pattern: pattern, options: opts)
    } catch {
        return ExplainResult(ok: false, tokens: [], flags: [], error: error.localizedDescription)
    }

    var tokens: [RegexToken] = []
    func push(_ token: String, _ description: String) { tokens.append(RegexToken(token: token, description: description)) }
    let p = Array(pattern) // per-character walk, mirroring the TS per-code-unit walk
    var i = 0
    while i < p.count {
        let ch = String(p[i])
        switch ch {
        case "^": push("^", "start of the string (or line with /m)"); i += 1
        case "$": push("$", "end of the string (or line with /m)"); i += 1
        case ".": push(".", "any character (except newline, unless /s)"); i += 1
        case "|": push("|", "OR - alternation between groups"); i += 1
        case "\\":
            let next = i + 1 < p.count ? String(p[i + 1]) : ""
            push("\\" + next, escapeDesc[next] ?? "an escaped literal \"\(next)\"")
            i += 2
        case "[":
            let end = findClassEnd(p, i)
            let cls = String(p[i...end])
            let negated = i + 1 < p.count && p[i + 1] == "^"
            let inner = String(p[(i + 1 + (negated ? 1 : 0))..<end])
            push(cls, "match any \(negated ? "character NOT in" : "of"): \(describeClass(inner))")
            i = end + 1
        case "(":
            let end = findGroupEnd(p, i)
            let grp = String(p[i...end])
            push(grp, describeGroup(grp))
            i = end + 1
        case "*", "+", "?":
            let lazy = i + 1 < p.count && p[i + 1] == "?"
            let base = ["*": "0 or more times", "+": "1 or more times", "?": "0 or 1 time (optional)"][ch]!
            push(ch + (lazy ? "?" : ""), "quantifier - \(base)\(lazy ? " (lazy/non-greedy)" : " (greedy)")")
            i += lazy ? 2 : 1
        case "{":
            if let end = p[i...].firstIndex(of: "}") { // bounded quantifier {n,m}
                let lazy = end + 1 < p.count && p[end + 1] == "?"
                let q = String(p[i...end])
                push(q + (lazy ? "?" : ""),
                     "quantifier - repeat \(String(p[(i + 1)..<end])) time(s)\(lazy ? " (lazy)" : "")")
                i = end + 1 + (lazy ? 1 : 0)
            } else { // no closing brace: a literal "{"
                push("{", "the literal \"{\"")
                i += 1
            }
        default: // a literal character
            push(ch, "the literal \"\(ch)\"")
            i += 1
        }
    }

    let flagList = flags.map { FlagInfo(flag: String($0),
        description: flagDesc[String($0)] ?? "unknown flag \"\($0)\"") }
    return ExplainResult(ok: true, tokens: tokens, flags: flagList, error: nil)
}

let r = explainRegex("^(\\w+)@([\\w.-]+)$", "gi")
if let err = r.error {
    print("error: \(err)")
} else {
    for t in r.tokens { print(t.token.padding(toLength: 14, withPad: " ", startingAt: 0) + " " + t.description) }
    for f in r.flags { print("flag \(f.flag): \(f.description)") }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →