4
\$\begingroup\$

Now I have incorporated all the suggestions made by bowmore in the previous and initial iteration of this collection. Also, I have added the Boyer-Moore algorithms, both conventional and naïve.

As of now, the collection includes:

  1. Knuth-Morris-Pratt algorithm,
  2. Finite automaton matcher,
  3. Rabin-Karp algorithm,
  4. Z algorithm.
  5. Boyer-Moore algorithm,
  6. Naïve Boyer-Moore algorithm.

My code follows:

ExactStringMatcher.java

package net.coderodde.string.matching;

/**
 * This interface defines the API for exact string matching algorithms.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Nov 7, 2015)
 */
public interface ExactStringMatcher {

    /**
     * The index returned whenever there is no match.
     */
    public int NOT_FOUND_INDEX = -1;

    /**
     * Attempts to find an occurrence of {@code pattern} in the {@code text} 
     * omitting first {@code startIndex} characters from {@code text}.
     * 
     * @param text       the string in which to search for the pattern.
     * @param pattern    the string to find in the {@code text}.
     * @param startIndex the number of leftmost characters to omit from 
     *                   consideration.
     * @return the smallest index no less than {@code startIndex} where the 
     *         {@code pattern} occurs in {@code text}, or 
     *         {@code NOT_FOUND_INDEX} if there is no match.
     */
    public int match(String text, String pattern, int startIndex);

    /**
     * Attempts to find an occurrence of {@code pattern} in the {@code text} 
     * searching in the entire string {@code text}.
     * 
     * @param text    the string in which to search for the pattern.
     * @param pattern the string to find in the {@code text}.
     * @return the smallest index no less than {@code startIndex} where the 
     *         {@code pattern} occurs in {@code text}, or 
     *         {@code NOT_FOUND_INDEX} if there is no match.
     */
    public default int match(String text, String pattern) {
        return match(text, pattern, 0);
    }
}

ExactStringMatchers.java

package net.coderodde.string.matching;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
 * This class contains different string matching algorithms as static methods.
 *
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Nov 7, 2015)
 */
public final class ExactStringMatchers {

    /**
     * Returns an exact string matcher implementation based on the 
     * <a href="https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm">
     * Knuth-Morris-Pratt algorithm</a>, that runs in time {@code O(n + m)}, 
     * where {@code n} is the length of the string to search in, and {@code m} 
     * is the length of the pattern. The space complexity is {@code m} for 
     * holding a so-called "failure function".
     * 
     * @return an exact string matcher.
     */
    public static ExactStringMatcher getKnuthMorrisPrattMatcher() {
        return new KnuthMorrisPrattMatcher();
    }

    /**
     * Returns an exact string matcher implementation based on 
     * <a href="http://www.geeksforgeeks.org/pattern-searching-set-5-efficient-constructtion-of-finite-automata/">
     * finite automata</a>. Runs in time {@code O(n + ms)}, where {@code n} is
     * the length of the range to search, {@code m} is the length of the pattern
     * to search for, and {@code s} is the number of distinct characters in the
     * pattern. The space complexity is {@code O(ms)}.
     * 
     * @return an exact string matcher.
     */
    public static ExactStringMatcher getFiniteAutomatonMatcher() {
        return new FiniteAutomatonMatcher();
    }

    /**
     * Returns an exact string matcher implementation based on the
     * <a href="https://en.wikipedia.org/wiki/Rabin%E2%80%93Karp_algorithm">
     * Rabin-Karp algorithm</a>. Worst case running time is {@code O(nm)}, where 
     * {@code n} is the length of the range to search in, and {@code m} is the 
     * length of the pattern to search for. The space complexity is constant.
     * 
     * @return an exact string matcher.
     */
    public static ExactStringMatcher getRabinKarpMatcher() {
        return new RabinKarpMatcher();
    }

    /**
     * Returns an exact string matcher implementation based on the 
     * <a href="http://www.geeksforgeeks.org/z-algorithm-linear-time-pattern-searching-algorithm/">
     * Z-algorithm</a>. Runs in time {@code O(n + m)}, where {@code n} is the 
     * length of the range to search in and {@code m} is the length of the 
     * pattern to search for. Space complexity is {@code O(n + m)}.
     * 
     * @return an exact string matcher. 
     */
    public static ExactStringMatcher getZMatcher() {
        return new ZMatcher();
    }

