13
\$\begingroup\$

Requirement: Parse a String into chunks of numeric characters and alpha characters. Alpha characters should be separated from the numeric, other characters should be ignored.

Example Data:

Input           Desired Output
1A              [1, A]
12              [12]
12G             [12, G]
12ABC-SFS513    [12, ABC, SFS, 513]
AGE+W#FE        [AGE, W, FE]
-12WE-          [12, WE]
-12- &%3WE-     [12, 3, WE]

Question:

The code below accomplishes this. However, I am looking for any suggestions as to a better way to accomplish this (maybe a crazy regex using String.split()? ) or any changes that could make this code more readable/easy to follow.

Code:

private static String VALID_PATTERN = "[0-9]+|[A-Z]+";

private List<String> parse(String toParse){
    List<String> chunks = new LinkedList<String>();
    toParse = toParse + "$"; //Added invalid character to force the last chunk to be chopped off
    int beginIndex = 0;
    int endIndex = 0;
    while(endIndex < toParse.length()){         
        while(toParse.substring(beginIndex, endIndex + 1).matches(VALID_PATTERN)){
            endIndex++;
        }
        if(beginIndex != endIndex){
            chunks.add(toParse.substring(beginIndex, endIndex));    
        } else {
            endIndex++;
        }  
        beginIndex = endIndex;
    }               
    return chunks;
}
\$\endgroup\$

4 Answers 4

15
\$\begingroup\$

First of all, yes there is a crazy regex you can give to String.split:

"[^A-Z0-9]+|(?<=[A-Z])(?=[0-9])|(?<=[0-9])(?=[A-Z])"

What this means is to split on any sequence of characters which aren't digits or capital letters as well as between any occurrence of a capital letter followed by a digit or any digit followed by a capital letter. The trick here is to match the space between a capital letter and a digit (or vice-versa) without consuming the letter or the digit. For this we use look-behind to match the part before the split and look-ahead to match the part after the split.

However as you've probably noticed, the above regex is quite a bit more complicated than your VALID_PATTERN. This is because what you're really doing is trying to extract certain parts from the string, not to split it.


So finding all the parts of the string which match the pattern and putting them in a list is the more natural approach to the problem. This is what your code does, but it does so in a needlessly complicated way. You can greatly simplify your code, by simply using Pattern.matcher like this:

private static final Pattern VALID_PATTERN = Pattern.compile("[0-9]+|[A-Z]+");

private List<String> parse(String toParse) {
    List<String> chunks = new LinkedList<String>();
    Matcher matcher = VALID_PATTERN.matcher(toParse);
    while (matcher.find()) {
        chunks.add( matcher.group() );
    }
    return chunks;
}

If you do something like this more than once, you might want to refactor the body of this method into a method findAll which takes the string and the pattern as arguments, and then call it as findAll(toParse, VALID_PATTERN) in parse.

\$\endgroup\$
1
  • \$\begingroup\$ I had not looked into the Matcher class. This is excellent. \$\endgroup\$
    – jzd
    May 10, 2011 at 14:30
4
\$\begingroup\$

I'm sure this possible with some crazy RegExp and .split(), but I would avoid RegExps for "simple" tasks like these.

One thing I would change, is the check if a "chunk" matches the pattern. For example, when working on the string "AB12", then currently you first check if "A" fits the pattern, and then if "AB" does, but we already know that "A" matches, so it isn't really necessary to do it a again.

Also adding a "invalid" character to the end, seems the wrong way to exit.

My suggestion would be to do it like this (untested):

private int charType(char c) {
  if ('A' <= c && c <= 'Z')
    return 1;
  else if ('0' <= c && c <= '9')
    return 2;
  else
    return 0;
}

private List<String> parse(String toParse){
    List<String> chunks = new LinkedList<String>();
    int length = toParse.length();
    int beginIndex = 0;
    int endIndex = 0;
    int currentType;
    while (endIndex < length) {
        currentType = charType(toParse.charAt(endIndex));
        if (currentType != 0) {
          do {
              endIndex++;
          } while (endIndex < length && currentType == charType(toParse.charAt(endIndex)));
          chunks.add(toParse.substring(beginIndex, endIndex));    
        } else {
            endIndex++;
        }  
        beginIndex = endIndex;
    }               
    return chunks;
}
\$\endgroup\$
4
  • 2
    \$\begingroup\$ Why would you avoid regexps in this case - especially since the OP's regex is so simple (and thus perfectly readable)? 'A' <= c && c <= 'Z' seems in no way more readable to me then [A-Z]. Further using regex allows you to find the matches much more easily than this. \$\endgroup\$
    – sepp2k
    May 10, 2011 at 14:24
  • \$\begingroup\$ Granted, my goal wasn't readability, but speed. A RegExp is most likely much slower than a few simple numerical comparisons. \$\endgroup\$
    – RoToRa
    May 10, 2011 at 14:40
  • \$\begingroup\$ The switch statement in charType isn't valid Java. The case expressions need to be compile time constant expressions. \$\endgroup\$
    – Stephen C
    May 12, 2011 at 2:49
  • \$\begingroup\$ @Stephan C: Duh. Confused it with JavaScript. I'll change it. \$\endgroup\$
    – RoToRa
    May 12, 2011 at 9:40
2
\$\begingroup\$

If you do not care what order it is in, this worked for me

MyString = MyString.replaceAll("[^A-Z ]", "") + " " + MyString.replaceAll("[^0-9 ]", "");
\$\endgroup\$
1
  • \$\begingroup\$ The order is important, but this is interesting for those who might not. \$\endgroup\$
    – jzd
    Oct 9, 2012 at 11:16
1
\$\begingroup\$

You should have a look at Guava, especially at CharMatcher and Splitter. While both "manual" splitting and regexes certainly work, there is no need to make your life more complicated if there is already an easy and safe solution available.

\$\endgroup\$
1
  • \$\begingroup\$ sepp2k answer was nice and simple, plus did not need an external library. If there is better match in Guava I might consider it, but even it was one line of code and super simple, I don't think it is work adding another library. \$\endgroup\$
    – jzd
    May 12, 2011 at 10:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.