Find & Replace — Swift 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 Swift 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: Swift (5.9+, Foundation's NSRegularExpression)
// 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 escaped with
// escapedPattern(for:) and matched verbatim; an isRegex find is compiled
// as-is. \b wraps the pattern when wholeWord is set, and NSRegularExpression
// composes .caseInsensitive and .anchorsMatchLines (the JS i / m flags).
// The throwing initializer's error is surfaced as the error string (the
// lib never traps), and an empty find is a no-op.
//
// Replacement $-substitution is implemented in expandMatch (not
// NSRegularExpression's template syntax) 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 intentionally unsupported.
//
// NSRegularExpression works in UTF-16 space, so this port runs the loop on
// NSString ranges (the idiomatic Foundation dance) and converts once at
// the end.
import Foundation
/// Options mirror the TypeScript lib's FindReplaceOptions field for field.
struct FindReplaceOptions {
var isRegex = false
var caseSensitive = true
var wholeWord = false
var global = true
var multiline = false
}
struct FindReplaceResult {
var result: String
var matches: Int
var error: String?
}
/// Escape metacharacters for literals, wrap \b..\b for whole-word.
func buildPattern(_ find: String, _ o: FindReplaceOptions) -> String {
var pattern = o.isRegex ? find : NSRegularExpression.escapedPattern(for: find)
if o.wholeWord {
pattern = "\\b\(pattern)\\b"
}
return pattern
}
func isAsciiDigit(_ c: Character) -> Bool {
if let v = c.wholeNumberValue, c.isASCII, (0...9).contains(v) {
return true
}
return false
}
/// 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).
/// A group that did not participate has location NSNotFound and yields "",
/// as in JS.
func expandMatch(_ template: String, _ m: NSTextCheckingResult, _ input: NSString) -> String {
let numGroups = m.numberOfRanges - 1
func group(_ n: Int) -> String {
guard n <= numGroups else { return "" }
let r = m.range(at: n)
if r.location == NSNotFound { return "" }
return input.substring(with: r)
}
let chars = Array(template)
var out = ""
var i = 0
while i < chars.count {
let c = chars[i]
if c != "$" {
out.append(c)
i += 1
continue
}
guard i + 1 < chars.count else {
out.append("$")
i += 1
continue
}
let n = chars[i + 1]
if n == "$" {
out.append("$")
i += 2
} else if n == "&" {
out.append(group(0))
i += 2
} else if isAsciiDigit(n), let d1 = n.wholeNumberValue {
// Greedily try a second digit ($nn), matching JS.
var two: Int?
if i + 2 < chars.count, isAsciiDigit(chars[i + 2]),
let d2c = chars[i + 2].wholeNumberValue {
two = d1 * 10 + d2c
}
if let d2 = two, (1...numGroups).contains(d2) {
out.append(group(d2))
i += 3
continue
}
if (1...numGroups).contains(d1) {
out.append(group(d1))
i += 2
} else {
out.append("$")
out.append(n)
i += 2
}
} else {
out.append("$")
i += 1
}
}
return out
}
func findReplace(
_ input: String,
_ find: String,
_ replacement: String,
_ o: FindReplaceOptions = FindReplaceOptions()
) -> FindReplaceResult {
if find.isEmpty {
return FindReplaceResult(result: input, matches: 0, error: nil) // empty find is a no-op
}
var opts = NSRegularExpression.Options()
if !o.caseSensitive { opts.insert(.caseInsensitive) }
if o.isRegex && o.multiline { opts.insert(.anchorsMatchLines) }
let re: NSRegularExpression
do {
re = try NSRegularExpression(pattern: buildPattern(find, o), options: opts)
} catch let error as NSError {
return FindReplaceResult(result: input, matches: 0, error: error.localizedDescription)
}
let numGroups = re.numberOfCaptureGroups
let nsInput = input as NSString
let full = NSRange(location: 0, length: nsInput.length)
// matches(in:options:range:) guarantees forward progress on empty matches.
let allMatches = re.matches(in: input, options: [], range: full)
let out = NSMutableString()
var last = 0
var matches = 0
for m in allMatches {
let r = m.range // group 0
out.append(nsInput.substring(with: NSRange(location: last, length: r.location - last)))
out.append(expandMatch(replacement, m, nsInput))
last = r.location + r.length
matches += 1
if !o.global { break }
}
out.append(nsInput.substring(with: NSRange(location: last, length: nsInput.length - last)))
if matches != 0 && !o.global {
matches = 1 + numGroups // JS String.match length quirk
}
return FindReplaceResult(result: out as String, matches: matches, error: nil)
}
// Top-level demo — run as main.swift (swift main.swift).
let demo = findReplace("Hello World world", "world", "Universe",
FindReplaceOptions(caseSensitive: false))
if let err = demo.error {
print("error: \(err)")
} else {
print("\(demo.result) (\(demo.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 →