    /**
     * Returns an exact string matcher implementation based on the 
     * <a href="https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string_search_algorithm">
     * Boyer-Moore algorithm</a>. The worst case running time is {@code O(nm)}, 
     * where {@code n} is the length of the text being searched in, and 
     * {@code m} is the length of the pattern being searched for. However, this
     * matcher runs fast in practice. Space complexity is {@code O(m)}.
     * 
     * @return an exact string matcher.
     */
    public static ExactStringMatcher getBoyerMooreMatcher() {
        return new BoyerMooreMatcher(false);
    }

    /**
     * Returns an exact string matcher implementation based on the <b>naïve</b>
     * <a href="https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string_search_algorithm">
     * Boyer-Moore algorithm</a>. The worst case running time is {@code O(nm)}, 
     * where {@code n} is the length of the text being searched in, and 
     * {@code m} is the length of the pattern being searched for. However, this
     * matcher runs fast in practice. Space complexity is {@code O(m)}.
     * 
     * @return an exact string matcher.
     */
    public static ExactStringMatcher getNaiveBoyerMooreMatcher() {
        return new BoyerMooreMatcher(true);
    }

    private static final class KnuthMorrisPrattMatcher 
    implements ExactStringMatcher {

        @Override
        public int match(String text, String pattern, int startIndex) {
            if (pattern.isEmpty()) {
                return 0;
            }

            int n = text.length();
            int m = pattern.length();

            if (m > n) {
                return -1;
            }

            int[] prefixFunction = computePrefixFunction(pattern);
            int q = 0;

            for (int i = Math.max(0, startIndex); i < n; ++i) {
                while (q > 0 && pattern.charAt(q) != text.charAt(i)) {
                    q = prefixFunction[q - 1];
                }

                if (pattern.charAt(q) == text.charAt(i)) {
                    ++q;
                }

                if (q == m) {
                    return i - m + 1;
                }
            }

            return NOT_FOUND_INDEX;
        }

        private static int[] computePrefixFunction(String pattern) {
            int m = pattern.length();
            int[] prefixFunction = new int[m];
            int k = 0;

            for (int q = 1; q < m; ++q) {
                while (k > 0 && pattern.charAt(k) != pattern.charAt(q)) {
                    k = prefixFunction[k - 1];
                }

                if (pattern.charAt(k) == pattern.charAt(q)) {
                    ++k;
                }

                prefixFunction[q] = k;
            }

            return prefixFunction;
        }
    }

    private static final class FiniteAutomatonMatcher 
    implements ExactStringMatcher {

        @Override
        public int match(String text, String pattern, int startIndex) {
            if (pattern.isEmpty()) {
                return 0;
            }

            int n = text.length();
            Integer m = pattern.length();

            if (m > n) {
                return NOT_FOUND_INDEX;
            }

            TransitionFunction transitionFunction = 
                    computeTransitionFunction(pattern);

            Integer j = 0;

            for (int i = Math.max(0, startIndex); i < n; ++i) {
                if (j == null) {
                    j = 0;
                }

                j = transitionFunction.getState(j, text.charAt(i));

                if (m.equals(j)) {
                    return i - m + 1;
                }
            }

            return NOT_FOUND_INDEX;
        }


        private static TransitionFunction 
        computeTransitionFunction(String pattern) {
            return new TransitionFunction(pattern);
        }

        private static final class TransitionFunction {

            private final Map<Character, Integer>[] function;

            TransitionFunction(String pattern) {
                int m = pattern.length();
                this.function = new HashMap[m + 1];

                Set<Character> filter = new HashSet(m);

                for (Character c : pattern.toCharArray()) {
                    filter.add(c);
                }

                int numberOfCharacters = filter.size();
                Character[] characterArray = new Character[numberOfCharacters];
                filter.toArray(characterArray);

                for (int i = 0; i < function.length; ++i) {
                    function[i] = new HashMap<>(numberOfCharacters);
                }

                for (int i = 0; i < numberOfCharacters; ++i) {
                    function[0].put(characterArray[i], 0);
                }

                function[0].put(pattern.charAt(0), 1);

                for (int i = 1, lps = 0; i <= m; ++i) {
                    for (int x = 0; x < numberOfCharacters; ++x) {
                        function[i].put(characterArray[x], 
                                        function[lps].get(characterArray[x]));
                    }


                    if (i < m) {
                        function[i].put(pattern.charAt(i), i + 1);
                        lps = function[lps].get(pattern.charAt(i));
                    }
                }
            }

            Integer getState(int currentState, char character) {
                return function[currentState].get(character);
            }

            @Override
            public String toString() {
                StringBuilder sb = new StringBuilder();
                Set<Character> alphabet = new TreeSet<>(function[0].keySet());
                Character[] array = new Character[alphabet.size()];
                alphabet.toArray(array);

                for (Map<Character, Integer> map : function) {
                    for (Character c : array) {
                        sb.append(map.get(c)).append(' ');
                    }

                    sb.append('\n');
                }

                return sb.toString();
            }
        }
    }

