This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git


The following commit(s) were added to refs/heads/master by this push:
     new 6941f81cd Add Strings and refactor StringUtils
6941f81cd is described below

commit 6941f81cd321a61217a687c0077fcd0d0a3044a2
Author: Gary Gregory <garydgreg...@gmail.com>
AuthorDate: Sat Sep 21 10:35:43 2024 -0400

    Add Strings and refactor StringUtils
---
 src/changes/changes.xml                            |   3 +-
 src/conf/spotbugs-exclude-filter.xml               |  10 +
 .../apache/commons/lang3/CharSequenceUtils.java    |   9 +-
 .../java/org/apache/commons/lang3/StringUtils.java | 486 +++-------
 .../java/org/apache/commons/lang3/Strings.java     | 976 +++++++++++++++++++++
 .../commons/lang3/builder/ToStringStyle.java       |   3 +-
 .../lang3/exception/DefaultExceptionContext.java   |   8 +-
 .../org/apache/commons/lang3/text/StrBuilder.java  |   3 +-
 .../java/org/apache/commons/lang3/StringsTest.java |  57 ++
 9 files changed, 1183 insertions(+), 372 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index e98200ee3..3de1ad13c 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -50,8 +50,9 @@ The <action> type attribute can be add,update,fix,remove.
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Fix flaky FileUtilsWaitForTest.testWaitForNegativeDuration().</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Pick up exec-maven-plugin version from parent POM.</action>
     <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Speed up and sanitize StopWatchTest.</action>
-    <action                   type="fix" dev="ggregory" due-to="Fabrice 
Benhamouda">Fix handling of non-ASCII letters & numbers in RandomStringUtils 
#1273.</action> 
+    <action                   type="fix" dev="ggregory" due-to="Fabrice 
Benhamouda">Fix handling of non-ASCII letters and numbers in RandomStringUtils 
#1273.</action> 
     <!-- ADD -->
+    <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add Strings and refactor StringUtils.</action>
     <!-- UPDATE -->
     <action                   type="update" dev="ggregory" due-to="Gary 
Gregory, Dependabot">Bump org.apache.commons:commons-parent from 73 to 75 
#1267, #1277.</action>
   </release>
diff --git a/src/conf/spotbugs-exclude-filter.xml 
b/src/conf/spotbugs-exclude-filter.xml
index d935cb51d..3a89b82d5 100644
--- a/src/conf/spotbugs-exclude-filter.xml
+++ b/src/conf/spotbugs-exclude-filter.xml
@@ -113,6 +113,16 @@
     </Or>
     <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ" />
   </Match>
+  <Match>
+    <Class name="org.apache.commons.lang3.Strings$CiStrings" />
+    <Method name="compare" params="java.lang.String,java.lang.String"/>
+    <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ" />
+  </Match>
+  <Match>
+    <Class name="org.apache.commons.lang3.Strings$CsStrings" />
+    <Method name="compare" params="java.lang.String,java.lang.String"/>
+    <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ" />
+  </Match>
 
   <!-- Reason: Very much intended to do a fall through on the switch -->
   <Match>
