Strings.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.lang3;

import static org.apache.commons.lang3.StringUtils.INDEX_NOT_FOUND;

import org.apache.commons.lang3.builder.AbstractSupplier;
import org.apache.commons.lang3.function.ToBooleanBiFunction;

/**
 * String operations where you choose case-sensitive {@link #CS} vs. case-insensitive {@link #CI} through a singleton instance.
 *
 * @see CharSequenceUtils
 * @see StringUtils
 * @since 3.18.0
 */
public abstract class Strings {

    /**
     * Builds {@link Strings} instances.
     */
    public static class Builder extends AbstractSupplier<Strings, Builder, RuntimeException> {

        /**
         * Ignores case when possible.
         */
        private boolean ignoreCase;

        /**
         * Compares null as less when possible.
         */
        private boolean nullIsLess;

        /**
         * Constructs a new instance.
         */
        private Builder() {
            // empty
        }

        /**
         * Gets a new {@link Strings} instance.
         */
        @Override
        public Strings get() {
            return ignoreCase ? new CiStrings(nullIsLess) : new CsStrings(nullIsLess);
        }

        /**
         * Sets the ignoreCase property for new Strings instances.
         *
         * @param ignoreCase the ignoreCase property for new Strings instances.
         * @return {@code this} instance.
         */
        public Builder setIgnoreCase(final boolean ignoreCase) {
            this.ignoreCase = ignoreCase;
            return asThis();
        }

        /**
         * Sets the nullIsLess property for new Strings instances.
         *
         * @param nullIsLess the nullIsLess property for new Strings instances.
         * @return {@code this} instance.
         */
        public Builder setNullIsLess(final boolean nullIsLess) {
            this.nullIsLess = nullIsLess;
            return asThis();
        }

    }

    /**
     * Case-insensitive extension.
     */
    private static final class CiStrings extends Strings {

        private CiStrings(final boolean nullIsLess) {
            super(true, nullIsLess);
        }

        @Override
        public int compare(final String s1, final String s2) {
            if (s1 == s2) {
                // Both null or same object
                return 0;
            }
            if (s1 == null) {
                return isNullIsLess() ? -1 : 1;
            }
            if (s2 == null) {
                return isNullIsLess() ? 1 : -1;
            }
            return s1.compareToIgnoreCase(s2);
        }

