Вопрос

У меня есть строка, содержащая цифры и буквы.Я хочу разбить строку на смежные фрагменты цифр и смежные фрагменты букв.

Рассмотрим строку «34A312O5M444123A».

Я хотел бы вывести:["34", "А", "312", "О", "5", "М", "444123", "А"]

У меня есть код, который работает и выглядит так:

List<String> digitsAsElements(String str){
  StringBuilder digitCollector = new StringBuilder();

  List<String> output = new ArrayList<String>();

  for (int i = 0; i < str.length(); i++){
    char cChar = str.charAt(i);

    if (Character.isDigit(cChar))
       digitCollector.append(cChar);
    else{
      output.add(digitCollector.toString());
      output.add(""+cChar);

      digitCollector = new StringBuilder();
    }         
  }

  return output;
}

Я подумал о том, чтобы разделить str дважды, чтобы получить массив, содержащий все фрагменты чисел, и массив, содержащий фрагменты всех букв.Затем объединяем результаты.Я избегал этого, так как это навредило бы читабельности.

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

  • Отладчики плохо с ними справляются.
  • Они прерывают поток чтения исходного кода.
  • Регулярные выражения со сверхурочной работой растут органично и превращаются в монстров.
  • Они глубоко неинтуитивны.

Мои вопросы:

  • Как я могу улучшить читаемость приведенного выше кода?
  • Есть лучший способ сделать это?Класс Util, который элегантно решает эту проблему.
  • Где вы проводите грань между использованием регулярного выражения и кодированием чего-то похожего на то, что я написал выше?
  • Как повысить читаемость/удобство обслуживания регулярных выражений?
Это было полезно?

Решение

Хотели бы вы использовать регулярные выражения, если бы это означало решение проблемы в одной строке кода?

// Split at any position that's either:
// preceded by a digit and followed by a non-digit, or
// preceded by a non-digit and followed by a digit.
String[] parts = str.split("(?<=\\d)(?=\\D)|(?<=\\D)(?=\\d)");

С комментарием, объясняющим регулярное выражение, я думаю, что он более читаем, чем любое из решений, не связанных с регулярными выражениями (или любое другое решение для регулярных выражений, если на то пошло).

Другие советы

Для этой конкретной задачи я всегда использовал регулярное выражение вместо того, чтобы писать что-то подобное от руки.Код, который вы привели выше, по крайней мере для меня, менее читабелен, чем простое регулярное выражение (которое было бы (\d+|[^\d]+) в данном случае, насколько я понимаю).

Возможно, вам следует избегать написания регулярных выражений, длина которых превышает несколько строк.Они могут быть и обычно являются нечитаемыми и трудными для понимания. но как и код, которым их можно заменить! Парсеры почти никогда не бывают красивыми, и обычно лучше прочитать исходную грамматику, чем пытаться разобраться в сгенерированном (или написанном от руки) парсере.То же самое касается (имхо) регулярных выражений, которые представляют собой просто краткое описание обычной грамматики.

Итак, в целом я бы сказал, что запрет регулярных выражений в пользу кода, который вы указали в своем вопросе, звучит как ужасно глупая идея.А регулярные выражения — это всего лишь инструмент, не меньше и не больше.Если что-то другое лучше справляется с анализом текста (скажем, настоящий синтаксический анализатор, какая-то магия подстрок и т. д.), тогда используйте его.Но не отказывайтесь от возможностей только потому, что вам с ними некомфортно – у других может быть меньше проблем с ними, и все люди способны учиться.

РЕДАКТИРОВАТЬ:Обновлено регулярное выражение после комментария ммайерса.

Для класса полезности проверьте java.util.Сканер.Здесь есть несколько вариантов решения вашей проблемы.У меня есть несколько комментариев по вашим вопросам.

Отладчики плохо справляются с ними (регулярными выражениями).

Работает ли регулярное выражение или нет, зависит от того, что находится в ваших данных.Есть несколько хороших плагинов, которые помогут вам создать регулярное выражение, например QuickREx для Eclipse, действительно ли отладчик помогает вам написать правильный синтаксический анализатор для ваших данных?

Они прерывают поток чтения исходного кода.

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

Регулярные выражения со сверхурочной работой растут органично и превращаются в монстров.

Я думаю, что да, но это, вероятно, проблема в том, что код, в котором они живут, становится несфокусированным.Если сложность исходных данных возрастает, вам, вероятно, нужно следить за тем, нужно ли вам более выразительное решение (возможно, генератор синтаксического анализатора, такой как ANTLR).

Они глубоко неинтуитивны.

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

Как я могу улучшить читаемость приведенного выше кода?

Не уверен, кроме использования регулярного выражения.

Есть лучший способ сделать это?Класс Util, который элегантно решает эту проблему.

Упомянутый выше java.util.Scanner.

Где вы проводите грань между использованием регулярного выражения и кодированием чего-то похожего на то, что я написал выше?

Лично я использую регулярное выражение для чего-то достаточно простого.

Как повысить читаемость/удобство обслуживания регулярных выражений?

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

Я бы использовал что-то вроде этого (предупреждение, непроверенный код).Для меня это гораздо более читаемо, чем попытка избежать регулярных выражений.Регулярные выражения — отличный инструмент, если использовать их в правильном месте.

Комментирование методов и предоставление примеров входных и выходных значений в комментариях также помогает.

List<String> digitsAsElements(String str){
    Pattern p = Pattern.compile("(\\d+|\\w+)*");
    Matcher m = p.matcher(str);

    List<String> output = new ArrayList<String>();
    for(int i = 1; i <= m.groupCount(); i++) {
       output.add(m.group(i));
    }
    return output;
}

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