diff --git a/src/main/java/org/apache/commons/lang3/CharSequenceUtils.java 
b/src/main/java/org/apache/commons/lang3/CharSequenceUtils.java
index f34eb2a15..ea6431115 100644
--- a/src/main/java/org/apache/commons/lang3/CharSequenceUtils.java
+++ b/src/main/java/org/apache/commons/lang3/CharSequenceUtils.java
@@ -41,12 +41,15 @@ public class CharSequenceUtils {
     /**
      * Used by the indexOf(CharSequence methods) as a green implementation of 
indexOf.
      *
-     * @param cs the {@link CharSequence} to be processed
+     * @param cs         the {@link CharSequence} to be processed
      * @param searchChar the {@link CharSequence} to be searched for
-     * @param start the start index
-     * @return the index where the search sequence was found
+     * @param start      the start index
+     * @return the index where the search sequence was found, or {@code -1} if 
there is no such occurrence.
      */
     static int indexOf(final CharSequence cs, final CharSequence searchChar, 
final int start) {
+        if (cs == null || searchChar == null) {
+            return StringUtils.INDEX_NOT_FOUND;
+        }
         if (cs instanceof String) {
             return ((String) cs).indexOf(searchChar.toString(), start);
         }
diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java 
b/src/main/java/org/apache/commons/lang3/StringUtils.java
index c69d0526d..0f16d63f3 100644
--- a/src/main/java/org/apache/commons/lang3/StringUtils.java
+++ b/src/main/java/org/apache/commons/lang3/StringUtils.java
@@ -31,7 +31,6 @@ import java.util.function.Supplier;
 import java.util.regex.Pattern;
 
 import org.apache.commons.lang3.function.Suppliers;
-import org.apache.commons.lang3.function.ToBooleanBiFunction;
 import org.apache.commons.lang3.stream.LangCollectors;
 import org.apache.commons.lang3.stream.Streams;
 
@@ -413,33 +412,7 @@ public class StringUtils {
     }
 
     /**
-     * Appends the suffix to the end of the string if the string does not
-     * already end with the suffix.
-     *
-     * @param str The string.
-     * @param suffix The suffix to append to the end of the string.
-     * @param ignoreCase Indicates whether the compare should ignore case.
-     * @param suffixes Additional suffixes that are valid terminators 
(optional).
-     *
-     * @return A new String if suffix was appended, the same string otherwise.
-     */
-    private static String appendIfMissing(final String str, final CharSequence 
suffix, final boolean ignoreCase, final CharSequence... suffixes) {
-        if (str == null || isEmpty(suffix) || endsWith(str, suffix, 
ignoreCase)) {
-            return str;
-        }
-        if (ArrayUtils.isNotEmpty(suffixes)) {
-            for (final CharSequence s : suffixes) {
-                if (endsWith(str, s, ignoreCase)) {
-                    return str;
-                }
-            }
-        }
-        return str + suffix;
-    }
-
-    /**
-     * Appends the suffix to the end of the string if the string does not
-     * already end with any of the suffixes.
+     * Appends the suffix to the end of the string if the string does not 
already end with any of the suffixes.
      *
      * <pre>
      * StringUtils.appendIfMissing(null, null)      = null
@@ -449,7 +422,10 @@ public class StringUtils {
      * StringUtils.appendIfMissing("abcxyz", "xyz") = "abcxyz"
      * StringUtils.appendIfMissing("abcXYZ", "xyz") = "abcXYZxyz"
      * </pre>
-     * <p>With additional suffixes,</p>
+     * <p>
+     * With additional suffixes,
+     * </p>
+     *
      * <pre>
      * StringUtils.appendIfMissing(null, null, null)       = null
      * StringUtils.appendIfMissing("abc", null, null)      = "abc"
@@ -463,16 +439,16 @@ public class StringUtils {
      * StringUtils.appendIfMissing("abcMNO", "xyz", "mno") = "abcMNOxyz"
      * </pre>
      *
-     * @param str The string.
-     * @param suffix The suffix to append to the end of the string.
+     * @param str      The string.
+     * @param suffix   The suffix to append to the end of the string.
      * @param suffixes Additional suffixes that are valid terminators.
-     *
      * @return A new String if suffix was appended, the same string otherwise.
-     *
      * @since 3.2
+     * @deprecated Use {@link Strings#appendIfMissing(String, CharSequence, 
CharSequence...) Strings.CS.appendIfMissing(String, CharSequence, 
CharSequence...)}
      */
+    @Deprecated
     public static String appendIfMissing(final String str, final CharSequence 
suffix, final CharSequence... suffixes) {
-        return appendIfMissing(str, suffix, false, suffixes);
+        return Strings.CS.appendIfMissing(str, suffix, suffixes);
     }
 
     /**
@@ -504,13 +480,13 @@ public class StringUtils {
      * @param str The string.
      * @param suffix The suffix to append to the end of the string.
      * @param suffixes Additional suffixes that are valid terminators.
-     *
      * @return A new String if suffix was appended, the same string otherwise.
-     *
      * @since 3.2
+     * @deprecated Use {@link Strings#appendIfMissing(String, CharSequence, 
CharSequence...) Strings.CI.appendIfMissing(String, CharSequence, 
CharSequence...)}
      */
+    @Deprecated
     public static String appendIfMissingIgnoreCase(final String str, final 
CharSequence suffix, final CharSequence... suffixes) {
-        return appendIfMissing(str, suffix, true, suffixes);
+        return Strings.CI.appendIfMissing(str, suffix, suffixes);
     }
 
     /**
@@ -818,9 +794,11 @@ public class StringUtils {
      * @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}
      * @since 3.5
+     * @deprecated Use {@link Strings#compare(String, String) 
Strings.CS.compare(String, String)}
      */
+    @Deprecated
     public static int compare(final String str1, final String str2) {
-        return compare(str1, str2, true);
+        return Strings.CS.compare(str1, str2);
     }
 
     /**
@@ -906,9 +884,11 @@ public class StringUtils {
      * @return &lt; 0, 0, &gt; 0, if {@code str1} is respectively less, equal 
ou greater than {@code str2},
      *          ignoring case differences.
      * @since 3.5
+     * @deprecated Use {@link Strings#compare(String, String) 
Strings.CI.compare(String, String)}
      */
+    @Deprecated
     public static int compareIgnoreCase(final String str1, final String str2) {
-        return compareIgnoreCase(str1, str2, true);
+        return Strings.CI.compare(str1, str2);
     }
 
     /**
@@ -984,12 +964,11 @@ public class StringUtils {
      *  false if not or {@code null} string input
      * @since 2.0
      * @since 3.0 Changed signature from contains(String, String) to 
contains(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#contains(CharSequence, CharSequence) 
Strings.CS.contains(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static boolean contains(final CharSequence seq, final CharSequence 
searchSeq) {
-        if (seq == null || searchSeq == null) {
-            return false;
-        }
-        return CharSequenceUtils.indexOf(seq, searchSeq, 0) >= 0;
+        return Strings.CS.contains(seq, searchSeq);
     }
 
     /**
@@ -1132,36 +1111,11 @@ public class StringUtils {
      *        null as well.
      * @return {@code true} if any of the search CharSequences are found, 
{@code false} otherwise
      * @since 3.4
+     * @deprecated Use {@link Strings#containsAny(CharSequence, 
CharSequence...) Strings.CS.containsAny(CharSequence, CharSequence...)}
      */
+    @Deprecated
     public static boolean containsAny(final CharSequence cs, final 
CharSequence... searchCharSequences) {
-        return containsAny(StringUtils::contains, cs, searchCharSequences);
-    }
-
-    /**
-     * 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
-     * @since 3.12.0
-     */
-    private static boolean containsAny(final ToBooleanBiFunction<CharSequence, 
CharSequence> test,
-        final CharSequence cs, final CharSequence... searchCharSequences) {
-        if (isEmpty(cs) || ArrayUtils.isEmpty(searchCharSequences)) {
-            return false;
-        }
-        for (final CharSequence searchCharSequence : searchCharSequences) {
-            if (test.applyAsBoolean(cs, searchCharSequence)) {
-                return true;
-            }
-        }
-        return false;
+        return Strings.CS.containsAny(cs, searchCharSequences);
     }
 
     /**
@@ -1189,9 +1143,11 @@ public class StringUtils {
      *        null as well.
      * @return {@code true} if any of the search CharSequences are found, 
{@code false} otherwise
      * @since 3.12.0
+     * @deprecated Use {@link Strings#containsAny(CharSequence, 
CharSequence...) Strings.CI.containsAny(CharSequence, CharSequence...)}
      */
+    @Deprecated
     public static boolean containsAnyIgnoreCase(final CharSequence cs, final 
CharSequence... searchCharSequences) {
-        return containsAny(StringUtils::containsIgnoreCase, cs, 
searchCharSequences);
+        return Strings.CI.containsAny(cs, searchCharSequences);
     }
 
     /**
@@ -1217,19 +1173,11 @@ public class StringUtils {
      * @return true if the CharSequence contains the search CharSequence 
irrespective of
      * case or false if not or {@code null} string input
      * @since 3.0 Changed signature from containsIgnoreCase(String, String) to 
containsIgnoreCase(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#contains(CharSequence, CharSequence) 
Strings.CI.contains(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static boolean containsIgnoreCase(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;
+        return Strings.CI.contains(str, searchStr);
     }
 
     /**
@@ -1736,31 +1684,11 @@ public class StringUtils {
      *  both {@code null}
      * @since 2.4
      * @since 3.0 Changed signature from endsWith(String, String) to 
endsWith(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#endsWith(CharSequence, CharSequence) 
Strings.CS.endsWith(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static boolean endsWith(final CharSequence str, final CharSequence 
suffix) {
-        return endsWith(str, suffix, false);
-    }
-
-    /**
-     * Tests if a CharSequence ends with a specified suffix (optionally 
case-insensitive).
-     *
-     * @see String#endsWith(String)
-     * @param str  the CharSequence to check, may be null
-     * @param suffix the suffix to find, may be null
-     * @param ignoreCase indicates whether the compare should ignore case
-     *  (case-insensitive) or not.
-     * @return {@code true} if the CharSequence starts with the prefix or
-     *  both {@code null}
-     */
-    private static boolean endsWith(final CharSequence str, final CharSequence 
suffix, final boolean ignoreCase) {
-        if (str == null || suffix == null) {
-            return str == suffix;
-        }
-        if (suffix.length() > str.length()) {
-            return false;
-        }
-        final int strOffset = str.length() - suffix.length();
-        return CharSequenceUtils.regionMatches(str, ignoreCase, strOffset, 
suffix, 0, suffix.length());
+        return Strings.CS.endsWith(str, suffix);
     }
 
     /**
@@ -1818,9 +1746,11 @@ public class StringUtils {
      *  both {@code null}
      * @since 2.4
      * @since 3.0 Changed signature from endsWithIgnoreCase(String, String) to 
endsWithIgnoreCase(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#endsWith(CharSequence, CharSequence) 
Strings.CS.endsWith(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static boolean endsWithIgnoreCase(final CharSequence str, final 
CharSequence suffix) {
-        return endsWith(str, suffix, true);
+        return Strings.CI.endsWith(str, suffix);
     }
 
     /**
@@ -1844,28 +1774,11 @@ public class StringUtils {
      * @since 3.0 Changed signature from equals(String, String) to 
equals(CharSequence, CharSequence)
      * @see Object#equals(Object)
      * @see #equalsIgnoreCase(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#equals(CharSequence, CharSequence) 
Strings.CS.equals(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static 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;
+        return Strings.CS.equals(cs1, cs2);
     }
 
     /**
@@ -1886,16 +1799,11 @@ public class StringUtils {
      * @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.
      * @since 3.5
+     * @deprecated Use {@link Strings#equalsAny(CharSequence, CharSequence...) 
Strings.CS.equalsAny(CharSequence, CharSequence...)}
      */
+    @Deprecated
     public static 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;
+        return Strings.CS.equalsAny(string, searchStrings);
     }
 
     /**
@@ -1916,16 +1824,11 @@ public class StringUtils {
      * @return {@code true} if the string is equal (case-insensitive) to any 
other element of {@code searchStrings};
      * {@code false} if {@code searchStrings} is null or contains no matches.
      * @since 3.5
+     * @deprecated Use {@link Strings#equalsAny(CharSequence, CharSequence...) 
Strings.CI-.equalsAny(CharSequence, CharSequence...)}
      */
+    @Deprecated
     public static boolean equalsAnyIgnoreCase(final CharSequence string, final 
CharSequence... searchStrings) {
-        if (ArrayUtils.isNotEmpty(searchStrings)) {
-            for (final CharSequence next : searchStrings) {
-                if (equalsIgnoreCase(string, next)) {
-                    return true;
-                }
-            }
-        }
-        return false;
+        return Strings.CI.equalsAny(string, searchStrings);
     }
 
     /**
@@ -1948,18 +1851,11 @@ public class StringUtils {
      * @return {@code true} if the CharSequences are equal (case-insensitive), 
or both {@code null}
      * @since 3.0 Changed signature from equalsIgnoreCase(String, String) to 
equalsIgnoreCase(CharSequence, CharSequence)
      * @see #equals(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#equals(CharSequence, CharSequence) 
Strings.CI.equals(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static boolean equalsIgnoreCase(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());
+        return Strings.CI.equals(cs1, cs2);
     }
 
     /**
@@ -2629,12 +2525,11 @@ public class StringUtils {
      *  -1 if no match or {@code null} string input
      * @since 2.0
      * @since 3.0 Changed signature from indexOf(String, String) to 
indexOf(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#indexOf(CharSequence, CharSequence) 
Strings.CS.indexOf(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static int indexOf(final CharSequence seq, final CharSequence 
searchSeq) {
-        if (seq == null || searchSeq == null) {
-            return INDEX_NOT_FOUND;
-        }
-        return CharSequenceUtils.indexOf(seq, searchSeq, 0);
+        return Strings.CS.indexOf(seq, searchSeq, 0);
     }
 
     /**
@@ -2669,12 +2564,11 @@ public class StringUtils {
      *  -1 if no match or {@code null} string input
      * @since 2.0
      * @since 3.0 Changed signature from indexOf(String, String, int) to 
indexOf(CharSequence, CharSequence, int)
+     * @deprecated Use {@link Strings#indexOf(CharSequence, CharSequence) 
Strings.CS.indexOf(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static int indexOf(final CharSequence seq, final CharSequence 
searchSeq, final int startPos) {
-        if (seq == null || searchSeq == null) {
-            return INDEX_NOT_FOUND;
-        }
-        return CharSequenceUtils.indexOf(seq, searchSeq, startPos);
+        return Strings.CS.indexOf(seq, searchSeq, startPos);
     }
 
     /**
@@ -3163,9 +3057,11 @@ public class StringUtils {
      *  -1 if no match or {@code null} string input
      * @since 2.5
      * @since 3.0 Changed signature from indexOfIgnoreCase(String, String) to 
indexOfIgnoreCase(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#indexOf(CharSequence, CharSequence) 
Strings.CI.indexOf(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static int indexOfIgnoreCase(final CharSequence str, final 
CharSequence searchStr) {
-        return indexOfIgnoreCase(str, searchStr, 0);
+        return Strings.CI.indexOf(str, searchStr, 0);
     }
 
     /**
@@ -3199,27 +3095,11 @@ public class StringUtils {
      *  -1 if no match or {@code null} string input
      * @since 2.5
      * @since 3.0 Changed signature from indexOfIgnoreCase(String, String, 
int) to indexOfIgnoreCase(CharSequence, CharSequence, int)
+     * @deprecated Use {@link Strings#indexOf(CharSequence, CharSequence, int) 
Strings.CI.indexOf(CharSequence, CharSequence, int)}
      */
+    @Deprecated
     public static int indexOfIgnoreCase(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;
+        return Strings.CI.indexOf(str, searchStr, startPos);
     }
 
     /**
@@ -4873,12 +4753,11 @@ public class StringUtils {
      *  -1 if no match or {@code null} string input
      * @since 2.0
      * @since 3.0 Changed signature from lastIndexOf(String, String) to 
lastIndexOf(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#lastIndexOf(CharSequence, CharSequence) 
Strings.CS.lastIndexOf(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static int lastIndexOf(final CharSequence seq, final CharSequence 
searchSeq) {
-        if (seq == null) {
-            return INDEX_NOT_FOUND;
-        }
-        return CharSequenceUtils.lastIndexOf(seq, searchSeq, seq.length());
+        return Strings.CS.lastIndexOf(seq, searchSeq);
     }
 
     /**
@@ -4915,9 +4794,11 @@ public class StringUtils {
      *  -1 if no match or {@code null} string input
      * @since 2.0
      * @since 3.0 Changed signature from lastIndexOf(String, String, int) to 
lastIndexOf(CharSequence, CharSequence, int)
+     * @deprecated Use {@link Strings#lastIndexOf(CharSequence, CharSequence, 
int) Strings.CS.lastIndexOf(CharSequence, CharSequence, int)}
      */
+    @Deprecated
     public static int lastIndexOf(final CharSequence seq, final CharSequence 
searchSeq, final int startPos) {
-        return CharSequenceUtils.lastIndexOf(seq, searchSeq, startPos);
+        return Strings.CS.lastIndexOf(seq, searchSeq, startPos);
     }
 
     /**
@@ -5078,12 +4959,11 @@ public class StringUtils {
      *  -1 if no match or {@code null} string input
      * @since 2.5
      * @since 3.0 Changed signature from lastIndexOfIgnoreCase(String, String) 
to lastIndexOfIgnoreCase(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#lastIndexOf(CharSequence, CharSequence) 
Strings.CI.lastIndexOf(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static int lastIndexOfIgnoreCase(final CharSequence str, final 
CharSequence searchStr) {
-        if (str == null || searchStr == null) {
-            return INDEX_NOT_FOUND;
-        }
-        return lastIndexOfIgnoreCase(str, searchStr, str.length());
+        return Strings.CI.lastIndexOf(str, searchStr);
     }
 
     /**
@@ -5117,29 +4997,11 @@ public class StringUtils {
      *  -1 if no match or {@code null} input
      * @since 2.5
      * @since 3.0 Changed signature from lastIndexOfIgnoreCase(String, String, 
int) to lastIndexOfIgnoreCase(CharSequence, CharSequence, int)
+     * @deprecated Use {@link Strings#lastIndexOf(CharSequence, CharSequence, 
int) Strings.CI.lastIndexOf(CharSequence, CharSequence, int)}
      */
+    @Deprecated
     public static int lastIndexOfIgnoreCase(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;
+        return Strings.CI.lastIndexOf(str, searchStr, startPos);
     }
 
     /**
@@ -5710,33 +5572,7 @@ public class StringUtils {
     }
 
     /**
-     * Prepends the prefix to the start of the string if the string does not
-     * already start with any of the prefixes.
-     *
-     * @param str The string.
-     * @param prefix The prefix to prepend to the start of the string.
-     * @param ignoreCase Indicates whether the compare should ignore case.
-     * @param prefixes Additional prefixes that are valid (optional).
-     *
-     * @return A new String if prefix was prepended, the same string otherwise.
-     */
-    private static String prependIfMissing(final String str, final 
CharSequence prefix, final boolean ignoreCase, final CharSequence... prefixes) {
-        if (str == null || isEmpty(prefix) || startsWith(str, prefix, 
ignoreCase)) {
-            return str;
-        }
-        if (ArrayUtils.isNotEmpty(prefixes)) {
-            for (final CharSequence p : prefixes) {
-                if (startsWith(str, p, ignoreCase)) {
-                    return str;
-                }
-            }
-        }
-        return prefix + str;
-    }
-
-    /**
-     * Prepends the prefix to the start of the string if the string does not
-     * already start with any of the prefixes.
+     * Prepends the prefix to the start of the string if the string does not 
already start with any of the prefixes.
      *
      * <pre>
      * StringUtils.prependIfMissing(null, null) = null
@@ -5746,7 +5582,10 @@ public class StringUtils {
      * StringUtils.prependIfMissing("xyzabc", "xyz") = "xyzabc"
      * StringUtils.prependIfMissing("XYZabc", "xyz") = "xyzXYZabc"
      * </pre>
-     * <p>With additional prefixes,</p>
+     * <p>
+     * With additional prefixes,
+     * </p>
+     *
      * <pre>
      * StringUtils.prependIfMissing(null, null, null) = null
      * StringUtils.prependIfMissing("abc", null, null) = "abc"
@@ -5760,16 +5599,17 @@ public class StringUtils {
      * StringUtils.prependIfMissing("MNOabc", "xyz", "mno") = "xyzMNOabc"
      * </pre>
      *
-     * @param str The string.
-     * @param prefix The prefix to prepend to the start of the string.
+     * @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.
-     *
      * @since 3.2
+     * @deprecated Use {@link Strings#prependIfMissing(String, CharSequence, 
CharSequence...) Strings.CS.prependIfMissing(String, CharSequence,
+     *             CharSequence...)}
      */
+    @Deprecated
     public static String prependIfMissing(final String str, final CharSequence 
prefix, final CharSequence... prefixes) {
-        return prependIfMissing(str, prefix, false, prefixes);
+        return Strings.CS.prependIfMissing(str, prefix, prefixes);
     }
 
     /**
@@ -5801,13 +5641,14 @@ public class StringUtils {
      * @param str The string.
      * @param prefix The prefix to prepend to the start of the string.
      * @param prefixes Additional prefixes that are valid (optional).
-     *
      * @return A new String if prefix was prepended, the same string otherwise.
-     *
      * @since 3.2
+     * @deprecated Use {@link Strings#prependIfMissing(String, CharSequence, 
CharSequence...) Strings.CI.prependIfMissing(String, CharSequence,
+     *             CharSequence...)}
      */
+    @Deprecated
     public static String prependIfMissingIgnoreCase(final String str, final 
CharSequence prefix, final CharSequence... prefixes) {
-        return prependIfMissing(str, prefix, true, prefixes);
+        return Strings.CI.prependIfMissing(str, prefix, prefixes);
     }
 
     /**
@@ -5946,15 +5787,11 @@ public class StringUtils {
      * @return the substring with the string removed if found,
      *  {@code null} if null String input
      * @since 2.1
+     * @deprecated Use {@link Strings#removeEnd(String, CharSequence) 
Strings.CS.removeEnd(String, CharSequence)}
      */
+    @Deprecated
     public static String removeEnd(final String str, final String remove) {
-        if (isEmpty(str) || isEmpty(remove)) {
-            return str;
-        }
-        if (str.endsWith(remove)) {
-            return str.substring(0, str.length() - remove.length());
-        }
-        return str;
+        return Strings.CS.removeEnd(str, remove);
     }
 
     /**
@@ -5982,15 +5819,11 @@ public class StringUtils {
      * @return the substring with the string removed if found,
      *  {@code null} if null String input
      * @since 2.4
+     * @deprecated Use {@link Strings#removeEnd(String, CharSequence) 
Strings.CI.removeEnd(String, CharSequence)}
      */
+    @Deprecated
     public static String removeEndIgnoreCase(final String str, final String 
remove) {
-        if (isEmpty(str) || isEmpty(remove)) {
-            return str;
-        }
-        if (endsWithIgnoreCase(str, remove)) {
-            return str.substring(0, str.length() - remove.length());
-        }
-        return str;
+        return Strings.CI.removeEnd(str, remove);
     }
 
     /**
@@ -6166,15 +5999,11 @@ public class StringUtils {
      * @return the substring with the string removed if found,
      *  {@code null} if null String input
      * @since 2.1
+     * @deprecated Use {@link Strings#removeStart(String, CharSequence) 
Strings.CS.removeStart(String, CharSequence)}
      */
+    @Deprecated
     public static String removeStart(final String str, final String remove) {
-        if (isEmpty(str) || isEmpty(remove)) {
-            return str;
-        }
-        if (str.startsWith(remove)) {
-            return str.substring(remove.length());
-        }
-        return str;
+        return Strings.CS.removeStart(str, remove);
     }
 
     /**
@@ -6201,12 +6030,11 @@ public class StringUtils {
      * @return the substring with the string removed if found,
      *  {@code null} if null String input
      * @since 2.4
+     * @deprecated Use {@link Strings#removeStart(String, CharSequence) 
Strings.CI.removeStart(String, CharSequence)}
      */
+    @Deprecated
     public static String removeStartIgnoreCase(final String str, final String 
remove) {
-        if (str != null && startsWithIgnoreCase(str, remove)) {
-            return str.substring(length(remove));
-        }
-        return str;
+        return Strings.CI.removeStart(str, remove);
     }
 
     /**
@@ -6345,9 +6173,11 @@ public class StringUtils {
      * @param replacement  the String to replace it with, may be null
      * @return the text with any replacements processed,
      *  {@code null} if null String input
+     * @deprecated Use {@link Strings#replace(String, String, String) 
Strings.CS.replace(String, String, String)}
      */
+    @Deprecated
     public static String replace(final String text, final String searchString, 
final String replacement) {
-        return replace(text, searchString, replacement, -1);
+        return Strings.CS.replace(text, searchString, replacement);
     }
 
     /**
@@ -6377,70 +6207,13 @@ public class StringUtils {
      * @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
+     * @deprecated Use {@link Strings#replace(String, String, String, int) 
Strings.CS.replace(String, String, String, int)}
      */
+    @Deprecated
     public static String replace(final String text, final String searchString, 
final String replacement, final int max) {
-        return replace(text, searchString, replacement, max, false);
+        return Strings.CS.replace(text, searchString, replacement, max);
     }
 
-    /**
-     * Replaces a String with another String inside a larger String,
-     * for the first {@code max} values of the search String,
-     * case-sensitively/insensitively based on {@code ignoreCase} value.
-     *
-     * <p>A {@code null} reference passed to this method is a no-op.</p>
-     *
-     * <pre>
-     * StringUtils.replace(null, *, *, *, false)         = null
-     * StringUtils.replace("", *, *, *, false)           = ""
-     * StringUtils.replace("any", null, *, *, false)     = "any"
-     * StringUtils.replace("any", *, null, *, false)     = "any"
-     * StringUtils.replace("any", "", *, *, false)       = "any"
-     * StringUtils.replace("any", *, *, 0, false)        = "any"
-     * StringUtils.replace("abaa", "a", null, -1, false) = "abaa"
-     * StringUtils.replace("abaa", "a", "", -1, false)   = "b"
-     * StringUtils.replace("abaa", "a", "z", 0, false)   = "abaa"
-     * StringUtils.replace("abaa", "A", "z", 1, false)   = "abaa"
-     * StringUtils.replace("abaa", "A", "z", 1, true)   = "zbaa"
-     * StringUtils.replace("abAa", "a", "z", 2, true)   = "zbza"
-     * StringUtils.replace("abAa", "a", "z", -1, true)  = "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
-     * @param ignoreCase if true replace is case-insensitive, otherwise 
case-sensitive
-     * @return the text with any replacements processed,
-     *  {@code null} if null String input
-     */
-     private static String replace(final String text, String searchString, 
final String replacement, int max, final boolean ignoreCase) {
-         if (isEmpty(text) || isEmpty(searchString) || replacement == null || 
max == 0) {
-             return text;
-         }
-         if (ignoreCase) {
-             searchString = searchString.toLowerCase();
-         }
-         int start = 0;
-         int end = ignoreCase ? indexOfIgnoreCase(text, searchString, start) : 
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 = ignoreCase ? indexOfIgnoreCase(text, searchString, start) : 
indexOf(text, searchString, start);
-         }
-         buf.append(text, start, text.length());
-         return buf.toString();
-     }
-
     /**
      * Replaces each substring of the text String that matches the given 
regular expression
      * with the given replacement.
@@ -6922,9 +6695,11 @@ public class StringUtils {
      * @return the text with any replacements processed,
      *  {@code null} if null String input
      * @since 3.5
+     * @deprecated Use {@link Strings#replace(String, String, String) 
Strings.CI.replace(String, String, String)}
      */
