Find & Replace — Java 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 Java 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: Java (17+, standard library only)
// 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 Pattern.quote'd (\Q..\E)
// and matched verbatim; an isRegex find is compiled as-is. \b wraps the
// pattern when wholeWord is set, and Pattern composes CASE_INSENSITIVE 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
// appendReplacement, which would re-interpret $ and \ in substituted text)
// 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.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public final class FindReplacePort {
/** Options mirror the TypeScript lib's FindReplaceOptions field for field. */
public record Options(
boolean isRegex,
boolean caseSensitive,
boolean wholeWord,
boolean globalReplace,
boolean multiline) {
public static final Options DEFAULT = new Options(false, true, false, true, false);
}
public record FindReplaceResult(String result, int matches, String error) {}
/** Quote literals for verbatim matching, wrap \b..\b for whole-word. */
static String buildPattern(String find, Options o) {
String pattern = o.isRegex() ? find : Pattern.quote(find);
return o.wholeWord() ? "\\b" + pattern + "\\b" : pattern;
}
static int compileFlags(Options o) {
int flags = 0;
if (!o.caseSensitive()) {
flags |= Pattern.CASE_INSENSITIVE;
}
if (o.isRegex() && o.multiline()) {
flags |= Pattern.MULTILINE;
}
return flags;
}
static boolean isAsciiDigit(char c) {
return c >= '0' && c <= '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).
* An unmatched group expands to "", as in JS.
*/
static String expandMatch(String template, Matcher m, int numGroups) {
StringBuilder out = new StringBuilder();
int i = 0;
int n = template.length();
while (i < n) {
char c = template.charAt(i);
if (c != '$') {
out.append(c);
i++;
continue;
}
char nx = i + 1 < n ? template.charAt(i + 1) : '\0';
if (nx == '$') {
out.append('$');
i += 2;
} else if (nx == '&') {
out.append(m.group());
i += 2;
} else if (isAsciiDigit(nx)) {
int d1 = nx - '0';
// Greedily try a second digit ($nn), matching JS.
if (i + 2 < n && isAsciiDigit(template.charAt(i + 2))) {
int d2 = d1 * 10 + (template.charAt(i + 2) - '0');
if (d2 >= 1 && d2 <= numGroups) {
String g2 = m.group(d2);
out.append(g2 == null ? "" : g2);
i += 3;
continue;
}
}
if (d1 >= 1 && d1 <= numGroups) {
String g1 = m.group(d1);
out.append(g1 == null ? "" : g1);
i += 2;
} else {
out.append('$').append(nx);
i += 2;
}
} else {
out.append('$');
i++;
}
}
return out.toString();
}
public static FindReplaceResult findReplace(
String input, String find, String replacement, Options o) {
if (find.isEmpty()) {
return new FindReplaceResult(input, 0, null); // empty find is a no-op
}
Pattern re;
try {
re = Pattern.compile(buildPattern(find, o), compileFlags(o));
} catch (PatternSyntaxException e) {
return new FindReplaceResult(input, 0, e.getMessage());
}
Matcher m = re.matcher(input);
int numGroups = m.groupCount();
// Built by hand (not appendReplacement) so $ and \ inside captured
// text are never re-interpreted. Matcher.find guarantees forward
// progress on empty matches.
StringBuilder out = new StringBuilder();
int matches = 0;
int last = 0;
while (m.find()) {
out.append(input, last, m.start());
out.append(expandMatch(replacement, m, numGroups));
last = m.end();
matches++;
if (!o.globalReplace()) {
break;
}
}
out.append(input, last, input.length());
if (matches != 0 && !o.globalReplace()) {
matches = 1 + numGroups; // JS String.match length quirk
}
return new FindReplaceResult(out.toString(), matches, null);
}
public static void main(String[] args) {
FindReplaceResult r = findReplace(
"Hello World world", "world", "Universe",
new Options(false, false, false, true, false));
if (r.error() != null) {
System.out.println("error: " + r.error());
} else {
System.out.println(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 →