My name is Zemian Deng, and I am a Senior Application Engineer working at Oracle for the Enterprise Knowledge Management product. NOTE: The views expressed on my blog and social network are my own and do not necessarily reflect the views of my employer. Zemian is a DZone MVB and is not an employee of DZone and has posted 83 posts at DZone. You can read more from them at their website. View Full User Profile

Java Implementation of String#next() Successor

01.05.2013
| 3129 views |
  • submit to reddit

I'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, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)