    private static final class RabinKarpMatcher implements ExactStringMatcher {

        @Override
        public int match(String text, String pattern, int startIndex) {
            int m = pattern.length();

            if (m == 0) {
                return 0;
            }

            int n = text.length();
            startIndex = Math.max(0, startIndex);

            if (m + startIndex > n) {
                return NOT_FOUND_INDEX;
            }

            Set<Character> alphabet = new HashSet<>();

            for (char c : pattern.toCharArray()) {
                alphabet.add(c);
            }

            long d = alphabet.size();
            long q = 2;
            long h = intpow(d, m - 1) % q;

            long p = 0;
            long t = 0;

            // Beginning of preprocessing.
            for (int i = 0; i < m; ++i) {
                p = (d * p + (long)(pattern.charAt(i))) % q;
                t = (d * t + (long)(text.charAt(i + startIndex))) % q;
            }
            // End of preprocessing.

            // Beginning of matching.
            for (int s = startIndex; s <= n - m;  ++s) {
                if (p == t) {
                    if (hasMatch(pattern, text, s)) {
                        return s;
                    }
                }

                if (s < n - m) {
                    long save_t = t;
                    t = (d * (save_t - (long)(text.charAt(s)) * h) + 
                         (long)(text.charAt(s + m))) % q;

                    if (t < 0) {
                        t = (t + q);
                    }
                }
            }

            return NOT_FOUND_INDEX;
        }

        private static boolean hasMatch(String pattern, 
                                        String text, 
                                        int shift) {
            int m = pattern.length();

            for (int i = 0; i < m; ++i) {
                if (pattern.charAt(i) != text.charAt(i + shift)) {
                    return false;
                }
            }

            return true;
        }

        private static long intpow(long base, int exponent) {
            long ret = 1;

            for (int i = 0; i < exponent; ++i) {
                ret *= base;
            }

            return ret;
        }
    }


    private static final class ZMatcher implements ExactStringMatcher {

        @Override
        public int match(String text, String pattern, int startIndex) {
            if (pattern.isEmpty()) {
                return 0;
            }

            int n = text.length();
            int m = pattern.length();

            if (m > n) {
                return -1;
            }

            startIndex = Math.max(0, startIndex);

            if (startIndex != 0) {
                text = text.substring(startIndex);
            }

            StringBuilder sb = new StringBuilder(text.length() + 
                                                 pattern.length() + 1 -
                                                 startIndex);

            sb.append(pattern).append(Character.valueOf('\0')).append(text);
            // Do not create a new string from the StringBuilder, but rather
            // use the builder to access the data.
            int[] zArray = computeZArray(sb);

            for (int i = Math.max(0, startIndex); i < zArray.length; ++i) {
                if (zArray[i] == m) {
                    return i - m - 1 + startIndex;
                }
            }

            return NOT_FOUND_INDEX;
        }

        private static int[] computeZArray(StringBuilder sb) {
            int n = sb.length();
            int[] ret = new int[n];

            int l = 0;
            int r = 0;

            for (int i = 1; i < n; ++i) {
                if (i > r) {
                    l = i;
                    r = i;

                    while (r < n && sb.charAt(r - l) == sb.charAt(r)) {
                        ++r;
                    }

                    ret[i] = r - l;
                    --r;
                } else {
                    int k = i - l;

                    if (ret[k] < r - i + 1) {
                        ret[i] = ret[k];
                    } else {
                        l = i;

                        while (r < n && sb.charAt(r - l) == sb.charAt(r)) {
                            ++r;
                        }

                        ret[i] = r - l;
                        --r;
                    }
                }
            }

            return ret;
        }
    }

