Помощь по лучшему способу разбора цифр из строки в Java - PullRequest
5 голосов
/ 04 июня 2009

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

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

Я хотел бы вывести: ["34", "A", "312", "O", "5", "M", "444123", "A"]

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

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, который решает эту проблему элегантно.
  • Где вы проводите грань между использованием regEx и написанием чего-то более простого, чем написанное выше?
  • Как вы повышаете удобочитаемость / удобство обслуживания regExes?

Ответы [ 8 ]

13 голосов
/ 04 июня 2009

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

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

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

EDIT: обновлено регулярное выражение после комментария mmyers.

7 голосов
/ 04 июня 2009

Для служебного класса, проверьте java.util.Scanner . Есть несколько вариантов того, как вы можете решить свою проблему. У меня есть несколько комментариев на ваши вопросы.

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

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

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

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

Сверхурочные регулярные выражения растут органично и становятся монстрами.

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

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

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

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

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

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

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

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

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

Как вы повышаете удобочитаемость / удобство обслуживания regExes?

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

5 голосов
/ 05 июня 2009

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

// 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)");

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

2 голосов
/ 04 июня 2009

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

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

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;
}
1 голос
/ 05 июня 2009

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

Первая версия без регулярных выражений. Обратите внимание, что я использую 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;
}

Теперь версия регулярного выражения. Это в основном тот же код, который был опубликован Juha S., но регулярное выражение на самом деле работает.

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]
1 голос
/ 05 июня 2009

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

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

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

1 голос
/ 05 июня 2009

вы можете использовать этот класс для упрощения вашего цикла:

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 цента

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

1 голос
/ 04 июня 2009

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

Выход:

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"+"]");
    }

}
...