Удаление пробелов, не находящихся в кавычках

Есть строка String str = "\"hi, pasha\" + \"hi \"";
Из нее надо сделать такую: "\"hi, pasha\" + \"hi \"" (то есть убрать все повторяющиеся подряд пробелы, которые не находятся между кавычек)

Я пытался сделать вот так:

public static String removeDuplicatedSpaces(String string) {
    ArrayList<Character> chars = new ArrayList<>();
    for (char c : string.toCharArray()) {
        chars.add(c);
    }
    String removed = "";
    boolean isIn = false;

    for (int i = 0; i < chars.size(); i++) {
        if (chars.get(i) == '"') {
            isIn = !isIn;
        }
        if (chars.get(i) == ' ' && chars.get(i + 1) == ' ' && !isIn && i != chars.size() - 1) {
            chars.remove(i + 1);
        }
        else {
            removed += chars.get(i);
        }
    }

    return removed;
}

Но выводит "hi, pasha" +"hi "


Ответы (1 шт):

Автор решения: Nowhere Man

Основная проблема в представленном коде для заданного примера, проверяющего только дублированные пробелы, сводится к тому, что после удаления второго пробела нужно "остаться" на предыдущей позиции в списке символов, то есть декрементировать i.

Есть также баг в том, что сначала происходит обращение к следующему символу и лишь потом проверяется выход за пределы списка символов, что приводит к исключению IndexOutOfBoundsException, когда несколько пробелов находятся в конце исходной строки.

Исправленный код выглядит так:

// ...
    if (chars.get(i) == ' ' && !isIn && i < chars.size() - 1 && chars.get(i + 1) == ' ') {
        System.out.println("CHAR : '" + chars.get(i) + "' - REMOVING");
        chars.remove(i--);  // удалить текущий пробел и сохранить позицию
    }
// ...

Результат работы теста (без отладочного вывода):

System.out.println("[" + removeDuplicatedSpaces("\"hi,    pasha\"             +    \"hi  \",   how  are  ya  doin?   ") + "]");
["hi,    pasha" + "hi  ", how are ya doin? ]

Однако следует заметить, что для решения данной задачи преобразование строки в список символов с последующим удалением символов из этого списка и конкатенация в цикле в строке removed += chars.get(i) значительно снижают эффективность метода.

Кроме того, можно обрабатывать не только пробелы, но и пробельные символы (например, табуляцию \t), используя метод Character::isWhitespace.

Более производительный способ заключается в использовании StringBuilder, для которого резервируется размер строки, и добавляются только нужные символы:

public static String removeDuplicatedSpaces2(String str) {
    if (null == str || str.isEmpty()) return str;
    int n = str.length();
    StringBuilder sb = new StringBuilder(n);
    boolean inQuotes = false;
    boolean prevSpace = false;

    for (int i = 0; i < n; i++) {
        char c = str.charAt(i);
        if (c == '"') {
            inQuotes = !inQuotes;
        } else if (Character.isWhitespace(c)) {
            if (!inQuotes && prevSpace) {
                continue;
            }
            prevSpace = true;
        } else {
            prevSpace = false;
        }
        sb.append(c);
    }

    return sb.toString();
}

Результат теста аналогичен предыдущему.


И наконец, для наиболее лаконичного решения можно составить регулярное выражение и воспользоваться методом из Java 9+ Matcher::replaceAll(Function<MatchResult, String> replacer):

private static Pattern SPACES = Pattern.compile("(\"[^\"]*(\"|$))|\\s+");    
public static String removeDuplicatedSpacesRegex(String str) {
    return SPACES.matcher(str).replaceAll(mr -> null == mr.group(1) ? " " : mr.group(1));
}

то есть, регулярка (\"[^\"]*(\"|$))|\\s ищет либо текст в кавычках (\"[^\"]*(\"|$)) (причём кавычка может быть незакрытая) как первую группу, либо один и более пробельных символов \\s+.

Затем лямбда-функция mr -> null == mr.group(1) ? " " : mr.group(1) либо возвращает соответственно первую группу group(1) как есть, либо заменяет последовательность пробельных символов одним пробелом.

Замечание: при необходимости вместо стандартного класса пробельных символов
\s : [ \t\n\x0B\f\r]
можно использовать класс горизонтальных пробельных символов
\h : [ \t\xA0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000].

→ Ссылка