    private static final class BoyerMooreMatcher implements ExactStringMatcher {

        private final boolean naive;

        BoyerMooreMatcher(boolean naive) {
            this.naive = naive;
        }

        @Override
        public int match(String text, String pattern, int startIndex) {
            if (pattern.isEmpty()) {
                return 0;
            }

            int n = text.length();
            int m = pattern.length();

            if (m > n) {
                return -1;
            }

            startIndex = Math.max(0, startIndex);

            if (naive) {
                return matchNaiveImpl(text, pattern, startIndex);
            } else {
                return matchImpl(text, pattern, startIndex);
            }
        } 

        private int matchImpl(String text, String pattern, int startIndex) {
            int n = text.length();
            int m = pattern.length();

            Map<Character, Integer> charTable = createCharTable(pattern);
            int offsetTable[] = createOffsetTable(pattern);

            for (int i = m - 1 + startIndex, j; i < n;) {
                for (j = m - 1; pattern.charAt(j) == text.charAt(i); --i, --j) {
                    if (j == 0) {
                        return i;
                    }
                }

                i += Math.max(offsetTable[m - j - 1], 
                              charTable.getOrDefault(text.charAt(i), m));
            }

            return NOT_FOUND_INDEX;
        }

        private int matchNaiveImpl(String text, 
                                   String pattern, 
                                   int startIndex) {
            int n = text.length();
            int m = pattern.length();

            for (int i = m - 1 + startIndex, j; i < n;) {
                for (j = m - 1; pattern.charAt(j) == text.charAt(i); --i, --j) {
                    if (j == 0) {
                        return i;
                    }
                }

                i += m - j; 
            }

            return NOT_FOUND_INDEX;
        }

        private static Map<Character, Integer> createCharTable(String pattern) {
            int m = pattern.length();
            Map<Character, Integer> table = new HashMap<>(m);

            for (char c : pattern.toCharArray()) {
                table.put(c, m);
            }

            for (int i = 0; i < m - 1; ++i) {
                table.put(pattern.charAt(i), m - 1 - i);
            }

            return table;
        }

        private static int[] createOffsetTable(String pattern) {
            int m = pattern.length();
            int[] table = new int[m];
            int lastPrefixPosition = m;

            for (int i = m - 1; i >= 0; --i) {
                if (isPrefix(pattern, i + 1)) {
                    lastPrefixPosition = i + 1;
                }

                table[m - 1 - i] = lastPrefixPosition - i + m - 1;
            }

            for (int i = 0; i < m - 1; ++i) {
                int suffixLength = suffixLength(pattern, i);
                table[suffixLength] = m - 1 - i + suffixLength;
            }

            return table;
        }

        private static boolean isPrefix(String pattern, int p) {
            int m = pattern.length();

            for (int i = p, j = 0; i < m; ++i, ++j) {
                if (pattern.charAt(i) != pattern.charAt(j)) {
                    return false;
                }
            }

            return true;
        }

        private static int suffixLength(String pattern, int p) {
            int length = 0;
            int m = pattern.length();

            for (int i = p, j = m - 1; 
                    i >= 0 && pattern.charAt(i) == pattern.charAt(j); 
                    --i, --j) {
                ++length;
            }

            return length;
        }
    }
}

PerformanceDemo.java

import java.util.Random;
import net.coderodde.string.matching.ExactStringMatcher;
import net.coderodde.string.matching.ExactStringMatchers;

public class PerformanceDemo {

    public static void main(final String... args) {
        int N = 5_000_000;

        String text = getWorstCaseText(N);

        System.out.println("[WORST CASE OF String.indexOf]");

        for (int i = 3000; i > 0; i -= 500) {
            System.out.println();
            System.out.println("[Pattern length: " + i + "]");
            String pattern = getWorstCaseText(i);
            demo(text, pattern);
        }

        long seed = System.currentTimeMillis();
        Random random = new Random(seed);
        text = getRandomText(random);
        String pattern = getRandomPattern(random);

        System.out.println();
        System.out.println("[RANDOM STRINGS]");
        System.out.println("[SEED: " + seed + "]");

        demo(text, pattern);
    }

