Skip to content

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 "$&lt;digits&gt;" 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 →