+    @Deprecated
      public static String replaceIgnoreCase(final String text, final String 
searchString, final String replacement) {
-         return replaceIgnoreCase(text, searchString, replacement, -1);
+         return Strings.CI.replace(text, searchString, replacement);
      }
 
     /**
@@ -6955,9 +6730,11 @@ public class StringUtils {
      * @return the text with any replacements processed,
      *  {@code null} if null String input
      * @since 3.5
+     * @deprecated Use {@link Strings#replace(String, String, String, int) 
Strings.CI.replace(String, String, String, int)}
      */
+    @Deprecated
     public static String replaceIgnoreCase(final String text, final String 
searchString, final String replacement, final int max) {
-        return replace(text, searchString, replacement, max, true);
+        return Strings.CI.replace(text, searchString, replacement, max);
     }
 
     /**
@@ -6982,9 +6759,11 @@ public class StringUtils {
      * @param replacement  the String to replace with, may be null
      * @return the text with any replacements processed,
      *  {@code null} if null String input
+     * @deprecated Use {@link Strings#replaceOnce(String, String, String) 
Strings.CS.replaceOnce(String, String, String)}
      */
+    @Deprecated
     public static String replaceOnce(final String text, final String 
searchString, final String replacement) {
-        return replace(text, searchString, replacement, 1);
+        return Strings.CS.replaceOnce(text, searchString, replacement);
     }
 
     /**
@@ -7011,9 +6790,11 @@ public class StringUtils {
      * @return the text with any replacements processed,
      *  {@code null} if null String input
      * @since 3.5
+     * @deprecated Use {@link Strings#replaceOnce(String, String, String) 
Strings.CI.replaceOnce(String, String, String)}
      */