Например, если вы закодировали метод «Захватить блок цифр или букв», вызывающий объект будет очень простым, прямым циклом, просто печатающим результаты каждого вызова, а метод, который вы вызываете, будет четко определен, поэтому Цель регулярного выражения будет ясна, даже если вы ничего не знаете о синтаксисе, а метод будет ограничен, чтобы люди не могли его испортить со временем.

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

Поскольку, похоже, никто еще не опубликовал правильный код, я попробую.

Сначала версия без регулярных выражений.Обратите внимание, что я использую StringBuilder для накопления любого типа символов, который был виден последним (цифровой или нецифровой).Если состояние изменится, я выгружаю его содержимое в список и запускаю новый StringBuilder.Таким образом, последовательные нецифры группируются так же, как и последовательные цифры.

static List<String> digitsAsElements(String str) {
    StringBuilder collector = new StringBuilder();

    List<String> output = new ArrayList<String>();
    boolean lastWasDigit = false;
    for (int i = 0; i < str.length(); i++) {
        char cChar = str.charAt(i);

        boolean isDigit = Character.isDigit(cChar);
        if (isDigit != lastWasDigit) {
            if (collector.length() > 0) {
                output.add(collector.toString());
                collector = new StringBuilder();
            }
            lastWasDigit = isDigit;
        }
        collector.append(cChar);
    }
    if (collector.length() > 0)
        output.add(collector.toString());

    return output;
}

Теперь версия регулярного выражения.По сути, это тот же код, который был опубликован Юхой С., но регулярное выражение действительно работает.

private static final Pattern DIGIT_OR_NONDIGIT_STRING =
        Pattern.compile("(\\d+|[^\\d]+)");
static List<String> digitsAsElementsR(String str) {
    // Match a consecutive series of digits or non-digits
    final Matcher matcher = DIGIT_OR_NONDIGIT_STRING.matcher(str);
    final List<String> output = new ArrayList<String>();
    while (matcher.find()) {
        output.add(matcher.group());
    }
    return output;
}

Один из способов сохранить читабельность регулярных выражений — это их имена.Я думаю DIGIT_OR_NONDIGIT_STRING довольно хорошо передает то, что я (программист) думаю, и тестирование должно убедиться, что он действительно делает то, для чего предназначен.

public static void main(String[] args) {
    System.out.println(digitsAsElements( "34A312O5MNI444123A"));
    System.out.println(digitsAsElementsR("34A312O5MNI444123A"));
}

принты:

[34, A, 312, O, 5, MNI, 444123, A]
[34, A, 312, O, 5, MNI, 444123, A]

Оууу, кто-то опередил меня в написании кода.Я думаю, что версию регулярного выражения легче читать/поддерживать.Также обратите внимание на разницу в выводе двух реализаций по сравнению с ожидаемым результатом...

Выход:

digitsAsElements1("34A312O5MNI444123A") = [34, A, 312, O, 5, M, , N, , I, 444123, A]
digitsAsElements2("34A312O5MNI444123A") = [34, A, 312, O, 5, MNI, 444123, A]
Expected: [34, A, 312, O, 5, MN, 444123, A]

Сравнивать:

DigitsAsElements.java:

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class DigitsAsElements {

    static List<String> digitsAsElements1(String str){
        StringBuilder digitCollector = new StringBuilder();

        List<String> output = new ArrayList<String>();

        for (int i = 0; i < str.length(); i++){
          char cChar = str.charAt(i);

          if (Character.isDigit(cChar))
             digitCollector.append(cChar);
          else{
            output.add(digitCollector.toString());
            output.add(""+cChar);

            digitCollector = new StringBuilder();
          }         
        }

        return output;
      }

    static List<String> digitsAsElements2(String str){
        // Match a consecutive series of digits or non-digits
        final Pattern pattern = Pattern.compile("(\\d+|\\D+)");
        final Matcher matcher = pattern.matcher(str);

        final List<String> output = new ArrayList<String>();
        while (matcher.find()) {
            output.add(matcher.group());
        }

        return output;
      }

    /**
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("digitsAsElements(\"34A312O5MNI444123A\") = " +
                digitsAsElements1("34A312O5MNI444123A"));
        System.out.println("digitsAsElements2(\"34A312O5MNI444123A\") = " +
                digitsAsElements2("34A312O5MNI444123A"));
        System.out.println("Expected: [" +
                "34, A, 312, O, 5, MN, 444123, A"+"]");
    }

}

вы можете использовать этот класс, чтобы упростить цикл:

public class StringIterator implements Iterator<Character> {

    private final char[] chars;
    private int i;

    private StringIterator(char[] chars) {
        this.chars = chars;
    }

    public boolean hasNext() {
        return i < chars.length;
    }

    public Character next() {
        return chars[i++];
    }

    public void remove() {
        throw new UnsupportedOperationException("Not supported.");
    }

    public static Iterable<Character> of(String string) {
        final char[] chars = string.toCharArray();

        return new Iterable<Character>() {

            @Override
            public Iterator<Character> iterator() {
                return new StringIterator(chars);
            }
        };
    }
}

Теперь вы можете переписать это:

for (int i = 0; i < str.length(); i++){
    char cChar = str.charAt(i);
    ...
}

с:

for (Character cChar : StringIterator.of(str)) {
    ...
}

мои 2 цента

Кстати, этот класс также можно использовать повторно в другом контексте.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top