        @Override
        public boolean contains(final CharSequence str, final CharSequence searchStr) {
            if (str == null || searchStr == null) {
                return false;
            }
            final int len = searchStr.length();
            final int max = str.length() - len;
            for (int i = 0; i <= max; i++) {
                if (CharSequenceUtils.regionMatches(str, true, i, searchStr, 0, len)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean equals(final CharSequence cs1, final CharSequence cs2) {
            if (cs1 == cs2) {
                return true;
            }
            if (cs1 == null || cs2 == null) {
                return false;
            }
            if (cs1.length() != cs2.length()) {
                return false;
            }
            return CharSequenceUtils.regionMatches(cs1, true, 0, cs2, 0, cs1.length());
        }

        @Override
        public boolean equals(final String s1, final String s2) {
            return s1 == null ? s2 == null : s1.equalsIgnoreCase(s2);
        }

        @Override
        public int indexOf(final CharSequence str, final CharSequence searchStr, int startPos) {
            if (str == null || searchStr == null) {
                return INDEX_NOT_FOUND;
            }
            if (startPos < 0) {
                startPos = 0;
            }
            final int endLimit = str.length() - searchStr.length() + 1;
            if (startPos > endLimit) {
                return INDEX_NOT_FOUND;
            }
            if (searchStr.length() == 0) {
                return startPos;
            }
            for (int i = startPos; i < endLimit; i++) {
                if (CharSequenceUtils.regionMatches(str, true, i, searchStr, 0, searchStr.length())) {
                    return i;
                }
            }
            return INDEX_NOT_FOUND;
        }

        @Override
        public int lastIndexOf(final CharSequence str, final CharSequence searchStr, int startPos) {
            if (str == null || searchStr == null) {
                return INDEX_NOT_FOUND;
            }
            final int searchStrLength = searchStr.length();
            final int strLength = str.length();
            if (startPos > strLength - searchStrLength) {
                startPos = strLength - searchStrLength;
            }
            if (startPos < 0) {
                return INDEX_NOT_FOUND;
            }
            if (searchStrLength == 0) {
                return startPos;
            }
            for (int i = startPos; i >= 0; i--) {
                if (CharSequenceUtils.regionMatches(str, true, i, searchStr, 0, searchStrLength)) {
                    return i;
                }
            }
            return INDEX_NOT_FOUND;
        }

    }

    /**
     * Case-sentive extension.
     */
    private static final class CsStrings extends Strings {

        private CsStrings(final boolean nullIsLess) {
            super(false, nullIsLess);
        }

        @Override
        public int compare(final String s1, final String s2) {
            if (s1 == s2) {
                // Both null or same object
                return 0;
            }
            if (s1 == null) {
                return isNullIsLess() ? -1 : 1;
            }
            if (s2 == null) {
                return isNullIsLess() ? 1 : -1;
            }
            return s1.compareTo(s2);
        }

        @Override
        public boolean contains(final CharSequence seq, final CharSequence searchSeq) {
            return CharSequenceUtils.indexOf(seq, searchSeq, 0) >= 0;
        }

        @Override
        public boolean equals(final CharSequence cs1, final CharSequence cs2) {
            if (cs1 == cs2) {
                return true;
            }
            if (cs1 == null || cs2 == null) {
                return false;
            }
            if (cs1.length() != cs2.length()) {
                return false;
            }
            if (cs1 instanceof String && cs2 instanceof String) {
                return cs1.equals(cs2);
            }
            // Step-wise comparison
            final int length = cs1.length();
            for (int i = 0; i < length; i++) {
                if (cs1.charAt(i) != cs2.charAt(i)) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public boolean equals(final String s1, final String s2) {
            return eq(s1, s2);
        }

        @Override
        public int indexOf(final CharSequence seq, final CharSequence searchSeq, final int startPos) {
            return CharSequenceUtils.indexOf(seq, searchSeq, startPos);
        }

        @Override
        public int lastIndexOf(final CharSequence seq, final CharSequence searchSeq, final int startPos) {
            return CharSequenceUtils.lastIndexOf(seq, searchSeq, startPos);
        }

    }

    /**
     * The <strong>C</strong>ase-<strong>I</strong>nsensitive singleton instance.
     */
    public static final Strings CI = new CiStrings(true);

    /**
     * The <strong>C</strong>ase-<strong>S</strong>ensitive singleton instance.
     */
    public static final Strings CS = new CsStrings(true);

    /**
     * Constructs a new {@link Builder} instance.
     *
     * @return a new {@link Builder} instance.
     */
    public static final Builder builder() {
        return new Builder();
    }

    /**
     * Tests if the CharSequence contains any of the CharSequences in the given array.
     *
     * <p>
     * A {@code null} {@code cs} CharSequence will return {@code false}. A {@code null} or zero length search array will return {@code false}.
     * </p>
     *
     * @param cs                  The CharSequence to check, may be null
     * @param searchCharSequences The array of CharSequences to search for, may be null. Individual CharSequences may be null as well.
     * @return {@code true} if any of the search CharSequences are found, {@code false} otherwise
     */
    private static boolean containsAny(final ToBooleanBiFunction<CharSequence, CharSequence> test, final CharSequence cs,
            final CharSequence... searchCharSequences) {
        if (StringUtils.isEmpty(cs) || ArrayUtils.isEmpty(searchCharSequences)) {
            return false;
        }
        for (final CharSequence searchCharSequence : searchCharSequences) {
            if (test.applyAsBoolean(cs, searchCharSequence)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Tests for equality in a null-safe manner.
     *
     * See JDK-8015417.
     */
    private static boolean eq(final Object o1, final Object o2) {
        return o1 == null ? o2 == null : o1.equals(o2);
    }

    /**
     * Ignores case when possible.
     */
    private final boolean ignoreCase;

    /**
     * Compares null as less when possible.
     */
    private final boolean nullIsLess;

    /**
     * Constructs a new instance.
     *
     * @param ignoreCase Ignores case when possible.
     * @param nullIsLess Compares null as less when possible.
     */
    private Strings(final boolean ignoreCase, final boolean nullIsLess) {
        this.ignoreCase = ignoreCase;
        this.nullIsLess = nullIsLess;
    }

    /**
     * Appends the suffix to the end of the string if the string does not already end with the suffix.
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.appendIfMissing(null, null)      = null
     * Strings.CS.appendIfMissing("abc", null)     = "abc"
     * Strings.CS.appendIfMissing("", "xyz"        = "xyz"
     * Strings.CS.appendIfMissing("abc", "xyz")    = "abcxyz"
     * Strings.CS.appendIfMissing("abcxyz", "xyz") = "abcxyz"
     * Strings.CS.appendIfMissing("abcXYZ", "xyz") = "abcXYZxyz"
     * </pre>
     * <p>
     * With additional suffixes:
     * </p>
     *
     * <pre>
     * Strings.CS.appendIfMissing(null, null, null)       = null
     * Strings.CS.appendIfMissing("abc", null, null)      = "abc"
     * Strings.CS.appendIfMissing("", "xyz", null)        = "xyz"
     * Strings.CS.appendIfMissing("abc", "xyz", new CharSequence[]{null}) = "abcxyz"
     * Strings.CS.appendIfMissing("abc", "xyz", "")       = "abc"
     * Strings.CS.appendIfMissing("abc", "xyz", "mno")    = "abcxyz"
     * Strings.CS.appendIfMissing("abcxyz", "xyz", "mno") = "abcxyz"
     * Strings.CS.appendIfMissing("abcmno", "xyz", "mno") = "abcmno"
     * Strings.CS.appendIfMissing("abcXYZ", "xyz", "mno") = "abcXYZxyz"
     * Strings.CS.appendIfMissing("abcMNO", "xyz", "mno") = "abcMNOxyz"
     * </pre>
     *
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.appendIfMissing(null, null)      = null
     * Strings.CI.appendIfMissing("abc", null)     = "abc"
     * Strings.CI.appendIfMissing("", "xyz")       = "xyz"
     * Strings.CI.appendIfMissing("abc", "xyz")    = "abcxyz"
     * Strings.CI.appendIfMissing("abcxyz", "xyz") = "abcxyz"
     * Strings.CI.appendIfMissing("abcXYZ", "xyz") = "abcXYZ"
     * </pre>
     * <p>
     * With additional suffixes:
     * </p>
     *
     * <pre>
     * Strings.CI.appendIfMissing(null, null, null)       = null
     * Strings.CI.appendIfMissing("abc", null, null)      = "abc"
     * Strings.CI.appendIfMissing("", "xyz", null)        = "xyz"
     * Strings.CI.appendIfMissing("abc", "xyz", new CharSequence[]{null}) = "abcxyz"
     * Strings.CI.appendIfMissing("abc", "xyz", "")       = "abc"
     * Strings.CI.appendIfMissing("abc", "xyz", "mno")    = "abcxyz"
     * Strings.CI.appendIfMissing("abcxyz", "xyz", "mno") = "abcxyz"
     * Strings.CI.appendIfMissing("abcmno", "xyz", "mno") = "abcmno"
     * Strings.CI.appendIfMissing("abcXYZ", "xyz", "mno") = "abcXYZ"
     * Strings.CI.appendIfMissing("abcMNO", "xyz", "mno") = "abcMNO"
     * </pre>
     *
     * @param str      The string.
     * @param suffix   The suffix to append to the end of the string.
     * @param suffixes Additional suffixes that are valid terminators (optional).
     * @return A new String if suffix was appended, the same string otherwise.
     */
    public String appendIfMissing(final String str, final CharSequence suffix, final CharSequence... suffixes) {
        if (str == null || StringUtils.isEmpty(suffix) || endsWith(str, suffix)) {
            return str;
        }
        if (ArrayUtils.isNotEmpty(suffixes)) {
            for (final CharSequence s : suffixes) {
                if (endsWith(str, s)) {
                    return str;
                }
            }
        }
        return str + suffix;
    }

    /**
     * Compare two Strings lexicographically, like {@link String#compareTo(String)}.
     * <p>
     * The return values are:
     * </p>
     * <ul>
     * <li>{@code int = 0}, if {@code str1} is equal to {@code str2} (or both {@code null})</li>
     * <li>{@code int < 0}, if {@code str1} is less than {@code str2}</li>
     * <li>{@code int > 0}, if {@code str1} is greater than {@code str2}</li>
     * </ul>
     *
     * <p>
     * This is a {@code null} safe version of :
     * </p>
     *
     * <pre>
     * str1.compareTo(str2)
     * </pre>
     *
     * <p>
     * {@code null} value is considered less than non-{@code null} value. Two {@code null} references are considered equal.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>{@code
     * Strings.CS.compare(null, null)   = 0
     * Strings.CS.compare(null , "a")   < 0
     * Strings.CS.compare("a", null)   > 0
     * Strings.CS.compare("abc", "abc") = 0
     * Strings.CS.compare("a", "b")     < 0
     * Strings.CS.compare("b", "a")     > 0
     * Strings.CS.compare("a", "B")     > 0
     * Strings.CS.compare("ab", "abc")  < 0
     * }</pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>{@code
     * Strings.CI.compare(null, null)   = 0
     * Strings.CI.compare(null , "a")   < 0
     * Strings.CI.compare("a", null)    > 0
     * Strings.CI.compare("abc", "abc") = 0
     * Strings.CI.compare("abc", "ABC") = 0
     * Strings.CI.compare("a", "b")     < 0
     * Strings.CI.compare("b", "a")     > 0
     * Strings.CI.compare("a", "B")     < 0
     * Strings.CI.compare("A", "b")     < 0
     * Strings.CI.compare("ab", "ABC")  < 0
     * }</pre>
     *
     * @see String#compareTo(String)
     * @param str1 the String to compare from
     * @param str2 the String to compare to
     * @return &lt; 0, 0, &gt; 0, if {@code str1} is respectively less, equal or greater than {@code str2}
     */
    public abstract int compare(String str1, String str2);

    /**
     * Tests if CharSequence contains a search CharSequence, handling {@code null}. This method uses {@link String#indexOf(String)} if possible.
     *
     * <p>
     * A {@code null} CharSequence will return {@code false}.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.contains(null, *)     = false
     * Strings.CS.contains(*, null)     = false
     * Strings.CS.contains("", "")      = true
     * Strings.CS.contains("abc", "")   = true
     * Strings.CS.contains("abc", "a")  = true
     * Strings.CS.contains("abc", "z")  = false
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.contains(null, *)    = false
     * Strings.CI.contains(*, null)    = false
     * Strings.CI.contains("", "")     = true
     * Strings.CI.contains("abc", "")  = true
     * Strings.CI.contains("abc", "a") = true
     * Strings.CI.contains("abc", "z") = false
     * Strings.CI.contains("abc", "A") = true
     * Strings.CI.contains("abc", "Z") = false
     * </pre>
     *
     * @param seq       the CharSequence to check, may be null
     * @param searchSeq the CharSequence to find, may be null
     * @return true if the CharSequence contains the search CharSequence, false if not or {@code null} string input
     */
    public abstract boolean contains(CharSequence seq, CharSequence searchSeq);

    /**
     * Tests if the CharSequence contains any of the CharSequences in the given array.
     *
     * <p>
     * A {@code null} {@code cs} CharSequence will return {@code false}. A {@code null} or zero length search array will return {@code false}.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.containsAny(null, *)            = false
     * Strings.CS.containsAny("", *)              = false
     * Strings.CS.containsAny(*, null)            = false
     * Strings.CS.containsAny(*, [])              = false
     * Strings.CS.containsAny("abcd", "ab", null) = true
     * Strings.CS.containsAny("abcd", "ab", "cd") = true
     * Strings.CS.containsAny("abc", "d", "abc")  = true
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.containsAny(null, *)            = false
     * Strings.CI.containsAny("", *)              = false
     * Strings.CI.containsAny(*, null)            = false
     * Strings.CI.containsAny(*, [])              = false
     * Strings.CI.containsAny("abcd", "ab", null) = true
     * Strings.CI.containsAny("abcd", "ab", "cd") = true
     * Strings.CI.containsAny("abc", "d", "abc")  = true
     * Strings.CI.containsAny("abc", "D", "ABC")  = true
     * Strings.CI.containsAny("ABC", "d", "abc")  = true
     * </pre>
     *
     * @param cs                  The CharSequence to check, may be null
     * @param searchCharSequences The array of CharSequences to search for, may be null. Individual CharSequences may be null as well.
     * @return {@code true} if any of the search CharSequences are found, {@code false} otherwise
     */
    public boolean containsAny(final CharSequence cs, final CharSequence... searchCharSequences) {
        return containsAny(this::contains, cs, searchCharSequences);
    }

    /**
     * Tests if a CharSequence ends with a specified suffix.
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.endsWith(null, null)      = true
     * Strings.CS.endsWith(null, "def")     = false
     * Strings.CS.endsWith("abcdef", null)  = false
     * Strings.CS.endsWith("abcdef", "def") = true
     * Strings.CS.endsWith("ABCDEF", "def") = false
     * Strings.CS.endsWith("ABCDEF", "cde") = false
     * Strings.CS.endsWith("ABCDEF", "")    = true
     * </pre>
     *
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.endsWith(null, null)      = true
     * Strings.CI.endsWith(null, "def")     = false
     * Strings.CI.endsWith("abcdef", null)  = false
     * Strings.CI.endsWith("abcdef", "def") = true
     * Strings.CI.endsWith("ABCDEF", "def") = true
     * Strings.CI.endsWith("ABCDEF", "cde") = false
     * </pre>
     *
     * @param str    the CharSequence to check, may be null.
     * @param suffix the suffix to find, may be null.
     * @return {@code true} if the CharSequence starts with the prefix or both {@code null}.
     * @see String#endsWith(String)
     */
    public boolean endsWith(final CharSequence str, final CharSequence suffix) {
        if (str == null || suffix == null) {
            return str == suffix;
        }
        final int sufLen = suffix.length();
        if (sufLen > str.length()) {
            return false;
        }
        return CharSequenceUtils.regionMatches(str, ignoreCase, str.length() - sufLen, suffix, 0, sufLen);
    }

    /**
     * Tests if a CharSequence ends with any of the provided suffixes.
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.endsWithAny(null, null)                  = false
     * Strings.CS.endsWithAny(null, new String[] {"abc"})  = false
     * Strings.CS.endsWithAny("abcxyz", null)              = false
     * Strings.CS.endsWithAny("abcxyz", new String[] {""}) = true
     * Strings.CS.endsWithAny("abcxyz", new String[] {"xyz"}) = true
     * Strings.CS.endsWithAny("abcxyz", new String[] {null, "xyz", "abc"}) = true
     * Strings.CS.endsWithAny("abcXYZ", "def", "XYZ")      = true
     * Strings.CS.endsWithAny("abcXYZ", "def", "xyz")      = false
     * </pre>
     *
     * @param sequence      the CharSequence to check, may be null
     * @param searchStrings the CharSequence suffixes to find, may be empty or contain {@code null}
     * @see Strings#endsWith(CharSequence, CharSequence)
     * @return {@code true} if the input {@code sequence} is {@code null} AND no {@code searchStrings} are provided, or the input {@code sequence} ends in any
     *         of the provided {@code searchStrings}.
     */
    public boolean endsWithAny(final CharSequence sequence, final CharSequence... searchStrings) {
        if (StringUtils.isEmpty(sequence) || ArrayUtils.isEmpty(searchStrings)) {
            return false;
        }
        for (final CharSequence searchString : searchStrings) {
            if (endsWith(sequence, searchString)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Compares two CharSequences, returning {@code true} if they represent equal sequences of characters.
     *
     * <p>
     * {@code null}s are handled without exceptions. Two {@code null} references are considered to be equal.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.equals(null, null)   = true
     * Strings.CS.equals(null, "abc")  = false
     * Strings.CS.equals("abc", null)  = false
     * Strings.CS.equals("abc", "abc") = true
     * Strings.CS.equals("abc", "ABC") = false
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.equals(null, null)   = true
     * Strings.CI.equals(null, "abc")  = false
     * Strings.CI.equals("abc", null)  = false
     * Strings.CI.equals("abc", "abc") = true
     * Strings.CI.equals("abc", "ABC") = true
     * </pre>
     *
     * @param cs1 the first CharSequence, may be {@code null}
     * @param cs2 the second CharSequence, may be {@code null}
     * @return {@code true} if the CharSequences are equal (case-sensitive), or both {@code null}
     * @see Object#equals(Object)
     * @see String#compareTo(String)
     * @see String#equalsIgnoreCase(String)
     */
    public abstract boolean equals(CharSequence cs1, CharSequence cs2);

    /**
     * Compares two CharSequences, returning {@code true} if they represent equal sequences of characters.
     *
     * <p>
     * {@code null}s are handled without exceptions. Two {@code null} references are considered to be equal.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.equals(null, null)   = true
     * Strings.CS.equals(null, "abc")  = false
     * Strings.CS.equals("abc", null)  = false
     * Strings.CS.equals("abc", "abc") = true
     * Strings.CS.equals("abc", "ABC") = false
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.equals(null, null)   = true
     * Strings.CI.equals(null, "abc")  = false
     * Strings.CI.equals("abc", null)  = false
     * Strings.CI.equals("abc", "abc") = true
     * Strings.CI.equals("abc", "ABC") = true
     * </pre>
     *
     * @param str1 the first CharSequence, may be {@code null}
     * @param str2 the second CharSequence, may be {@code null}
     * @return {@code true} if the CharSequences are equal (case-sensitive), or both {@code null}
     * @see Object#equals(Object)
     * @see String#compareTo(String)
     * @see String#equalsIgnoreCase(String)
     */
    public abstract boolean equals(String str1, String str2);

    /**
     * Compares given {@code string} to a CharSequences vararg of {@code searchStrings}, returning {@code true} if the {@code string} is equal to any of the
     * {@code searchStrings}.
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.equalsAny(null, (CharSequence[]) null) = false
     * Strings.CS.equalsAny(null, null, null)    = true
     * Strings.CS.equalsAny(null, "abc", "def")  = false
     * Strings.CS.equalsAny("abc", null, "def")  = false
     * Strings.CS.equalsAny("abc", "abc", "def") = true
     * Strings.CS.equalsAny("abc", "ABC", "DEF") = false
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.equalsAny(null, (CharSequence[]) null) = false
     * Strings.CI.equalsAny(null, null, null)    = true
     * Strings.CI.equalsAny(null, "abc", "def")  = false
     * Strings.CI.equalsAny("abc", null, "def")  = false
     * Strings.CI.equalsAny("abc", "abc", "def") = true
     * Strings.CI.equalsAny("abc", "ABC", "DEF") = true
     * </pre>
     *
     * @param string        to compare, may be {@code null}.
     * @param searchStrings a vararg of strings, may be {@code null}.
     * @return {@code true} if the string is equal (case-sensitive) to any other element of {@code searchStrings}; {@code false} if {@code searchStrings} is
     *         null or contains no matches.
     */
    public boolean equalsAny(final CharSequence string, final CharSequence... searchStrings) {
        if (ArrayUtils.isNotEmpty(searchStrings)) {
            for (final CharSequence next : searchStrings) {
                if (equals(string, next)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Finds the first index within a CharSequence, handling {@code null}. This method uses {@link String#indexOf(String, int)} if possible.
     *
     * <p>
     * A {@code null} CharSequence will return {@code -1}.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.indexOf(null, *)          = -1
     * Strings.CS.indexOf(*, null)          = -1
     * Strings.CS.indexOf("", "")           = 0
     * Strings.CS.indexOf("", *)            = -1 (except when * = "")
     * Strings.CS.indexOf("aabaabaa", "a")  = 0
     * Strings.CS.indexOf("aabaabaa", "b")  = 2
     * Strings.CS.indexOf("aabaabaa", "ab") = 1
     * Strings.CS.indexOf("aabaabaa", "")   = 0
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.indexOf(null, *)          = -1
     * Strings.CI.indexOf(*, null)          = -1
     * Strings.CI.indexOf("", "")           = 0
     * Strings.CI.indexOf(" ", " ")         = 0
     * Strings.CI.indexOf("aabaabaa", "a")  = 0
     * Strings.CI.indexOf("aabaabaa", "b")  = 2
     * Strings.CI.indexOf("aabaabaa", "ab") = 1
     * </pre>
     *
     * @param seq       the CharSequence to check, may be null
     * @param searchSeq the CharSequence to find, may be null
     * @return the first index of the search CharSequence, -1 if no match or {@code null} string input
     */
    public int indexOf(final CharSequence seq, final CharSequence searchSeq) {
        return indexOf(seq, searchSeq, 0);
    }

    /**
     * Finds the first index within a CharSequence, handling {@code null}. This method uses {@link String#indexOf(String, int)} if possible.
     *
     * <p>
     * A {@code null} CharSequence will return {@code -1}. A negative start position is treated as zero. An empty ("") search CharSequence always matches. A
     * start position greater than the string length only matches an empty search CharSequence.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.indexOf(null, *, *)          = -1
     * Strings.CS.indexOf(*, null, *)          = -1
     * Strings.CS.indexOf("", "", 0)           = 0
     * Strings.CS.indexOf("", *, 0)            = -1 (except when * = "")
     * Strings.CS.indexOf("aabaabaa", "a", 0)  = 0
     * Strings.CS.indexOf("aabaabaa", "b", 0)  = 2
     * Strings.CS.indexOf("aabaabaa", "ab", 0) = 1
     * Strings.CS.indexOf("aabaabaa", "b", 3)  = 5
     * Strings.CS.indexOf("aabaabaa", "b", 9)  = -1
     * Strings.CS.indexOf("aabaabaa", "b", -1) = 2
     * Strings.CS.indexOf("aabaabaa", "", 2)   = 2
     * Strings.CS.indexOf("abc", "", 9)        = 3
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.indexOf(null, *, *)          = -1
     * Strings.CI.indexOf(*, null, *)          = -1
     * Strings.CI.indexOf("", "", 0)           = 0
     * Strings.CI.indexOf("aabaabaa", "A", 0)  = 0
     * Strings.CI.indexOf("aabaabaa", "B", 0)  = 2
     * Strings.CI.indexOf("aabaabaa", "AB", 0) = 1
     * Strings.CI.indexOf("aabaabaa", "B", 3)  = 5
     * Strings.CI.indexOf("aabaabaa", "B", 9)  = -1
     * Strings.CI.indexOf("aabaabaa", "B", -1) = 2
     * Strings.CI.indexOf("aabaabaa", "", 2)   = 2
     * Strings.CI.indexOf("abc", "", 9)        = -1
     * </pre>
     *
     * @param seq       the CharSequence to check, may be null
     * @param searchSeq the CharSequence to find, may be null
     * @param startPos  the start position, negative treated as zero
     * @return the first index of the search CharSequence (always &ge; startPos), -1 if no match or {@code null} string input
     */
    public abstract int indexOf(CharSequence seq, CharSequence searchSeq, int startPos);

    /**
     * Tests whether to ignore case.
     *
     * @return whether to ignore case.
     */
    public boolean isCaseSensitive() {
        return !ignoreCase;
    }

    /**
     * Tests whether null is less when comparing.
     *
     * @return whether null is less when comparing.
     */
    boolean isNullIsLess() {
        return nullIsLess;
    }

    /**
     * Finds the last index within a CharSequence, handling {@code null}. This method uses {@link String#lastIndexOf(String)} if possible.
     *
     * <p>
     * A {@code null} CharSequence will return {@code -1}.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.lastIndexOf(null, *)          = -1
     * Strings.CS.lastIndexOf(*, null)          = -1
     * Strings.CS.lastIndexOf("", "")           = 0
     * Strings.CS.lastIndexOf("aabaabaa", "a")  = 7
     * Strings.CS.lastIndexOf("aabaabaa", "b")  = 5
     * Strings.CS.lastIndexOf("aabaabaa", "ab") = 4
     * Strings.CS.lastIndexOf("aabaabaa", "")   = 8
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.lastIndexOf(null, *)          = -1
     * Strings.CI.lastIndexOf(*, null)          = -1
     * Strings.CI.lastIndexOf("aabaabaa", "A")  = 7
     * Strings.CI.lastIndexOf("aabaabaa", "B")  = 5
     * Strings.CI.lastIndexOf("aabaabaa", "AB") = 4
     * </pre>
     *
     * @param str       the CharSequence to check, may be null
     * @param searchStr the CharSequence to find, may be null
     * @return the last index of the search String, -1 if no match or {@code null} string input
     */
    public int lastIndexOf(final CharSequence str, final CharSequence searchStr) {
        if (str == null) {
            return INDEX_NOT_FOUND;
        }
        return lastIndexOf(str, searchStr, str.length());
    }

    /**
     * Finds the last index within a CharSequence, handling {@code null}. This method uses {@link String#lastIndexOf(String, int)} if possible.
     *
     * <p>
     * A {@code null} CharSequence will return {@code -1}. A negative start position returns {@code -1}. An empty ("") search CharSequence always matches unless
     * the start position is negative. A start position greater than the string length searches the whole string. The search starts at the startPos and works
     * backwards; matches starting after the start position are ignored.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.lastIndexOf(null, *, *)          = -1
     * Strings.CS.lastIndexOf(*, null, *)          = -1
     * Strings.CS.lastIndexOf("aabaabaa", "a", 8)  = 7
     * Strings.CS.lastIndexOf("aabaabaa", "b", 8)  = 5
     * Strings.CS.lastIndexOf("aabaabaa", "ab", 8) = 4
     * Strings.CS.lastIndexOf("aabaabaa", "b", 9)  = 5
     * Strings.CS.lastIndexOf("aabaabaa", "b", -1) = -1
     * Strings.CS.lastIndexOf("aabaabaa", "a", 0)  = 0
     * Strings.CS.lastIndexOf("aabaabaa", "b", 0)  = -1
     * Strings.CS.lastIndexOf("aabaabaa", "b", 1)  = -1
     * Strings.CS.lastIndexOf("aabaabaa", "b", 2)  = 2
     * Strings.CS.lastIndexOf("aabaabaa", "ba", 2)  = 2
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.lastIndexOf(null, *, *)          = -1
     * Strings.CI.lastIndexOf(*, null, *)          = -1
     * Strings.CI.lastIndexOf("aabaabaa", "A", 8)  = 7
     * Strings.CI.lastIndexOf("aabaabaa", "B", 8)  = 5
     * Strings.CI.lastIndexOf("aabaabaa", "AB", 8) = 4
     * Strings.CI.lastIndexOf("aabaabaa", "B", 9)  = 5
     * Strings.CI.lastIndexOf("aabaabaa", "B", -1) = -1
     * Strings.CI.lastIndexOf("aabaabaa", "A", 0)  = 0
     * Strings.CI.lastIndexOf("aabaabaa", "B", 0)  = -1
     * </pre>
     *
     * @param seq       the CharSequence to check, may be null
     * @param searchSeq the CharSequence to find, may be null
     * @param startPos  the start position, negative treated as zero
     * @return the last index of the search CharSequence (always &le; startPos), -1 if no match or {@code null} string input
     */
    public abstract int lastIndexOf(CharSequence seq, CharSequence searchSeq, int startPos);

    /**
     * Prepends the prefix to the start of the string if the string does not already start with any of the prefixes.
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.prependIfMissing(null, null) = null
     * Strings.CS.prependIfMissing("abc", null) = "abc"
     * Strings.CS.prependIfMissing("", "xyz") = "xyz"
     * Strings.CS.prependIfMissing("abc", "xyz") = "xyzabc"
     * Strings.CS.prependIfMissing("xyzabc", "xyz") = "xyzabc"
     * Strings.CS.prependIfMissing("XYZabc", "xyz") = "xyzXYZabc"
     * </pre>
     * <p>
     * With additional prefixes,
     * </p>
     *
     * <pre>
     * Strings.CS.prependIfMissing(null, null, null) = null
     * Strings.CS.prependIfMissing("abc", null, null) = "abc"
     * Strings.CS.prependIfMissing("", "xyz", null) = "xyz"
     * Strings.CS.prependIfMissing("abc", "xyz", new CharSequence[]{null}) = "xyzabc"
     * Strings.CS.prependIfMissing("abc", "xyz", "") = "abc"
     * Strings.CS.prependIfMissing("abc", "xyz", "mno") = "xyzabc"
     * Strings.CS.prependIfMissing("xyzabc", "xyz", "mno") = "xyzabc"
     * Strings.CS.prependIfMissing("mnoabc", "xyz", "mno") = "mnoabc"
     * Strings.CS.prependIfMissing("XYZabc", "xyz", "mno") = "xyzXYZabc"
     * Strings.CS.prependIfMissing("MNOabc", "xyz", "mno") = "xyzMNOabc"
     * </pre>
     *
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.prependIfMissing(null, null) = null
     * Strings.CI.prependIfMissing("abc", null) = "abc"
     * Strings.CI.prependIfMissing("", "xyz") = "xyz"
     * Strings.CI.prependIfMissing("abc", "xyz") = "xyzabc"
     * Strings.CI.prependIfMissing("xyzabc", "xyz") = "xyzabc"
     * Strings.CI.prependIfMissing("XYZabc", "xyz") = "XYZabc"
     * </pre>
     * <p>
     * With additional prefixes,
     * </p>
     *
     * <pre>
     * Strings.CI.prependIfMissing(null, null, null) = null
     * Strings.CI.prependIfMissing("abc", null, null) = "abc"
     * Strings.CI.prependIfMissing("", "xyz", null) = "xyz"
     * Strings.CI.prependIfMissing("abc", "xyz", new CharSequence[]{null}) = "xyzabc"
     * Strings.CI.prependIfMissing("abc", "xyz", "") = "abc"
     * Strings.CI.prependIfMissing("abc", "xyz", "mno") = "xyzabc"
     * Strings.CI.prependIfMissing("xyzabc", "xyz", "mno") = "xyzabc"
     * Strings.CI.prependIfMissing("mnoabc", "xyz", "mno") = "mnoabc"
     * Strings.CI.prependIfMissing("XYZabc", "xyz", "mno") = "XYZabc"
     * Strings.CI.prependIfMissing("MNOabc", "xyz", "mno") = "MNOabc"
     * </pre>
     *
     * @param str      The string.
     * @param prefix   The prefix to prepend to the start of the string.
     * @param prefixes Additional prefixes that are valid.
     * @return A new String if prefix was prepended, the same string otherwise.
     */
    public String prependIfMissing(final String str, final CharSequence prefix, final CharSequence... prefixes) {
        if (str == null || StringUtils.isEmpty(prefix) || startsWith(str, prefix)) {
            return str;
        }
        if (ArrayUtils.isNotEmpty(prefixes)) {
            for (final CharSequence p : prefixes) {
                if (startsWith(str, p)) {
                    return str;
                }
            }
        }
        return prefix + str;
    }

    /**
     * Removes all occurrences of a substring from within the source string.
     *
     * <p>
     * A {@code null} source string will return {@code null}. An empty ("") source string will return the empty string. A {@code null} remove string will return
     * the source string. An empty ("") remove string will return the source string.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.remove(null, *)        = null
     * Strings.CS.remove("", *)          = ""
     * Strings.CS.remove(*, null)        = *
     * Strings.CS.remove(*, "")          = *
     * Strings.CS.remove("queued", "ue") = "qd"
     * Strings.CS.remove("queued", "zz") = "queued"
     * </pre>
     *
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.remove(null, *)        = null
     * Strings.CI.remove("", *)          = ""
     * Strings.CI.remove(*, null)        = *
     * Strings.CI.remove(*, "")          = *
     * Strings.CI.remove("queued", "ue") = "qd"
     * Strings.CI.remove("queued", "zz") = "queued"
     * Strings.CI.remove("quEUed", "UE") = "qd"
     * Strings.CI.remove("queued", "zZ") = "queued"
     * </pre>
     *
     * @param str    the source String to search, may be null
     * @param remove the String to search for and remove, may be null
     * @return the substring with the string removed if found, {@code null} if null String input
     */
    public String remove(final String str, final String remove) {
        return replace(str, remove, StringUtils.EMPTY, -1);
    }

    /**
     * Case-insensitive removal of a substring if it is at the end of a source string, otherwise returns the source string.
     *
     * <p>
     * A {@code null} source string will return {@code null}. An empty ("") source string will return the empty string. A {@code null} search string will return
     * the source string.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.removeEnd(null, *)      = null
     * Strings.CS.removeEnd("", *)        = ""
     * Strings.CS.removeEnd(*, null)      = *
     * Strings.CS.removeEnd("www.domain.com", ".com.")  = "www.domain.com"
     * Strings.CS.removeEnd("www.domain.com", ".com")   = "www.domain"
     * Strings.CS.removeEnd("www.domain.com", "domain") = "www.domain.com"
     * Strings.CS.removeEnd("abc", "")    = "abc"
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.removeEnd(null, *)      = null
     * Strings.CI.removeEnd("", *)        = ""
     * Strings.CI.removeEnd(*, null)      = *
     * Strings.CI.removeEnd("www.domain.com", ".com.")  = "www.domain.com"
     * Strings.CI.removeEnd("www.domain.com", ".com")   = "www.domain"
     * Strings.CI.removeEnd("www.domain.com", "domain") = "www.domain.com"
     * Strings.CI.removeEnd("abc", "")    = "abc"
     * Strings.CI.removeEnd("www.domain.com", ".COM") = "www.domain")
     * Strings.CI.removeEnd("www.domain.COM", ".com") = "www.domain")
     * </pre>
     *
     * @param str    the source String to search, may be null
     * @param remove the String to search for (case-insensitive) and remove, may be null
     * @return the substring with the string removed if found, {@code null} if null String input
     */
    public String removeEnd(final String str, final CharSequence remove) {
        if (StringUtils.isEmpty(str) || StringUtils.isEmpty(remove)) {
            return str;
        }
        if (endsWith(str, remove)) {
            return str.substring(0, str.length() - remove.length());
        }
        return str;
    }

    /**
     * Case-insensitive removal of a substring if it is at the beginning of a source string, otherwise returns the source string.
     *
     * <p>
     * A {@code null} source string will return {@code null}. An empty ("") source string will return the empty string. A {@code null} search string will return
     * the source string.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.removeStart(null, *)      = null
     * Strings.CS.removeStart("", *)        = ""
     * Strings.CS.removeStart(*, null)      = *
     * Strings.CS.removeStart("www.domain.com", "www.")   = "domain.com"
     * Strings.CS.removeStart("domain.com", "www.")       = "domain.com"
     * Strings.CS.removeStart("www.domain.com", "domain") = "www.domain.com"
     * Strings.CS.removeStart("abc", "")    = "abc"
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.removeStart(null, *)      = null
     * Strings.CI.removeStart("", *)        = ""
     * Strings.CI.removeStart(*, null)      = *
     * Strings.CI.removeStart("www.domain.com", "www.")   = "domain.com"
     * Strings.CI.removeStart("www.domain.com", "WWW.")   = "domain.com"
     * Strings.CI.removeStart("domain.com", "www.")       = "domain.com"
     * Strings.CI.removeStart("www.domain.com", "domain") = "www.domain.com"
     * Strings.CI.removeStart("abc", "")    = "abc"
     * </pre>
     *
     * @param str    the source String to search, may be null
     * @param remove the String to search for (case-insensitive) and remove, may be null
     * @return the substring with the string removed if found, {@code null} if null String input
     */
    public String removeStart(final String str, final CharSequence remove) {
        if (str != null && startsWith(str, remove)) {
            return str.substring(StringUtils.length(remove));
        }
        return str;
    }

    /**
     * Case insensitively replaces all occurrences of a String within another String.
     *
     * <p>
     * A {@code null} reference passed to this method is a no-op.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.replace(null, *, *)        = null
     * Strings.CS.replace("", *, *)          = ""
     * Strings.CS.replace("any", null, *)    = "any"
     * Strings.CS.replace("any", *, null)    = "any"
     * Strings.CS.replace("any", "", *)      = "any"
     * Strings.CS.replace("aba", "a", null)  = "aba"
     * Strings.CS.replace("aba", "a", "")    = "b"
     * Strings.CS.replace("aba", "a", "z")   = "zbz"
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.replace(null, *, *)        = null
     * Strings.CI.replace("", *, *)          = ""
     * Strings.CI.replace("any", null, *)    = "any"
     * Strings.CI.replace("any", *, null)    = "any"
     * Strings.CI.replace("any", "", *)      = "any"
     * Strings.CI.replace("aba", "a", null)  = "aba"
     * Strings.CI.replace("abA", "A", "")    = "b"
     * Strings.CI.replace("aba", "A", "z")   = "zbz"
     * </pre>
     *
     * @see #replace(String text, String searchString, String replacement, int max)
     * @param text         text to search and replace in, may be null
     * @param searchString the String to search for (case-insensitive), may be null
     * @param replacement  the String to replace it with, may be null
     * @return the text with any replacements processed, {@code null} if null String input
     */
    public String replace(final String text, final String searchString, final String replacement) {
        return replace(text, searchString, replacement, -1);
    }

    /**
     * Replaces a String with another String inside a larger String, for the first {@code max} values of the search String.
     *
     * <p>
     * A {@code null} reference passed to this method is a no-op.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.replace(null, *, *, *)         = null
     * Strings.CS.replace("", *, *, *)           = ""
     * Strings.CS.replace("any", null, *, *)     = "any"
     * Strings.CS.replace("any", *, null, *)     = "any"
     * Strings.CS.replace("any", "", *, *)       = "any"
     * Strings.CS.replace("any", *, *, 0)        = "any"
     * Strings.CS.replace("abaa", "a", null, -1) = "abaa"
     * Strings.CS.replace("abaa", "a", "", -1)   = "b"
     * Strings.CS.replace("abaa", "a", "z", 0)   = "abaa"
     * Strings.CS.replace("abaa", "a", "z", 1)   = "zbaa"
     * Strings.CS.replace("abaa", "a", "z", 2)   = "zbza"
     * Strings.CS.replace("abaa", "a", "z", -1)  = "zbzz"
     * </pre>
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.replace(null, *, *, *)         = null
     * Strings.CI.replace("", *, *, *)           = ""
     * Strings.CI.replace("any", null, *, *)     = "any"
     * Strings.CI.replace("any", *, null, *)     = "any"
     * Strings.CI.replace("any", "", *, *)       = "any"
     * Strings.CI.replace("any", *, *, 0)        = "any"
     * Strings.CI.replace("abaa", "a", null, -1) = "abaa"
     * Strings.CI.replace("abaa", "a", "", -1)   = "b"
     * Strings.CI.replace("abaa", "a", "z", 0)   = "abaa"
     * Strings.CI.replace("abaa", "A", "z", 1)   = "zbaa"
     * Strings.CI.replace("abAa", "a", "z", 2)   = "zbza"
     * Strings.CI.replace("abAa", "a", "z", -1)  = "zbzz"
     * </pre>
     *
     * @param text         text to search and replace in, may be null
     * @param searchString the String to search for (case-insensitive), may be null
     * @param replacement  the String to replace it with, may be null
     * @param max          maximum number of values to replace, or {@code -1} if no maximum
     * @return the text with any replacements processed, {@code null} if null String input
     */
    public String replace(final String text, String searchString, final String replacement, int max) {
        if (StringUtils.isEmpty(text) || StringUtils.isEmpty(searchString) || replacement == null || max == 0) {
            return text;
        }
        if (ignoreCase) {
            searchString = searchString.toLowerCase();
        }
        int start = 0;
        int end = indexOf(text, searchString, start);
        if (end == INDEX_NOT_FOUND) {
            return text;
        }
        final int replLength = searchString.length();
        int increase = Math.max(replacement.length() - replLength, 0);
        increase *= max < 0 ? 16 : Math.min(max, 64);
        final StringBuilder buf = new StringBuilder(text.length() + increase);
        while (end != INDEX_NOT_FOUND) {
            buf.append(text, start, end).append(replacement);
            start = end + replLength;
            if (--max == 0) {
                break;
            }
            end = indexOf(text, searchString, start);
        }
        buf.append(text, start, text.length());
        return buf.toString();
    }

    /**
     * Replaces a String with another String inside a larger String, once.
     *
     * <p>
     * A {@code null} reference passed to this method is a no-op.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.replaceOnce(null, *, *)        = null
     * Strings.CS.replaceOnce("", *, *)          = ""
     * Strings.CS.replaceOnce("any", null, *)    = "any"
     * Strings.CS.replaceOnce("any", *, null)    = "any"
     * Strings.CS.replaceOnce("any", "", *)      = "any"
     * Strings.CS.replaceOnce("aba", "a", null)  = "aba"
     * Strings.CS.replaceOnce("aba", "a", "")    = "ba"
     * Strings.CS.replaceOnce("aba", "a", "z")   = "zba"
     * </pre>
     *
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.replaceOnce(null, *, *)        = null
     * Strings.CI.replaceOnce("", *, *)          = ""
     * Strings.CI.replaceOnce("any", null, *)    = "any"
     * Strings.CI.replaceOnce("any", *, null)    = "any"
     * Strings.CI.replaceOnce("any", "", *)      = "any"
     * Strings.CI.replaceOnce("aba", "a", null)  = "aba"
     * Strings.CI.replaceOnce("aba", "a", "")    = "ba"
     * Strings.CI.replaceOnce("aba", "a", "z")   = "zba"
     * Strings.CI.replaceOnce("FoOFoofoo", "foo", "") = "Foofoo"
     * </pre>
     *
     * @see #replace(String text, String searchString, String replacement, int max)
     * @param text         text to search and replace in, may be null
     * @param searchString the String to search for, may be null
     * @param replacement  the String to replace with, may be null
     * @return the text with any replacements processed, {@code null} if null String input
     */
    public String replaceOnce(final String text, final String searchString, final String replacement) {
        return replace(text, searchString, replacement, 1);
    }

    /**
     * Tests if a CharSequence starts with a specified prefix.
     *
     * <p>
     * {@code null}s are handled without exceptions. Two {@code null} references are considered to be equal.
     * </p>
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.startsWith(null, null)      = true
     * Strings.CS.startsWith(null, "abc")     = false
     * Strings.CS.startsWith("abcdef", null)  = false
     * Strings.CS.startsWith("abcdef", "abc") = true
     * Strings.CS.startsWith("ABCDEF", "abc") = false
     * </pre>
     *
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.startsWith(null, null)      = true
     * Strings.CI.startsWith(null, "abc")     = false
     * Strings.CI.startsWith("abcdef", null)  = false
     * Strings.CI.startsWith("abcdef", "abc") = true
     * Strings.CI.startsWith("ABCDEF", "abc") = true
     * </pre>
     *
     * @see String#startsWith(String)
     * @param str    the CharSequence to check, may be null
     * @param prefix the prefix to find, may be null
     * @return {@code true} if the CharSequence starts with the prefix, case-sensitive, or both {@code null}
     */
    public boolean startsWith(final CharSequence str, final CharSequence prefix) {
        if (str == null || prefix == null) {
            return str == prefix;
        }
        final int preLen = prefix.length();
        if (preLen > str.length()) {
            return false;
        }
        return CharSequenceUtils.regionMatches(str, ignoreCase, 0, prefix, 0, preLen);
    }

    /**
     * Tests if a CharSequence starts with any of the provided prefixes.
     *
     * <p>
     * Case-sensitive examples
     * </p>
     *
     * <pre>
     * Strings.CS.startsWithAny(null, null)      = false
     * Strings.CS.startsWithAny(null, new String[] {"abc"})  = false
     * Strings.CS.startsWithAny("abcxyz", null)     = false
     * Strings.CS.startsWithAny("abcxyz", new String[] {""}) = true
     * Strings.CS.startsWithAny("abcxyz", new String[] {"abc"}) = true
     * Strings.CS.startsWithAny("abcxyz", new String[] {null, "xyz", "abc"}) = true
     * Strings.CS.startsWithAny("abcxyz", null, "xyz", "ABCX") = false
     * Strings.CS.startsWithAny("ABCXYZ", null, "xyz", "abc") = false
     * </pre>
     *
     * <p>
     * Case-insensitive examples
     * </p>
     *
     * <pre>
     * Strings.CI.startsWithAny(null, null)      = false
     * Strings.CI.startsWithAny(null, new String[] {"aBc"})  = false
     * Strings.CI.startsWithAny("AbCxYz", null)     = false
     * Strings.CI.startsWithAny("AbCxYz", new String[] {""}) = true
     * Strings.CI.startsWithAny("AbCxYz", new String[] {"aBc"}) = true
     * Strings.CI.startsWithAny("AbCxYz", new String[] {null, "XyZ", "aBc"}) = true
     * Strings.CI.startsWithAny("abcxyz", null, "xyz", "ABCX") = true
     * Strings.CI.startsWithAny("ABCXYZ", null, "xyz", "abc") = true
     * </pre>
     *
     * @param sequence      the CharSequence to check, may be null
     * @param searchStrings the CharSequence prefixes, may be empty or contain {@code null}
     * @see Strings#startsWith(CharSequence, CharSequence)
     * @return {@code true} if the input {@code sequence} is {@code null} AND no {@code searchStrings} are provided, or the input {@code sequence} begins with
     *         any of the provided {@code searchStrings}.
     */
    public boolean startsWithAny(final CharSequence sequence, final CharSequence... searchStrings) {
        if (StringUtils.isEmpty(sequence) || ArrayUtils.isEmpty(searchStrings)) {
            return false;
        }
        for (final CharSequence searchString : searchStrings) {
            if (startsWith(sequence, searchString)) {
                return true;
            }
        }
        return false;
    }

}