5

我有一个包含数字和字母的字符串。我希望将字符串拆分为连续的数字块和连续的字母块。

考虑字符串“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 类。
  • 您在使用正则表达式和编写类似于我上面所写内容的代码之间划清界限?
  • 您如何提高正则表达式的可读性/可维护性?
4

8 回答 8

13

对于这个特定的任务,我总是使用正则表达式而不是手写类似的东西。至少对我而言,您上面给出的代码比简单的正则表达式可读性差((\d+|[^\d]+)据我所知,在这种情况下)。

您可能希望避免编写超过几行的正则表达式。这些可能而且通常是不可读且难以理解的,但是可以替换它们的代码也是如此!解析器几乎从来都不是漂亮的,您通常最好阅读原始语法而不是试图理解生成的(或手写的)解析器。正则表达式也是如此(恕我直言),这只是对常规语法的简明描述。

因此,总的来说,我会说禁止正则表达式以支持您在问题中给出的代码听起来是一个非常愚蠢的想法。而正则表达式只是一个工具,仅此而已。如果其他东西在文本解析方面做得更好(例如,真正的解析器,一些子字符串魔术等),那么使用它。但是不要仅仅因为你对它们感到不舒服就放弃可能性——其他人在应对它们时可能会遇到更少的问题,而且所有人都能学习。

编辑:在 mmyers 评论后更新了正则表达式。

于 2009-06-04T19:35:24.450 回答
7

对于实用程序类,请查看java.util.Scanner。关于如何解决问题,有很多选择。我对你的问题有几点意见。

调试器不能很好地处理它们(正则表达式)

正则表达式是否有效取决于您的数据中的内容。您可以使用一些不错的插件来帮助您构建正则表达式,例如用于 Eclipse 的QuickREx,调试器是否真的可以帮助您为数据编写正确的解析器?

它们打断了阅读源代码的人的流程。

我想这取决于你对他们的舒适程度。就个人而言,我宁愿阅读一个合理的正则表达式,也不愿多读 50 行字符串解析代码,但这可能是个人的事情。

加班正则表达式有机地成长并成为怪物。

我想他们可能会,但这可能是他们生活的代码变得不集中的问题。如果源数据的复杂性在增加,您可能需要密切关注是否需要更具表现力的解决方案(可能是像 ANTLR 这样的解析器生成器)

它们非常不直观。

它们是一种模式匹配语言。我会说它们在这种情况下非常直观。

如何提高上述代码的可读性?

不确定,除了使用正则表达式。

有一个更好的方法吗?一个优雅地解决这个问题的 Util 类。

上面提到了java.util.Scanner。

您在使用正则表达式和编写类似于我上面所写内容的代码之间划清界限?

我个人将正则表达式用于任何相当简单的事情。

您如何提高正则表达式的可读性/可维护性?

在扩展之前仔细考虑,特别注意详细注释代码和正则表达式,以便清楚你在做什么。

于 2009-06-04T19:59:12.470 回答
5

如果正则表达式意味着用一行代码解决问题,你愿意使用它吗?

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

有了解释正则表达式的评论,我认为这比任何非正则表达式解决方案(或任何其他正则表达式解决方案,就此而言)更具可读性。

于 2009-06-05T00:50:05.703 回答
2

我会使用这样的东西(警告,未经测试的代码)。对我来说,这比试图避免正则表达式更具可读性。在正确的地方使用正则表达式是一个很好的工具。

注释方法并在注释中提供输入和输出值的示例也有帮助。

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;
}
于 2009-06-04T19:54:40.980 回答
1

哇,有人打我写代码。我认为正则表达式版本更易于阅读/维护。另外,请注意 2 个实现与预期输出之间的输出差异......

输出:

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

}
于 2009-06-04T19:58:47.960 回答
1

你可以使用这个类来简化你的循环:

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 美分

顺便说一句,这个类也可以在其他上下文中重用。

于 2009-06-04T20:04:08.217 回答
1

我自己并没有对正则表达式过于疯狂,但这似乎是他们真正简化事情的一个案例。您可能想要做的是将它们放入您可以设计的最小方法中,恰当地命名,然后将所有控制代码放入另一个方法中。

例如,如果您编写了一个“抓取数字或字母块”方法,调用者将是一个非常简单、直接的循环,只打印每次调用的结果,并且您调用的方法将是明确定义的,因此即使您对语法一无所知,正则表达式的意图也会很清楚,并且该方法将受到限制,因此人们不太可能随着时间的推移将其搞砸。

这样做的问题是正则表达式工具非常简单并且非常适合这种用途,因此很难证明为此调用方法是正确的。

于 2009-06-04T20:06:37.400 回答
1

由于似乎还没有人发布正确的代码,所以我会试一试。

首先是非正则表达式版本。请注意,我使用 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]
于 2009-06-04T20:30:30.377 回答