+    @Deprecated
     public static String replaceOnceIgnoreCase(final String text, final String 
searchString, final String replacement) {
-        return replaceIgnoreCase(text, searchString, replacement, 1);
+        return Strings.CI.replaceOnce(text, searchString, replacement);
     }
 
     /**
@@ -8018,32 +7799,11 @@ public class StringUtils {
      *  both {@code null}
      * @since 2.4
      * @since 3.0 Changed signature from startsWith(String, String) to 
startsWith(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#startsWith(CharSequence, CharSequence) 
Strings.CS.startsWith(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static boolean startsWith(final CharSequence str, final 
CharSequence prefix) {
-        return startsWith(str, prefix, false);
-    }
-
-    /**
-     * Check if a CharSequence starts with a specified prefix (optionally 
case-insensitive).
-     *
-     * @see String#startsWith(String)
-     * @param str  the CharSequence to check, may be null
-     * @param prefix the prefix to find, may be null
-     * @param ignoreCase indicates whether the compare should ignore case
-     *  (case-insensitive) or not.
-     * @return {@code true} if the CharSequence starts with the prefix or
-     *  both {@code null}
-     */
-    private static boolean startsWith(final CharSequence str, final 
CharSequence prefix, final boolean ignoreCase) {
-        if (str == null || prefix == null) {
-            return str == prefix;
-        }
-        // Get length once instead of twice in the unlikely case that it 
changes.
-        final int preLen = prefix.length();
-        if (preLen > str.length()) {
-            return false;
-        }
-        return CharSequenceUtils.regionMatches(str, ignoreCase, 0, prefix, 0, 
preLen);
+        return Strings.CS.startsWith(str, prefix);
     }
 
     /**
@@ -8101,9 +7861,11 @@ public class StringUtils {
      *  both {@code null}
      * @since 2.4
      * @since 3.0 Changed signature from startsWithIgnoreCase(String, String) 
to startsWithIgnoreCase(CharSequence, CharSequence)
+     * @deprecated Use {@link Strings#startsWith(CharSequence, CharSequence) 
Strings.CI.startsWith(CharSequence, CharSequence)}
      */
