Find & Replace — Kotlin source
Find and replace text with literal or regular-expression matching, global replace, case sensitivity, whole-word, and capture-group substitution. Live match counter.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Find & replace with literal or regex matching, $-substitution
// ($1 backrefs, $&, $$), case sensitivity, whole-word, and global modes.
//
// Language: Kotlin (1.9+, JVM — java.util.regex via the Kotlin stdlib)
// Source: CosmoDev polyglot showcase port of the find-replace tool,
// ported from src/lib/findReplace.ts (the canonical TypeScript
// implementation).
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Mirrors the live lib: a literal find string is Regex.escape'd and matched
// verbatim; an isRegex find is compiled as-is. \b wraps the pattern when
// wholeWord is set, and RegexOption composes IGNORE_CASE and MULTILINE (the
// JS i / m flags). PatternSyntaxException is caught and returned as the
// error string (the lib never throws), and an empty find is a no-op.
//
// Replacement $-substitution is implemented in expandMatch (not Java's \1)
// so it matches JavaScript's String.replace exactly for the realistic
// cases: $$ -> $, $& -> whole match, $1..$99 -> capture group (literal
// "$<digits>" when out of range). JS's $` and $' are unsupported.
import java.util.regex.PatternSyntaxException
/** Options mirror the TypeScript lib's FindReplaceOptions field for field. */
data class FindReplaceOptions(
val isRegex: Boolean = false,
val caseSensitive: Boolean = true,
val wholeWord: Boolean = false,
val global: Boolean = true,
val multiline: Boolean = false,
)
data class FindReplaceResult(
val result: String,
val matches: Int,
val error: String? = null,
)
private fun Char.isAsciiDigit(): Boolean = this in '0'..'9'
/** Apply JS String.replace $-substitution for one match.
* "$$" -> "$"; "$&" -> whole match; "$1".."$99" -> capture group N
* (literal "$<digits>" when N is out of range, matching JS).
* groups[n] returns "" for a group that did not participate, as in JS. */
fun expandMatch(template: String, match: MatchResult): String {
val numGroups = match.groups.size - 1
fun group(n: Int): String = match.groups[n]?.value ?: ""
val out = StringBuilder()
var i = 0
while (i < template.length) {
val c = template[i]
if (c != '$') {
out.append(c)
i++
continue
}
val n = if (i + 1 < template.length) template[i + 1] else null
when {
n == '$' -> {
out.append('$')
i += 2
}
n == '&' -> {
out.append(group(0))
i += 2
}
n != null && n.isAsciiDigit() -> {
val d1 = n - '0'
// Greedily try a second digit ($nn), matching JS.
if (i + 2 < template.length && template[i + 2].isAsciiDigit()) {
val d2 = d1 * 10 + (template[i + 2] - '0')
if (d2 in 1..numGroups) {
out.append(group(d2))
i += 3
continue
}
}
if (d1 in 1..numGroups) {
out.append(group(d1))
i += 2
} else {
out.append('$').append(n)
i += 2
}
}
else -> {
out.append('$')
i++
}
}
}
return out.toString()
}
fun findReplace(
input: String,
find: String,
replacement: String,
options: FindReplaceOptions = FindReplaceOptions(),
): FindReplaceResult {
if (find.isEmpty()) return FindReplaceResult(input, 0) // empty find is a no-op
val pattern = if (options.isRegex) find else Regex.escape(find)
val wrapped = if (options.wholeWord) "\\b$pattern\\b" else pattern
val opts = buildSet {
if (!options.caseSensitive) add(RegexOption.IGNORE_CASE)
if (options.isRegex && options.multiline) add(RegexOption.MULTILINE)
}
val re = try {
Regex(wrapped, opts)
} catch (e: PatternSyntaxException) {
return FindReplaceResult(input, 0, e.message)
}
val out = StringBuilder()
var matches = 0
var numGroups = 0
var last = 0
// findAll guarantees forward progress on empty matches.
for (m in re.findAll(input)) {
numGroups = m.groups.size - 1
out.append(input, last, m.range.first)
out.append(expandMatch(replacement, m))
last = m.range.last + 1
matches++
if (!options.global) break
}
out.append(input, last, input.length)
if (matches != 0 && !options.global) {
matches = 1 + numGroups // JS String.match length quirk
}
return FindReplaceResult(out.toString(), matches)
}
fun main() {
val r = findReplace(
"Hello World world", "world", "Universe",
FindReplaceOptions(caseSensitive = false),
)
println(r.error?.let { "error: $it" } ?: "${r.result} (${r.matches} matches)")
}
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 →