Множественные одновременные замены подстрок в Java

(Я пришел из мира питонов, поэтому я извиняюсь, если в какой-то терминологии я использую банки с нормой.)

У меня есть String со List индексов начала и конца для замены. Не вдаваясь в подробности, рассмотрите этот базовый макет:

String text = "my email is [email protected] and my number is (213)-XXX-XXXX"
List<Token> findings = SomeModule.someFnc(text);

И у Token есть определение

class Token {
    int start, end;
    String type;
}

Этот List представляет начальную и конечную позиции чувствительных данных, которые я пытаюсь изменить.

Эффективно API возвращает данные, которые я перебираю, чтобы получить:

[{ "start" : 12, "end" : 22, "type" : "EMAIL_ADDRESS" }, { "start" : 41, "end" : 54, "type" : "PHONE_NUMBER" }]

Используя эти данные, моя конечная цель - отредактировать токены в text указанном этими объектами Token чтобы получить это:

"my email is [EMAIL_ADDRESS] and my number is [PHONE_NUMBER]"

То, что делает этот вопрос нетривиальным, заключается в том, что подстроки замены не всегда имеют ту же длину, что и подстроки, которые они заменяют.

Мой текущий план действий состоит в том, чтобы построить StringBuilder из text, отсортировать эти идентификаторы в обратном порядке от начальных индексов, а затем заменить с правого конца буфера.

Но что-то говорит мне, что должен быть лучший способ... есть?

Ответ 1

Этот подход работает:

import java.util.ArrayList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        String text = "my email is [email protected] and my number is (213)-XXX-XXXX";

        List<Token> findings = new ArrayList<>();
        findings.add(new Token(12, 22, "EMAIL_ADDRESS"));
        findings.add(new Token(41, 54, "PHONE_NUMBER"));

        System.out.println(replace(text, findings));
    }

    public static String replace(String text, List<Token> findings) {
        int position = 0;
        StringBuilder result = new StringBuilder();

        for (Token finding : findings) {
            result.append(text.substring(position, finding.start));
            result.append('[').append(finding.type).append(']');

            position = finding.end + 1;
        }

        return result.append(text.substring(position)).toString();
    }
}

class Token {
    int start, end;
    String type;

    Token(int start, int end, String type) {
        this.start = start;
        this.end = end;
        this.type = type;
    }
}

Выход:

my email is [EMAIL_ADDRESS] and my number is [PHONE_NUMBER]

Ответ 2

Убедитесь, что все маркеры отсортированы по start индексу в порядке возрастания:

List<Token> tokens = new ArrayList<>();
tokens.sort(Comparator.comparing(Token::getStart));

Теперь вы можете заменить все строки, начиная с конца входного текста:

public String replace(String text, List<Token> tokens) {
    StringBuilder sb = new StringBuilder(text);
    for (int i = tokens.size() - 1; i >= 0; i--) {
        Token token = tokens.get(i);
        sb.replace(token.start, token.end + 1, "[" + token.type + "]");
    }
    return sb.toString();
}

Ответ 3

Извлеките подстроку между началом и концом и разделите ее. Затем вы получаете массив из двух элементов, вставляете то, что хотите между ними. Затем вам нужно переместить следующие строки, чтобы заменить идентификаторы на разницу между (предыдущей строкой, которую вы заменили длиной), и (строкой, которую вы положили на свое место).

Код (в любом случае "конец" в Token является эксклюзивным):

public class Main {

    public static void main(String... args) {
        String text = "I want to replace AAA and B and scary wombat";
        Token[] tokens = {new Token(18, 21, "TEST"), new Token(26, 27, "TEST"), new Token(32, 44, "TEST")};
        int delta = 0;
        for (Token token : tokens) {
            String splitter = text.substring(token.start + delta, token.end + delta);
            System.out.println("Splitter: " + splitter);
            delta += token.replacement.length() - splitter.length();
            String[] beforeAndAfter = text.split(Pattern.quote(splitter));
            text = beforeAndAfter[0] + token.replacement + 
                    (beforeAndAfter.length == 2 ? beforeAndAfter[1] : ""); // in case where there are no more chars after splitter in text
        }
        System.out.println(text);
    }

    static class Token {
        public final int start, end;
        public final String replacement;

        public Token(int start, int end, String replacement) {
            this.start = start;
            this.end = end;
            this.replacement = replacement;
        }
    }
}