Skip to content

Regex Explainer — Kotlin source

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

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

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

data class RegexToken(val token: String, val description: String)
data class FlagInfo(val flag: String, val description: String)
data class ExplainResult(val ok: Boolean, val tokens: List<RegexToken>, val flags: List<FlagInfo>, val error: String?)

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

val ESCAPE_DESC = mapOf(
    "d" to "a digit [0-9]", "D" to "a non-digit", "w" to "a word character [A-Za-z0-9_]",
    "W" to "a non-word character", "s" to "a whitespace character", "S" to "a non-whitespace character",
    "b" to "a word boundary", "B" to "a non-word boundary", "n" to "a newline",
    "t" to "a tab", "r" to "a carriage return")

fun describeGroup(grp: String) = when {
    grp.startsWith("(?:") -> "non-capturing group"
    grp.startsWith("(?=") -> "lookahead assertion (positive)"
    grp.startsWith("(?!" ) -> "lookahead assertion (negative)"
    grp.startsWith("(?<=") -> "lookbehind assertion (positive)"
    grp.startsWith("(?<!") -> "lookbehind assertion (negative)"
    else -> "capturing group"
}

fun describeClass(inner: String) =
    if (inner.isEmpty()) "(empty)" else inner.replace("\\", "\\\\") // double '\' for display

/** Index of the ']' closing a class opened at start; a leading ']' is a literal member. */
fun findClassEnd(p: String, start: Int): Int {
    var i = start + 1
    if (i < p.length && p[i] == '^') i++
    if (i < p.length && p[i] == ']') i++
    while (i < p.length && p[i] != ']') { if (p[i] == '\\') i++; i++ }
    return if (i < p.length) i else p.length - 1
}

/** Index of the ')' matching the group opened at start; skips classes + escapes. */
fun findGroupEnd(p: String, start: Int): Int {
    var depth = 1
    var i = start + 1
    while (i < p.length && depth > 0) {
        when (p[i]) {
            '\\' -> i += 2
            '[' -> i = findClassEnd(p, i) + 1
            '(' -> { depth++; i++ }
            ')' -> { depth--; i++ }
            else -> i++
        }
    }
    return i - 1
}

/** Explain a regex pattern + flags into tokens. Never throws. */
fun explainRegex(pattern: String, flags: String = ""): ExplainResult {
    // Validate with the native engine first (JS i/m/s map onto java.util.regex bits).
    var opts = 0
    for (f in flags) when (f) {
        'i' -> opts = opts or (Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE)
        'm' -> opts = opts or Pattern.MULTILINE
        's' -> opts = opts or Pattern.DOTALL
    }
    try {
        Pattern.compile(pattern, opts)
    } catch (e: PatternSyntaxException) {
        return ExplainResult(false, emptyList(), emptyList(), e.message)
    }

    val tokens = mutableListOf<RegexToken>()
    fun push(token: String, description: String) { tokens += RegexToken(token, description) }
    var i = 0
    while (i < pattern.length) {
        when (val ch = pattern[i]) {
            '^' -> { push("^", "start of the string (or line with /m)"); i++ }
            '$' -> { push("$", "end of the string (or line with /m)"); i++ }
            '.' -> { push(".", "any character (except newline, unless /s)"); i++ }
            '|' -> { push("|", "OR - alternation between groups"); i++ }
            '\\' -> {
                val next = if (i + 1 < pattern.length) pattern[i + 1].toString() else ""
                push("\\$next", ESCAPE_DESC[next] ?: "an escaped literal \"$next\"")
                i += 2
            }
            '[' -> {
                val end = findClassEnd(pattern, i)
                val cls = pattern.substring(i, end + 1)
                val negated = pattern[i + 1] == '^'
                val inner = cls.substring(1 + (if (negated) 1 else 0), cls.length - 1)
                push(cls, "match any ${if (negated) "character NOT in" else "of"}: ${describeClass(inner)}")
                i = end + 1
            }
            '(' -> {
                val end = findGroupEnd(pattern, i)
                val grp = pattern.substring(i, end + 1)
                push(grp, describeGroup(grp))
                i = end + 1
            }
            '*', '+', '?' -> {
                val lazy = pattern[i + 1] == '?'
                val base = when (ch) { '*' -> "0 or more times"; '+' -> "1 or more times"; else -> "0 or 1 time (optional)" }
                push("$ch${if (lazy) "?" else ""}", "quantifier - $base${if (lazy) " (lazy/non-greedy)" else " (greedy)"}")
                i += if (lazy) 2 else 1
            }
            '{' -> {
                val end = pattern.indexOf('}', i)
                if (end != -1) { // bounded quantifier {n,m}
                    val lazy = end + 1 < pattern.length && pattern[end + 1] == '?'
                    val q = pattern.substring(i, end + 1)
                    push("$q${if (lazy) "?" else ""}", "quantifier - repeat ${q.substring(1, q.length - 1)} time(s)${if (lazy) " (lazy)" else ""}")
                    i = end + 1 + (if (lazy) 1 else 0)
                } else { // no closing brace: a literal '{'
                    push("{", "the literal \"{\"")
                    i++
                }
            }
            else -> { // a literal character
                push(ch.toString(), "the literal \"$ch\"")
                i++
            }
        }
    }

    val flagList = flags.map { FlagInfo(it.toString(), FLAG_DESC[it.toString()] ?: "unknown flag \"$it\"") }
    return ExplainResult(true, tokens, flagList, null)
}

fun main() {
    val r = explainRegex("^(\\w+)@([\\w.-]+)$", "gi")
    if (!r.ok) { println("error: ${r.error}"); return }
    r.tokens.forEach { println("%-14s %s".format(it.token, it.description)) }
    r.flags.forEach { println("flag ${it.flag}: ${it.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 →