Java Implementation of String#next() Successor
Join the DZone community and get the full member experience.
Join For FreeI've found the Ruby's String#next()
or #succ
very useful and productive, specially when generating data for testing. Here is what the Ruby doc says:
succ -> new_str
next -> new_str
Returns the successor to str. The successor is calculated by incrementing characters starting from the rightmost alphanumeric (or the rightmost character > if there are no alphanumerics) in the string. Incrementing a digit always results in another digit, and incrementing a letter results in another letter > of the same case. Incrementing nonalphanumerics uses the underlying character set’s collating sequence.
If the increment generates a “carry,” the character to the left of it is incremented. This process repeats until there is no carry, adding an additional > character if necessary.
"abcd".succ #=> "abce" "THX1138".succ #=> "THX1139" "<<koala>>".succ #=> "<<koalb>>" "1999zzz".succ #=> "2000aaa" "ZZZ9999".succ #=> "AAAA0000" "***".succ #=> "**+"
So when I saw Groovy actually has provided a String extension #next()
method, I was happy to try it out. But then I was quickly disappointed
when the behavior is very different. The Groovy version is very simple
and actually not very productive since it simply loop through Character set range in incrementally
(including non-printable characters blindly!). The Ruby's version,
however, is much more productive since it produce visible characters.
For examples:
bash> ruby -e 'puts "Z".next()'
AA
bash> groovy -e 'println("Z".next())'
[
I wish Groovy version would improve in future as it's not very useful at the moment. Just for fun, I wrote a Java implementation version that mimics the Ruby's behavior:
package deng.jdk; /** * Utilities method for manipulating String. * @author zemian 1/1/13 */ public class StringUtils { /** Calculate string successor value. Similar to Ruby's String#next() method. */ public static String next(String text) { // We will not process empty string int len = text.length(); if (len == 0) return text; // Determine where does the first alpha-numeric starts. boolean alphaNum = false; int alphaNumPos = -1; for (char c : text.toCharArray()) { alphaNumPos++; if (Character.isDigit(c) || Character.isLetter(c)) { alphaNum = true; break; } } // Now we go calculate the next successor char of the given text. StringBuilder buf = new StringBuilder(text); if (!alphaNum || alphaNumPos == 0 || alphaNumPos == len) { // do the entire input text next(buf, buf.length() - 1, alphaNum); } else { // Strip the input text for non alpha numeric prefix. We do not need to process these prefix but to save and // re-attach it later after the result. String prefix = text.substring(0, alphaNumPos); buf = new StringBuilder(text.substring(alphaNumPos)); next(buf, buf.length() - 1, alphaNum); buf.insert(0, prefix); } // We are done. return buf.toString(); } /** Internal method to calculate string successor value on alpha numeric chars only. */ private static void next(StringBuilder buf, int pos, boolean alphaNum) { // We are asked to carry over next value for the left most char if (pos == -1) { char c = buf.charAt(0); String rep = null; if (Character.isDigit(c)) rep = "1"; else if (Character.isLowerCase(c)) rep = "a"; else if (Character.isUpperCase(c)) rep = "A"; else rep = Character.toString((char) (c + 1)); buf.insert(0, rep); return; } // We are asked to calculate next successor char for index of pos. char c = buf.charAt(pos); if (Character.isDigit(c)) { if (c == '9') { buf.replace(pos, pos + 1, "0"); next(buf, pos - 1, alphaNum); } else { buf.replace(pos, pos + 1, Character.toString((char)(c + 1))); } } else if (Character.isLowerCase(c)) { if (c == 'z') { buf.replace(pos, pos + 1, "a"); next(buf, pos - 1, alphaNum); } else { buf.replace(pos, pos + 1, Character.toString((char)(c + 1))); } } else if (Character.isUpperCase(c)) { if (c == 'Z') { buf.replace(pos, pos + 1, "A"); next(buf, pos - 1, alphaNum); } else { buf.replace(pos, pos + 1, Character.toString((char)(c + 1))); } } else { // If input text has any alpha num at all then we are to calc next these characters only and ignore the // we will do this by recursively call into next char in buf. if (alphaNum) { next(buf, pos - 1, alphaNum); } else { // However if the entire input text is non alpha numeric, then we will calc successor by simply // increment to the next char in range (including non-printable char!) if (c == Character.MAX_VALUE) { buf.replace(pos, pos + 1, Character.toString(Character.MIN_VALUE)); next(buf, pos - 1, alphaNum); } else { buf.replace(pos, pos + 1, Character.toString((char)(c + 1))); } } } } }
And here is my unit test for sanity check:
package deng.jdk; import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Test; import java.lang.Exception; public class StringUtilsTest { @Test public void testNext() throws Exception { Assert.assertThat(StringUtils.next("abcd"), Matchers.is("abce")); Assert.assertThat(StringUtils.next("THX1138"), Matchers.is("THX1139")); Assert.assertThat(StringUtils.next("<<koala>>"), Matchers.is("<<koalb>>")); Assert.assertThat(StringUtils.next("1999zzz"), Matchers.is("2000aaa")); Assert.assertThat(StringUtils.next("ZZZ9999"), Matchers.is("AAAA0000")); Assert.assertThat(StringUtils.next("***"), Matchers.is("**+")); // Test next continually String s = "00"; for (int i = 0; i < 10 * 10; i++) s = StringUtils.next(s); Assert.assertThat(s, Matchers.is("100")); s = "AA"; for (int i = 0; i < 26 * 26; i++) s = StringUtils.next(s); Assert.assertThat(s, Matchers.is("AAA")); s = "AA00"; for (int i = 0; i < 26 * 26 * 10 * 10; i++) s = StringUtils.next(s); Assert.assertThat(s, Matchers.is("AAA00")); // Test some corner cases Assert.assertThat(StringUtils.next(""), Matchers.is("")); Assert.assertThat(StringUtils.next(" "), Matchers.is("!")); Assert.assertThat(StringUtils.next("#"), Matchers.is("$")); Assert.assertThat(StringUtils.next("Z"), Matchers.is("AA")); Assert.assertThat(StringUtils.next("#Z"), Matchers.is("#AA")); Assert.assertThat(StringUtils.next("#Z#"), Matchers.is("#AA#")); Assert.assertThat(StringUtils.next("999"), Matchers.is("1000")); } }
Published at DZone with permission of Zemian Deng, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
8 Data Anonymization Techniques to Safeguard User PII Data
-
What Is mTLS? How To Implement It With Istio
-
Write a Smart Contract With ChatGPT, MetaMask, Infura, and Truffle
-
AI Technology Is Drastically Disrupting the Background Screening Industry
Comments