    private static String getWorstCaseText(int length) {
        StringBuilder sb = new StringBuilder(length);

        for (int i = 0; i < length - 1; ++i) {
            sb.append('a');
        }

        return sb.append('b').toString();
    }

    private static String getRandomText(Random random) {
        return getRandomString(10_000_000, random);
    }

    private static String getRandomPattern(Random random) {
        return getRandomString(200, random);
    }

    private static String getRandomString(int size, Random random) {
        StringBuilder sb = new StringBuilder(size);

        for (int i = 0; i < size; ++i) {
            sb.append((char)('a' + random.nextInt(26)));
        }

        return sb.toString();
    }

    private static void profile(ExactStringMatcher matcher,
                                String text,
                                String pattern,
                                int expectedIndex) {
        long startTime = System.nanoTime();
        int index = matcher.match(text, pattern);
        long endTime = System.nanoTime();

        if (index != expectedIndex) {
            throw new IllegalStateException(
                    matcher.getClass() + " failed. Returned: " + index
                    + ", expected: " + expectedIndex);
        }

        System.out.printf("%s in %.3f milliseconds.\n", 
                          matcher.getClass().getSimpleName(),
                          (endTime - startTime) / 1e6);
    }

    private static void demo(String text, String pattern) {
        long startTime = System.nanoTime();
        int expectedIndex = text.indexOf(pattern);
        long endTime = System.nanoTime();

        System.out.printf("String.indexOf in %.3f millisecons.\n", 
                          (endTime - startTime) / 1e6);

        profile(ExactStringMatchers.getKnuthMorrisPrattMatcher(),
                text,
                pattern,
                expectedIndex);

        profile(ExactStringMatchers.getFiniteAutomatonMatcher(),
                text,
                pattern,
                expectedIndex);

        profile(ExactStringMatchers.getRabinKarpMatcher(),
                text,
                pattern,
                expectedIndex);

        profile(ExactStringMatchers.getZMatcher(),
                text,
                pattern,
                expectedIndex);

        profile(ExactStringMatchers.getBoyerMooreMatcher(),
                text,
                pattern,
                expectedIndex);

        System.out.print("Naïve ");
        profile(ExactStringMatchers.getNaiveBoyerMooreMatcher(),
                text,
                pattern,
                expectedIndex);
    }
}

ExactStringMatchersTest.java

package net.coderodde.string.matching;

import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import static org.junit.Assert.*;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

@RunWith(Parameterized.class)
public class ExactStringMatchersTest {

    @Parameterized.Parameters
    public static List<Object[]> getParameters() {
        return Arrays.asList(
                new Object[]{ ExactStringMatchers.getKnuthMorrisPrattMatcher() },
                new Object[]{ ExactStringMatchers.getFiniteAutomatonMatcher() },
                new Object[]{ ExactStringMatchers.getRabinKarpMatcher() },
                new Object[]{ ExactStringMatchers.getZMatcher() },
                new Object[]{ ExactStringMatchers.getBoyerMooreMatcher() },
                new Object[]{ ExactStringMatchers.getNaiveBoyerMooreMatcher() }
        );
    }

    private final ExactStringMatcher matcher;

    public ExactStringMatchersTest(ExactStringMatcher matcher) {
        this.matcher = matcher;
    }

    @Test
    public void testMatcher() {
        assertEquals(0,  matcher.match("acacaga", "acacaga"));
        assertEquals(-1, matcher.match("aaa", "aaaa"));
        assertEquals(0,  matcher.match("aaaa", "aaaa"));
        assertEquals(-1, matcher.match("aaaa", "bb"));
        assertEquals(1,  matcher.match("abbb", "bb"));
        assertEquals(2,  matcher.match("abcc", "cc"));

        assertEquals(5, matcher.match("aaaaaaab", "aab"));
        assertEquals(4, matcher.match("ababababaca", "ababaca"));

        assertTrue("".indexOf("")  == matcher.match("", ""));
        assertTrue("".indexOf("a") == matcher.match("", "a"));
        assertTrue("a".indexOf("") == matcher.match("a", ""));
        assertTrue("hello".indexOf("ello", -2) == 
                matcher.match("hello", "ello", -2));

        assertEquals(-1, matcher.match("aabaab", "aab", 5));
        assertEquals(-1, matcher.match("aabaab", "aab", 4));
        assertEquals(3,  matcher.match("aabaab", "aab", 3));
        assertEquals(3,  matcher.match("aabaab", "aab", 2));
        assertEquals(3,  matcher.match("aabaab", "aab", 1));
        assertEquals(0,  matcher.match("aabaab", "aab", 0));
        assertEquals(0,  matcher.match("aabaab", "aab", -1));
        assertEquals(0,  matcher.match("aabaab", "aab", -2));
        assertEquals(6,  matcher.match("aaaaaaaab", "aab"));
    }
}

