This has presented an interesting problem. Your solution concerns me, because it does not solve things the way I would. This, in itself, is not a problem, but I had to look through your code, and figure out why you were doing things. Your solution has a couple of unexpected, and useful side effects.
Your method is defined to split a word in to the tokens that are CamelCase or Underscore separated, but it actually splits on any other punctuation, including space (anything that is not Character.isLetterOrDigit(..)
, so your solution gets 'hello' and 'world' from '!hello! .... world !!!'
Your solution also splits some complex CamelCase examples well, like thisIsHTMLInCamelCase
, which surprised me.
So, I set out to suggest why your solution was a problem, but then found that it worked better than I thought.
This intrigued me, so I set about understanding your solution, and, bottom line, you do text-break-processing on punctuation and before-consecutive capitals....
Essentially, you split the word with spaces between all sequences of characters, and then you also add a space before the capital of a Xx combination, and before the capital of a xX combination (or both).
The process you are using, though, is really ugly. Regular expressions are a better tool for the job. They may be hard to understand, occasionally, but they 'compile' down to what are called Deterministic Finite Automaton.
So, your code does a lot more than the description, since it can deal with space and punctuation separated words (i.e. sentences), but it does it in a way that is slow, and requires intermediate results (a space separated string), which is then post-processed to words.
I took this system, and implemented it as regular expressions as an exercise myself:
First, I compiled two patterns, one to split the content in to words, the other to split the words in CamelCase, etc.
private static final Pattern PUNCTSPACE = Pattern.compile("[ \\p{Punct}]+");
private static final Pattern TRANSITION = Pattern.compile("(?<=[^\\p{Lu}])(?=[\\p{Lu}])|(?=[\\p{Lu}][^\\p{Lu}])");
Note that the PUNCTSPACE
pattern includes the underscore.
The second pattern TRANSITION
contains two zero-length lookahead/lookbehind alternatives. With CamelCase, you split before the first Capital after a lower letter, and you also split before the capital before a lower-case letter (or end-of-word). Consider CamelHTMLCase
where we split before the H, and before the C of Case. These are marked by Transitions, one is from lower-to-upper, the other is from upper-to-lower. In each case, we split before the upper.
Note, that \p{Lu}
is a regular expression identifier for all upper-case Unicode letters. The \
needs to be escaped in the String constant, so in the constant, you will see two patterns: [\\p{Lu}]
and [^\\p{Lu}]
The first represents any upper-case character, the second represents any non-upper-case character (including digits, and other punctuation).
So, there are two parts to the TRANSITION pattern:
(?<=[^\\p{Lu}])(?=[\\p{Lu}])
- This first one takes the given spot between two characters, and if the char before the spot is not upper, and the char after the spot is upper, then it splits at that spot.
(?=[\\p{Lu}][^\\p{Lu}])
- this pattern takes a spot before a character, and, if the character is upper, and after that character is a lower, then split at the spot.
Now, I am not suggesting this is easy, but, it works, and it will work fast.
When splitting the original sentence, it is possible for there to be some leading space, punctuation, or other junk. This may lead to having an initial empty string in the word-split, so we need to ignore empty words.
Putting this together, you get:
private static final Pattern PUNCTSPACE = Pattern.compile("[ \\p{Punct}]+");
private static final Pattern TRANSITION = Pattern.compile("(?<=[^\\p{Lu}])(?=[\\p{Lu}])|(?=[\\p{Lu}][^\\p{Lu}])");
public static final List<String> deHump(String text) {
List<String> result = new ArrayList<String>();
for (String word : PUNCTSPACE.split(text)) {
if (word.isEmpty()) {
continue;
}
for (String part : TRANSITION.split(word)) {
result.add(part);
}
}
return result;
}
Now, this is great, it will populate a list, but, in the world of Java8, with Lambda expressions, it is 'fun' to do it the streaming way too:
public static final List<String> deHumpLambda(String text) {
return Arrays.stream(PUNCTSPACE.split(text))
.filter(word -> !word.isEmpty())
.flatMap(word -> Arrays.asList(TRANSITION.split(word)).stream())
.collect(Collectors.toList());
}
The above splits the input in to words based on punctuation and the _ character, then it takes each non-empty word, and splits it again at CamelCase transitions. It accumulates the results in a List, and returns that.
StringTokenizer
JavaDoc - StringTokenizer is a legacy class that is retained for compatibility reasons although its use is discouraged in new code. Don't use it. \$\endgroup\$abc123_456xyz
? Your code gives[abc, 1, 2, 3, 4, 5, 6xyz]
, just want to confirm that's what is intended. Same question forISTHISLEGALPascalCase
. \$\endgroup\$