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 →