+    @Deprecated
     public static boolean startsWithIgnoreCase(final CharSequence str, final 
CharSequence prefix) {
-        return startsWith(str, prefix, true);
+        return Strings.CI.startsWith(str, prefix);
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/lang3/Strings.java 
b/src/main/java/org/apache/commons/lang3/Strings.java
new file mode 100644
index 000000000..c5614ba9d
--- /dev/null
+++ b/src/main/java/org/apache/commons/lang3/Strings.java
@@ -0,0 +1,976 @@
+/*
+ * 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
+ *
+ *      http://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-senstive {@link #CS} vs. 
case-insensitive {@link #CI} through a singleton instance.
+ *
+ * @see CharSequenceUtils
+ * @see StringUtils
+ * @since 3.18.0
+ */
+public abstract class Strings {
+
+    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;
+
+        @Override
+        public Strings get() {
+            return ignoreCase ? new CiStrings(ignoreCase) : new 
CsStrings(ignoreCase);
+        }
+
+        protected boolean isIgnoreCase() {
+            return ignoreCase;
+        }
+
+        protected boolean isNullIsLess() {
+            return nullIsLess;
+        }
+
+        public Builder setIgnoreCase(final boolean ignoreCase) {
+            this.ignoreCase = ignoreCase;
+            return asThis();
+        }
+
+        public Builder setNullIsLess(final boolean nullIsLess) {
+            this.nullIsLess = nullIsLess;
+            return asThis();
+        }
+
+    }
+
+    /**
+     * Case-insentive 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);
+        }
+
+        /**
+         * Tests if CharSequence contains a search CharSequence irrespective 
of case, handling {@code null}. Case-insensitivity is defined as by
+         * {@link String#equalsIgnoreCase(String)}.
+         *
+         * <p>
+         * A {@code null} CharSequence will return {@code false}.
+         * </p>
+         *
+         * <pre>
+         * StringUtils.containsIgnoreCase(null, *)    = false
+         * StringUtils.containsIgnoreCase(*, null)    = false
+         * StringUtils.containsIgnoreCase("", "")     = true
+         * StringUtils.containsIgnoreCase("abc", "")  = true
+         * StringUtils.containsIgnoreCase("abc", "a") = true
+         * StringUtils.containsIgnoreCase("abc", "z") = false
+         * StringUtils.containsIgnoreCase("abc", "A") = true
+         * StringUtils.containsIgnoreCase("abc", "Z") = false
+         * </pre>
+         *
+         * @param str       the CharSequence to check, may be null
+         * @param searchStr the CharSequence to find, may be null
+         * @return true if the CharSequence contains the search CharSequence 
irrespective of case or false if not or {@code null} string input
+         */
+        @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.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;
+        }
+
+        /**
+         * Case in-sensitive find of the last index within a CharSequence from 
the specified position.
+         *
+         * <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>
+         *
+         * <pre>
+         * StringUtils.lastIndexOfIgnoreCase(null, *, *)          = -1
+         * StringUtils.lastIndexOfIgnoreCase(*, null, *)          = -1
+         * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 8)  = 7
+         * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 8)  = 5
+         * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "AB", 8) = 4
+         * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 9)  = 5
+         * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", -1) = -1
+         * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "A", 0)  = 0
+         * StringUtils.lastIndexOfIgnoreCase("aabaabaa", "B", 0)  = -1
+         * </pre>
+         *
+         * @param str       the CharSequence to check, may be null
+         * @param searchStr the CharSequence to find, may be null
+         * @param startPos  the start position
+         * @return the last index of the search CharSequence (always &le; 
startPos), -1 if no match or {@code null} input
+         */
+        @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 s1.equals(s2);
+        }
+
+        @Override
+        public int indexOf(final CharSequence seq, final CharSequence 
searchSeq, final int startPos) {
+            return CharSequenceUtils.indexOf(seq, searchSeq, startPos);
+        }
+
+        /**
+         * 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>
+         *
+         * <pre>
+         * StringUtils.lastIndexOf(null, *, *)          = -1
+         * StringUtils.lastIndexOf(*, null, *)          = -1
+         * StringUtils.lastIndexOf("aabaabaa", "a", 8)  = 7
+         * StringUtils.lastIndexOf("aabaabaa", "b", 8)  = 5
+         * StringUtils.lastIndexOf("aabaabaa", "ab", 8) = 4
+         * StringUtils.lastIndexOf("aabaabaa", "b", 9)  = 5
+         * StringUtils.lastIndexOf("aabaabaa", "b", -1) = -1
+         * StringUtils.lastIndexOf("aabaabaa", "a", 0)  = 0
+         * StringUtils.lastIndexOf("aabaabaa", "b", 0)  = -1
+         * StringUtils.lastIndexOf("aabaabaa", "b", 1)  = -1
+         * StringUtils.lastIndexOf("aabaabaa", "b", 2)  = 2
+         * StringUtils.lastIndexOf("aabaabaa", "ba", 2)  = 2
+         * </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
+         */
+        @Override
+        public int lastIndexOf(final CharSequence seq, final CharSequence 
searchSeq, final int startPos) {
+            return CharSequenceUtils.lastIndexOf(seq, searchSeq, startPos);
+        }
+
+    }
+
+    /**
+     * The <b>C</b>ase-<b>I</b>nsensitive singleton instance.
+     */
+    public static final Strings CI = new CiStrings(true);
+
+    /**
+     * The <b>C</b>ase-<b>S</b>nsensitive singleton instance.
+     */
+    public static final Strings CS = new CsStrings(true);
+
+    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;
+    }
+
+    /**
+     * Ignores case when possible.
+     */
+    private final boolean ignoreCase;
+
+    /**
+     * Compares null as less when possible.
+     */
+    private final boolean nullIsLess;
+
+    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;
+    }
+
+    public abstract int compare(String s1, String s2);
+
+    /**
+     * 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>
+     *
+     * <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>
+     *
+     * @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, ignoring case.
+     *
+     * <p>
+     * A {@code null} {@code cs} CharSequence will return {@code false}. A 
{@code null} or zero length search array will return {@code false}.
+     * </p>
+     *
+     * <pre>
+     * StringUtils.containsAny(null, *)            = false
+     * StringUtils.containsAny("", *)              = false
+     * StringUtils.containsAny(*, null)            = false
+     * StringUtils.containsAny(*, [])              = false
+     * StringUtils.containsAny("abcd", "ab", null) = true
+     * StringUtils.containsAny("abcd", "ab", "cd") = true
+     * StringUtils.containsAny("abc", "d", "abc")  = true
+     * StringUtils.containsAny("abc", "D", "ABC")  = true
+     * StringUtils.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 (optionally 
case-insensitive).
+     *
+     * <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);
+    }
+
+    public abstract boolean equals(CharSequence s1, CharSequence s2);
+
+    /**
+     * Compares this string to the specified object. The result is {@code
+     * true} if and only if the argument is not {@code null} and is a {@code
+     * String} object that represents the same sequence of characters as this 
object.
+     *
+     * @param s1 The left string to compare this {@code String} against
+     * @param s2 The right string to compare this {@code String} against
+     *
+     * @return {@code true} if the given object represents a {@code String} 
equivalent to this string, {@code false} otherwise
+     *
+     * @see String#compareTo(String)
+     * @see String#equalsIgnoreCase(String)
+     */
+    public abstract boolean equals(String s1, String s2);
+
+    /**
+     * 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}.
+     *
+     * <pre>
+     * StringUtils.equalsAny(null, (CharSequence[]) null) = false
+     * StringUtils.equalsAny(null, null, null)    = true
+     * StringUtils.equalsAny(null, "abc", "def")  = false
+     * StringUtils.equalsAny("abc", null, "def")  = false
+     * StringUtils.equalsAny("abc", "abc", "def") = true
+     * StringUtils.equalsAny("abc", "ABC", "DEF") = false
+     * </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;
+    }
+
+    public int indexOf(final CharSequence str, final CharSequence searchStr) {
+        return indexOf(str, searchStr, 0);
+    }
+
+    public abstract int indexOf(CharSequence str, CharSequence searchStr, int 
startPos);
+
+    public boolean isCaseSensitive() {
+        return !ignoreCase;
+    }
+
+    boolean isIgnoreCase() {
+        return ignoreCase;
+    }
+
+    boolean isNullIsLess() {
+        return nullIsLess;
+    }
+
+    public int lastIndexOf(final CharSequence str, final CharSequence 
searchStr) {
+        if (str == null) {
+            return INDEX_NOT_FOUND;
+        }
+        return lastIndexOf(str, searchStr, str.length());
+    }
+
+    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>
+     * StringUtils.prependIfMissing(null, null) = null
+     * StringUtils.prependIfMissing("abc", null) = "abc"
+     * StringUtils.prependIfMissing("", "xyz") = "xyz"
+     * StringUtils.prependIfMissing("abc", "xyz") = "xyzabc"
+     * StringUtils.prependIfMissing("xyzabc", "xyz") = "xyzabc"
+     * StringUtils.prependIfMissing("XYZabc", "xyz") = "xyzXYZabc"
+     * </pre>
+     * <p>
+     * With additional prefixes,
+     * </p>
+     *
+     * <pre>
+     * StringUtils.prependIfMissing(null, null, null) = null
+     * StringUtils.prependIfMissing("abc", null, null) = "abc"
+     * StringUtils.prependIfMissing("", "xyz", null) = "xyz"
+     * StringUtils.prependIfMissing("abc", "xyz", new CharSequence[]{null}) = 
"xyzabc"
+     * StringUtils.prependIfMissing("abc", "xyz", "") = "abc"
+     * StringUtils.prependIfMissing("abc", "xyz", "mno") = "xyzabc"
+     * StringUtils.prependIfMissing("xyzabc", "xyz", "mno") = "xyzabc"
+     * StringUtils.prependIfMissing("mnoabc", "xyz", "mno") = "mnoabc"
+     * StringUtils.prependIfMissing("XYZabc", "xyz", "mno") = "xyzXYZabc"
+     * StringUtils.prependIfMissing("MNOabc", "xyz", "mno") = "xyzMNOabc"
+     * </pre>
+     *
+     * <p>
+     * Case-insensitive examples
+     * </p>
+     *
+     * <pre>
+     * StringUtils.prependIfMissingIgnoreCase(null, null) = null
+     * StringUtils.prependIfMissingIgnoreCase("abc", null) = "abc"
+     * StringUtils.prependIfMissingIgnoreCase("", "xyz") = "xyz"
+     * StringUtils.prependIfMissingIgnoreCase("abc", "xyz") = "xyzabc"
+     * StringUtils.prependIfMissingIgnoreCase("xyzabc", "xyz") = "xyzabc"
+     * StringUtils.prependIfMissingIgnoreCase("XYZabc", "xyz") = "XYZabc"
+     * </pre>
+     * <p>
+     * With additional prefixes,
+     * </p>
+     *
+     * <pre>
+     * StringUtils.prependIfMissingIgnoreCase(null, null, null) = null
+     * StringUtils.prependIfMissingIgnoreCase("abc", null, null) = "abc"
+     * StringUtils.prependIfMissingIgnoreCase("", "xyz", null) = "xyz"
+     * StringUtils.prependIfMissingIgnoreCase("abc", "xyz", new 
CharSequence[]{null}) = "xyzabc"
+     * StringUtils.prependIfMissingIgnoreCase("abc", "xyz", "") = "abc"
+     * StringUtils.prependIfMissingIgnoreCase("abc", "xyz", "mno") = "xyzabc"
+     * StringUtils.prependIfMissingIgnoreCase("xyzabc", "xyz", "mno") = 
"xyzabc"
+     * StringUtils.prependIfMissingIgnoreCase("mnoabc", "xyz", "mno") = 
"mnoabc"
+     * StringUtils.prependIfMissingIgnoreCase("XYZabc", "xyz", "mno") = 
"XYZabc"
+     * StringUtils.prependIfMissingIgnoreCase("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;
+    }
+
+    /**
+     * 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>
+     * StringUtils.removeEnd(null, *)      = null
+     * StringUtils.removeEnd("", *)        = ""
+     * StringUtils.removeEnd(*, null)      = *
+     * StringUtils.removeEnd("www.domain.com", ".com.")  = "www.domain.com"
+     * StringUtils.removeEnd("www.domain.com", ".com")   = "www.domain"
+     * StringUtils.removeEnd("www.domain.com", "domain") = "www.domain.com"
+     * StringUtils.removeEnd("abc", "")    = "abc"
+     * </pre>
+     * <p>
+     * Case-insensitive examples
+     * </p>
+     *
+     * <pre>
+     * StringUtils.removeEndIgnoreCase(null, *)      = null
+     * StringUtils.removeEndIgnoreCase("", *)        = ""
+     * StringUtils.removeEndIgnoreCase(*, null)      = *
+     * StringUtils.removeEndIgnoreCase("www.domain.com", ".com.")  = 
"www.domain.com"
+     * StringUtils.removeEndIgnoreCase("www.domain.com", ".com")   = 
"www.domain"
+     * StringUtils.removeEndIgnoreCase("www.domain.com", "domain") = 
"www.domain.com"
+     * StringUtils.removeEndIgnoreCase("abc", "")    = "abc"
+     * StringUtils.removeEndIgnoreCase("www.domain.com", ".COM") = 
"www.domain")
+     * StringUtils.removeEndIgnoreCase("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-insensitive examples
+     * </p>
+     *
+     * <pre>
+     * StringUtils.removeStartIgnoreCase(null, *)      = null
+     * StringUtils.removeStartIgnoreCase("", *)        = ""
+     * StringUtils.removeStartIgnoreCase(*, null)      = *
+     * StringUtils.removeStartIgnoreCase("www.domain.com", "www.")   = 
"domain.com"
+     * StringUtils.removeStartIgnoreCase("www.domain.com", "WWW.")   = 
"domain.com"
+     * StringUtils.removeStartIgnoreCase("domain.com", "www.")       = 
"domain.com"
+     * StringUtils.removeStartIgnoreCase("www.domain.com", "domain") = 
"www.domain.com"
+     * StringUtils.removeStartIgnoreCase("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>
+     * StringUtils.replace(null, *, *)        = null
+     * StringUtils.replace("", *, *)          = ""
+     * StringUtils.replace("any", null, *)    = "any"
+     * StringUtils.replace("any", *, null)    = "any"
+     * StringUtils.replace("any", "", *)      = "any"
+     * StringUtils.replace("aba", "a", null)  = "aba"
+     * StringUtils.replace("aba", "a", "")    = "b"
+     * StringUtils.replace("aba", "a", "z")   = "zbz"
+     * </pre>
+     * <p>
+     * Case-insensitive examples
+     * </p>
+     *
+     * <pre>
+     * StringUtils.replaceIgnoreCase(null, *, *)        = null
+     * StringUtils.replaceIgnoreCase("", *, *)          = ""
+     * StringUtils.replaceIgnoreCase("any", null, *)    = "any"
+     * StringUtils.replaceIgnoreCase("any", *, null)    = "any"
+     * StringUtils.replaceIgnoreCase("any", "", *)      = "any"
+     * StringUtils.replaceIgnoreCase("aba", "a", null)  = "aba"
+     * StringUtils.replaceIgnoreCase("abA", "A", "")    = "b"
+     * StringUtils.replaceIgnoreCase("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, case-sensitively/insensitively 
based
+     * on {@code ignoreCase} value.
+     *
+     * <p>
+     * A {@code null} reference passed to this method is a no-op.
+     * </p>
+     *
+     * <pre>
+     * StringUtils.replace(null, *, *, *, false)         = null
+     * StringUtils.replace("", *, *, *, false)           = ""
+     * StringUtils.replace("any", null, *, *, false)     = "any"
+     * StringUtils.replace("any", *, null, *, false)     = "any"
+     * StringUtils.replace("any", "", *, *, false)       = "any"
+     * StringUtils.replace("any", *, *, 0, false)        = "any"
+     * StringUtils.replace("abaa", "a", null, -1, false) = "abaa"
+     * StringUtils.replace("abaa", "a", "", -1, false)   = "b"
+     * StringUtils.replace("abaa", "a", "z", 0, false)   = "abaa"
+     * StringUtils.replace("abaa", "A", "z", 1, false)   = "abaa"
+     * StringUtils.replace("abaa", "A", "z", 1, true)   = "zbaa"
+     * StringUtils.replace("abAa", "a", "z", 2, true)   = "zbza"
+     * StringUtils.replace("abAa", "a", "z", -1, true)  = "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>
+     * StringUtils.replaceOnce(null, *, *)        = null
+     * StringUtils.replaceOnce("", *, *)          = ""
+     * StringUtils.replaceOnce("any", null, *)    = "any"
+     * StringUtils.replaceOnce("any", *, null)    = "any"
+     * StringUtils.replaceOnce("any", "", *)      = "any"
+     * StringUtils.replaceOnce("aba", "a", null)  = "aba"
+     * StringUtils.replaceOnce("aba", "a", "")    = "ba"
+     * StringUtils.replaceOnce("aba", "a", "z")   = "zba"
+     * </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>
+     * StringUtils.startsWith(null, null)      = true
+     * StringUtils.startsWith(null, "abc")     = false
+     * StringUtils.startsWith("abcdef", null)  = false
+     * StringUtils.startsWith("abcdef", "abc") = true
+     * StringUtils.startsWith("ABCDEF", "abc") = false
+     * </pre>
+     *
+     * <p>
+     * Case-insensitive examples
+     * </p>
+     *
+     * <pre>
+     * StringUtils.startsWithIgnoreCase(null, null)      = true
+     * StringUtils.startsWithIgnoreCase(null, "abc")     = false
+     * StringUtils.startsWithIgnoreCase("abcdef", null)  = false
+     * StringUtils.startsWithIgnoreCase("abcdef", "abc") = true
+     * StringUtils.startsWithIgnoreCase("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);
+    }
+}
diff --git a/src/main/java/org/apache/commons/lang3/builder/ToStringStyle.java 
b/src/main/java/org/apache/commons/lang3/builder/ToStringStyle.java
index df7fff831..fe2570135 100644
--- a/src/main/java/org/apache/commons/lang3/builder/ToStringStyle.java
+++ b/src/main/java/org/apache/commons/lang3/builder/ToStringStyle.java
@@ -28,6 +28,7 @@ import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringEscapeUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Strings;
 
 /**
  * Controls {@link String} formatting for {@link ToStringBuilder}.
@@ -2270,7 +2271,7 @@ public abstract class ToStringStyle implements 
Serializable {
      * @since 2.0
      */
     protected void removeLastFieldSeparator(final StringBuffer buffer) {
-        if (StringUtils.endsWith(buffer, fieldSeparator)) {
+        if (Strings.CS.endsWith(buffer, fieldSeparator)) {
             buffer.setLength(buffer.length() - fieldSeparator.length());
         }
     }
diff --git 
a/src/main/java/org/apache/commons/lang3/exception/DefaultExceptionContext.java 
b/src/main/java/org/apache/commons/lang3/exception/DefaultExceptionContext.java
index c98d3d4b2..cb9f182fa 100644
--- 
a/src/main/java/org/apache/commons/lang3/exception/DefaultExceptionContext.java
+++ 
b/src/main/java/org/apache/commons/lang3/exception/DefaultExceptionContext.java
@@ -24,7 +24,7 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Strings;
 import org.apache.commons.lang3.tuple.ImmutablePair;
 import org.apache.commons.lang3.tuple.Pair;
 
@@ -84,7 +84,7 @@ public class DefaultExceptionContext implements 
ExceptionContext, Serializable {
      */
     @Override
     public List<Object> getContextValues(final String label) {
-        return stream().filter(pair -> StringUtils.equals(label, 
pair.getKey())).map(Pair::getValue).collect(Collectors.toList());
+        return stream().filter(pair -> Strings.CS.equals(label, 
pair.getKey())).map(Pair::getValue).collect(Collectors.toList());
     }
 
     /**
@@ -92,7 +92,7 @@ public class DefaultExceptionContext implements 
ExceptionContext, Serializable {
      */
     @Override
     public Object getFirstContextValue(final String label) {
-        return stream().filter(pair -> StringUtils.equals(label, 
pair.getKey())).findFirst().map(Pair::getValue).orElse(null);
+        return stream().filter(pair -> Strings.CS.equals(label, 
pair.getKey())).findFirst().map(Pair::getValue).orElse(null);
     }
 
     /**
@@ -138,7 +138,7 @@ public class DefaultExceptionContext implements 
ExceptionContext, Serializable {
      */
     @Override
     public DefaultExceptionContext setContextValue(final String label, final 
Object value) {
-        contextValues.removeIf(p -> StringUtils.equals(label, p.getKey()));
+        contextValues.removeIf(p -> Strings.CS.equals(label, p.getKey()));
         addContextValue(label, value);
         return this;
     }
diff --git a/src/main/java/org/apache/commons/lang3/text/StrBuilder.java 
b/src/main/java/org/apache/commons/lang3/text/StrBuilder.java
index d700b4b36..6f907a8f1 100644
--- a/src/main/java/org/apache/commons/lang3/text/StrBuilder.java
+++ b/src/main/java/org/apache/commons/lang3/text/StrBuilder.java
@@ -29,6 +29,7 @@ import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.CharUtils;
 import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Strings;
 import org.apache.commons.lang3.builder.Builder;
 
 /**
@@ -2357,7 +2358,7 @@ public class StrBuilder implements CharSequence, 
Appendable, Serializable, Build
      * @return the last index of the string, or -1 if not found
      */
     public int lastIndexOf(final String str, final int startIndex) {
-        return StringUtils.lastIndexOf(this, str, startIndex);
+        return Strings.CS.lastIndexOf(this, str, startIndex);
     }
 
     /**
diff --git a/src/test/java/org/apache/commons/lang3/StringsTest.java 
b/src/test/java/org/apache/commons/lang3/StringsTest.java
new file mode 100644
index 000000000..1bb29ddc0
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/StringsTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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
+ *
+ *      http://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.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link Strings}.
+ */
+public class StringsTest {
+
+    @Test
+    public void testBuilder() {
+        
assertTrue(Strings.builder().setIgnoreCase(false).get().isCaseSensitive());
+        
assertFalse(Strings.builder().setIgnoreCase(true).get().isCaseSensitive());
+        //
+        
assertTrue(Strings.builder().setNullIsLess(false).get().isCaseSensitive());
+        
assertTrue(Strings.builder().setNullIsLess(true).get().isCaseSensitive());
+    }
+
+    @Test
+    public void testBuilderDefaults() {
+        final Strings strings = Strings.builder().get();
+        assertTrue(strings.isCaseSensitive());
+    }
+
+    @Test
+    public void testCaseInsensitiveConstant() {
+        assertNotNull(Strings.CI);
+        assertFalse(Strings.CI.isCaseSensitive());
+    }
+
+    @Test
+    public void testCaseSensitiveConstant() {
+        assertNotNull(Strings.CS);
+        assertTrue(Strings.CS.isCaseSensitive());
+    }
+}


Reply via email to