The performance figures are:

[WORST CASE OF String.indexOf]

[Pattern length: 3000]
String.indexOf in 11838.984 millisecons.
KnuthMorrisPrattMatcher in 84.294 milliseconds.
FiniteAutomatonMatcher in 126.147 milliseconds.
RabinKarpMatcher in 131.707 milliseconds.
ZMatcher in 190.653 milliseconds.
BoyerMooreMatcher in 196.471 milliseconds.
Naïve BoyerMooreMatcher in 24.561 milliseconds.

[Pattern length: 2500]
String.indexOf in 10402.658 millisecons.
KnuthMorrisPrattMatcher in 61.768 milliseconds.
FiniteAutomatonMatcher in 83.877 milliseconds.
RabinKarpMatcher in 116.658 milliseconds.
ZMatcher in 129.418 milliseconds.
BoyerMooreMatcher in 178.185 milliseconds.
Naïve BoyerMooreMatcher in 34.950 milliseconds.

.
.
.
[Pattern length: 500]
String.indexOf in 1945.018 millisecons.
KnuthMorrisPrattMatcher in 52.945 milliseconds.
FiniteAutomatonMatcher in 58.797 milliseconds.
RabinKarpMatcher in 37.688 milliseconds.
ZMatcher in 85.749 milliseconds.
BoyerMooreMatcher in 119.606 milliseconds.
Naïve BoyerMooreMatcher in 14.253 milliseconds.

[RANDOM STRINGS]
[SEED: 1446979474147]
String.indexOf in 15.881 millisecons.
KnuthMorrisPrattMatcher in 60.133 milliseconds.
FiniteAutomatonMatcher in 124.986 milliseconds.
RabinKarpMatcher in 184.710 milliseconds.
ZMatcher in 134.121 milliseconds.
BoyerMooreMatcher in 9.660 milliseconds.
Naïve BoyerMooreMatcher in 35.604 milliseconds.

Is there anything else I could do in order to funkify my code?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Nice work! How about a comparison results for larger string ? Performance impact \$\endgroup\$ Commented Nov 8, 2015 at 15:03
  • \$\begingroup\$ Larger haystack string? \$\endgroup\$
    – coderodde
    Commented Nov 8, 2015 at 15:36

1 Answer 1

2
\$\begingroup\$

It looks nice. Some comments from me:

Correctness

The following unit test will fail for all matchers:

@Test
public void empty_pattern_with_startIndex_greater_than_0_should_return_startIndex() throws Exception {
    assertEquals(1, matcher.match("abc", "", 1));
}

So either your code or the Javadoc of the match method is wrong.

Unit tests as specification

Unit tests can be the specification and documentation of the code if they are written well. For example, I was wondering what a negative startIndex does, since that was not mentioned in the Javadoc. By looking at the tests I could figure it out. I also was able to see from the tests, that the behaviour of your matchers is supposed to be exactly like the indexOf method. That was nice.

To reinforce this specification and documentation aspect, you should make a test method with a descriptive name for each "feature" of your code. See my example above. Don't put more than a few asserts in a test method. (This was already mentioned by somebody in the last iteration.)

It also wouldn't harm to create tests for some more boundary and special cases. For example Strings with characters outside the ASCII set. Or Integer.MAX_VALUE for startIndex.

You could use the Hamcrest matcher framework for even better readability of your asserts. Like this:

assertThat(matcher.match("abcc", "cc"), is(2));
\$\endgroup\$

Your Answer

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

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