Статьи

Java-реализация String # next () Преемник

Я нашел Ruby’s String#next()или#succ очень полезный и продуктивный, особенно при создании данных для тестирования. Вот что говорит Ruby Doc:

succ -> new_str

следующий -> new_str

Возвращает преемника на ул. Преемник вычисляется путем приращения символов, начиная с самого правого буквенно-цифрового (или самого правого символа>, если буквенно-цифровых символов нет) в строке. Увеличение цифры всегда приводит к другой цифре, а увеличение буквы — к другой букве> того же случая. Увеличение неалфавитно-цифровых символов использует последовательность сортировки основного набора символов.

Если приращение генерирует «перенос», то символ слева от него увеличивается. Этот процесс повторяется до тех пор, пока нет переноса, добавляя дополнительный символ>, если необходимо.

"abcd".succ        #=> "abce"
"THX1138".succ     #=> "THX1139"
"<<koala>>".succ   #=> "<<koalb>>"
"1999zzz".succ     #=> "2000aaa"
"ZZZ9999".succ     #=> "AAAA0000"
"***".succ         #=> "**+"

Поэтому, когда я увидел, что Groovy на самом деле предоставил метод расширения String#next() , я был счастлив попробовать его. Но потом я быстро разочаровался, когда поведение совсем другое. Версия Groovy очень проста и на самом деле не очень продуктивна, поскольку она просто циклически перебирает диапазон набора символов (в том числе непечатные символы вслепую!). Версия Ruby, однако, намного более производительна, поскольку она производит видимые символы. Например:

bash> ruby -e 'puts "Z".next()'
AA
bash> groovy -e 'println("Z".next())'
[

Хотелось бы, чтобы в будущем версия на Groovy улучшилась, так как в данный момент она не очень полезна. Ради интереса я написал версию реализации Java, которая имитирует поведение Ruby:

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)));
                }
            }
        }
    }
}

А вот мой юнит-тест для проверки работоспособности:

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