
software.amazon.awssdk.utils.StringUtils Maven / Gradle / Ivy
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.utils;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.util.Locale;
import software.amazon.awssdk.annotations.SdkProtectedApi;
/**
* Operations on {@link java.lang.String} that are
* {@code null} safe.
*
*
* - IsEmpty/IsBlank
* - checks if a String contains text
* - Trim/Strip
* - removes leading and trailing whitespace
* - Equals/Compare
* - compares two strings null-safe
* - startsWith
* - check if a String starts with a prefix null-safe
* - endsWith
* - check if a String ends with a suffix null-safe
* - IndexOf/LastIndexOf/Contains
* - null-safe index-of checks
*
- IndexOfAny/LastIndexOfAny/IndexOfAnyBut/LastIndexOfAnyBut
* - index-of any of a set of Strings
* - ContainsOnly/ContainsNone/ContainsAny
* - does String contains only/none/any of these characters
* - Substring/Left/Right/Mid
* - null-safe substring extractions
* - SubstringBefore/SubstringAfter/SubstringBetween
* - substring extraction relative to other strings
* - Split/Join
* - splits a String into an array of substrings and vice versa
* - Remove/Delete
* - removes part of a String
* - Replace/Overlay
* - Searches a String and replaces one String with another
* - Chomp/Chop
* - removes the last part of a String
* - AppendIfMissing
* - appends a suffix to the end of the String if not present
* - PrependIfMissing
* - prepends a prefix to the start of the String if not present
* - LeftPad/RightPad/Center/Repeat
* - pads a String
* - UpperCase/LowerCase/SwapCase/Capitalize/Uncapitalize
* - changes the case of a String
* - CountMatches
* - counts the number of occurrences of one String in another
* - IsAlpha/IsNumeric/IsWhitespace/IsAsciiPrintable
* - checks the characters in a String
* - DefaultString
* - protects against a null input String
* - Rotate
* - rotate (circular shift) a String
* - Reverse/ReverseDelimited
* - reverses a String
* - Abbreviate
* - abbreviates a string using ellipsis or another given String
* - Difference
* - compares Strings and reports on their differences
* - LevenshteinDistance
* - the number of changes needed to change one String into another
*
*
* The {@code StringUtils} class defines certain words related to
* String handling.
*
*
* - null - {@code null}
* - empty - a zero-length string ({@code ""})
* - space - the space character ({@code ' '}, char 32)
* - whitespace - the characters defined by {@link Character#isWhitespace(char)}
* - trim - the characters <= 32 as in {@link String#trim()}
*
*
* {@code StringUtils} handles {@code null} input Strings quietly.
* That is to say that a {@code null} input will return {@code null}.
* Where a {@code boolean} or {@code int} is being returned
* details vary by method.
*
* A side effect of the {@code null} handling is that a
* {@code NullPointerException} should be considered a bug in
* {@code StringUtils}.
*
* This class's source was modified from the Apache commons-lang library: https://github.com/apache/commons-lang/
*
* #ThreadSafe#
* @see java.lang.String
*/
@SdkProtectedApi
public final class StringUtils {
// Performance testing notes (JDK 1.4, Jul03, scolebourne)
// Whitespace:
// Character.isWhitespace() is faster than WHITESPACE.indexOf()
// where WHITESPACE is a string of all whitespace characters
//
// Character access:
// String.charAt(n) versus toCharArray(), then array[n]
// String.charAt(n) is about 15% worse for a 10K string
// They are about equal for a length 50 string
// String.charAt(n) is about 4 times better for a length 3 string
// String.charAt(n) is best bet overall
//
// Append:
// String.concat about twice as fast as StringBuffer.append
// (not sure who tested this)
/**
* The empty String {@code ""}.
*/
private static final String EMPTY = "";
/**
* {@code StringUtils} instances should NOT be constructed in
* standard programming. Instead, the class should be used as
* {@code StringUtils.trim(" foo ");}.
*/
private StringUtils() {
}
// Empty checks
//-----------------------------------------------------------------------
/**
* Checks if a CharSequence is empty ("") or null.
*
*
* StringUtils.isEmpty(null) = true
* StringUtils.isEmpty("") = true
* StringUtils.isEmpty(" ") = false
* StringUtils.isEmpty("bob") = false
* StringUtils.isEmpty(" bob ") = false
*
*
* NOTE: This method changed in Lang version 2.0.
* It no longer trims the CharSequence.
* That functionality is available in isBlank().
*
* @param cs the CharSequence to check, may be null
* @return {@code true} if the CharSequence is empty or null
* @since 3.0 Changed signature from isEmpty(String) to isEmpty(CharSequence)
*/
public static boolean isEmpty(final CharSequence cs) {
return cs == null || cs.length() == 0;
}
/**
* Checks if a CharSequence is empty (""), null or whitespace only.
*
* Whitespace is defined by {@link Character#isWhitespace(char)}.
*
*
* StringUtils.isBlank(null) = true
* StringUtils.isBlank("") = true
* StringUtils.isBlank(" ") = true
* StringUtils.isBlank("bob") = false
* StringUtils.isBlank(" bob ") = false
*
*
* @param cs the CharSequence to check, may be null
* @return {@code true} if the CharSequence is null, empty or whitespace only
* @since 2.0
* @since 3.0 Changed signature from isBlank(String) to isBlank(CharSequence)
*/
public static boolean isBlank(final CharSequence cs) {
if (cs == null || cs.length() == 0) {
return true;
}
for (int i = 0; i < cs.length(); i++) {
if (!Character.isWhitespace(cs.charAt(i))) {
return false;
}
}
return true;
}
/**
* Checks if a CharSequence is not empty (""), not null and not whitespace only.
*
* Whitespace is defined by {@link Character#isWhitespace(char)}.
*
*
* StringUtils.isNotBlank(null) = false
* StringUtils.isNotBlank("") = false
* StringUtils.isNotBlank(" ") = false
* StringUtils.isNotBlank("bob") = true
* StringUtils.isNotBlank(" bob ") = true
*
*
* @param cs the CharSequence to check, may be null
* @return {@code true} if the CharSequence is not empty and not null and not whitespace only
* @since 2.0
* @since 3.0 Changed signature from isNotBlank(String) to isNotBlank(CharSequence)
*/
public static boolean isNotBlank(final CharSequence cs) {
return !isBlank(cs);
}
// Trim
//-----------------------------------------------------------------------
/**
* Removes control characters (char <= 32) from both
* ends of this String, handling {@code null} by returning
* {@code null}.
*
* The String is trimmed using {@link String#trim()}.
* Trim removes start and end characters <= 32.
*
*
* StringUtils.trim(null) = null
* StringUtils.trim("") = ""
* StringUtils.trim(" ") = ""
* StringUtils.trim("abc") = "abc"
* StringUtils.trim(" abc ") = "abc"
*
*
* @param str the String to be trimmed, may be null
* @return the trimmed string, {@code null} if null String input
*/
public static String trim(final String str) {
return str == null ? null : str.trim();
}
/**
* Removes control characters (char <= 32) from both
* ends of this String returning {@code null} if the String is
* empty ("") after the trim or if it is {@code null}.
*
*
The String is trimmed using {@link String#trim()}.
* Trim removes start and end characters <= 32.
*
*
* StringUtils.trimToNull(null) = null
* StringUtils.trimToNull("") = null
* StringUtils.trimToNull(" ") = null
* StringUtils.trimToNull("abc") = "abc"
* StringUtils.trimToNull(" abc ") = "abc"
*
*
* @param str the String to be trimmed, may be null
* @return the trimmed String,
* {@code null} if only chars <= 32, empty or null String input
* @since 2.0
*/
public static String trimToNull(final String str) {
String ts = trim(str);
return isEmpty(ts) ? null : ts;
}
/**
* Removes control characters (char <= 32) from both
* ends of this String returning an empty String ("") if the String
* is empty ("") after the trim or if it is {@code null}.
*
*
The String is trimmed using {@link String#trim()}.
* Trim removes start and end characters <= 32.
*
*
* StringUtils.trimToEmpty(null) = ""
* StringUtils.trimToEmpty("") = ""
* StringUtils.trimToEmpty(" ") = ""
* StringUtils.trimToEmpty("abc") = "abc"
* StringUtils.trimToEmpty(" abc ") = "abc"
*
*
* @param str the String to be trimmed, may be null
* @return the trimmed String, or an empty String if {@code null} input
* @since 2.0
*/
public static String trimToEmpty(final String str) {
return str == null ? EMPTY : str.trim();
}
// Equals
//-----------------------------------------------------------------------
/**
* Compares two Strings, returning {@code true} if they represent
* equal sequences of characters.
*
* {@code null}s are handled without exceptions. Two {@code null}
* references are considered to be equal. The comparison is case sensitive.
*
*
* StringUtils.equals(null, null) = true
* StringUtils.equals(null, "abc") = false
* StringUtils.equals("abc", null) = false
* StringUtils.equals("abc", "abc") = true
* StringUtils.equals("abc", "ABC") = false
*
*
* @see Object#equals(Object)
* @param cs1 the first String, may be {@code null}
* @param cs2 the second String, may be {@code null}
* @return {@code true} if the Strings are equal (case-sensitive), or both {@code null}
*/
public static boolean equals(final String cs1, final String cs2) {
if (cs1 == null || cs2 == null) {
return false;
}
if (cs1.length() != cs2.length()) {
return false;
}
return cs1.equals(cs2);
}
// Substring
//-----------------------------------------------------------------------
/**
* Gets a substring from the specified String avoiding exceptions.
*
* A negative start position can be used to start {@code n}
* characters from the end of the String.
*
* A {@code null} String will return {@code null}.
* An empty ("") String will return "".
*
*
* StringUtils.substring(null, *) = null
* StringUtils.substring("", *) = ""
* StringUtils.substring("abc", 0) = "abc"
* StringUtils.substring("abc", 2) = "c"
* StringUtils.substring("abc", 4) = ""
* StringUtils.substring("abc", -2) = "bc"
* StringUtils.substring("abc", -4) = "abc"
*
*
* @param str the String to get the substring from, may be null
* @param start the position to start from, negative means count back from the end of the String by this many characters
* @return substring from start position, {@code null} if null String input
*/
public static String substring(final String str, int start) {
if (str == null) {
return null;
}
// handle negatives, which means last n characters
if (start < 0) {
start = str.length() + start; // remember start is negative
}
if (start < 0) {
start = 0;
}
if (start > str.length()) {
return EMPTY;
}
return str.substring(start);
}
/**
* Gets a substring from the specified String avoiding exceptions.
*
* A negative start position can be used to start/end {@code n}
* characters from the end of the String.
*
* The returned substring starts with the character in the {@code start}
* position and ends before the {@code end} position. All position counting is
* zero-based -- i.e., to start at the beginning of the string use
* {@code start = 0}. Negative start and end positions can be used to
* specify offsets relative to the end of the String.
*
* If {@code start} is not strictly to the left of {@code end}, ""
* is returned.
*
*
* StringUtils.substring(null, *, *) = null
* StringUtils.substring("", * , *) = "";
* StringUtils.substring("abc", 0, 2) = "ab"
* StringUtils.substring("abc", 2, 0) = ""
* StringUtils.substring("abc", 2, 4) = "c"
* StringUtils.substring("abc", 4, 6) = ""
* StringUtils.substring("abc", 2, 2) = ""
* StringUtils.substring("abc", -2, -1) = "b"
* StringUtils.substring("abc", -4, 2) = "ab"
*
*
* @param str the String to get the substring from, may be null
* @param start the position to start from, negative means count back from the end of the String by this many characters
* @param end the position to end at (exclusive), negative means count back from the end of the String by this many
* characters
* @return substring from start position to end position,
* {@code null} if null String input
*/
public static String substring(final String str, int start, int end) {
if (str == null) {
return null;
}
// handle negatives
if (end < 0) {
end = str.length() + end; // remember end is negative
}
if (start < 0) {
start = str.length() + start; // remember start is negative
}
// check length next
if (end > str.length()) {
end = str.length();
}
// if start is greater than end, return ""
if (start > end) {
return EMPTY;
}
if (start < 0) {
start = 0;
}
if (end < 0) {
end = 0;
}
return str.substring(start, end);
}
// Case conversion
//-----------------------------------------------------------------------
/**
* Converts a String to upper case as per {@link String#toUpperCase()}.
*
* A {@code null} input String returns {@code null}.
*
*
* StringUtils.upperCase(null) = null
* StringUtils.upperCase("") = ""
* StringUtils.upperCase("aBc") = "ABC"
*
*
* This uses "ENGLISH" as the locale.
*
* @param str the String to upper case, may be null
* @return the upper cased String, {@code null} if null String input
*/
public static String upperCase(final String str) {
if (str == null) {
return null;
}
return str.toUpperCase(Locale.ENGLISH);
}
/**
*
Converts a String to lower case as per {@link String#toLowerCase()}.
*
* A {@code null} input String returns {@code null}.
*
*
* StringUtils.lowerCase(null) = null
* StringUtils.lowerCase("") = ""
* StringUtils.lowerCase("aBc") = "abc"
*
*
* This uses "ENGLISH" as the locale.
*
* @param str the String to lower case, may be null
* @return the lower cased String, {@code null} if null String input
*/
public static String lowerCase(final String str) {
if (str == null) {
return null;
}
return str.toLowerCase(Locale.ENGLISH);
}
/**
*
Capitalizes a String changing the first character to title case as
* per {@link Character#toTitleCase(int)}. No other characters are changed.
*
*
* StringUtils.capitalize(null) = null
* StringUtils.capitalize("") = ""
* StringUtils.capitalize("cat") = "Cat"
* StringUtils.capitalize("cAt") = "CAt"
* StringUtils.capitalize("'cat'") = "'cat'"
*
*
* @param str the String to capitalize, may be null
* @return the capitalized String, {@code null} if null String input
* @see #uncapitalize(String)
* @since 2.0
*/
public static String capitalize(final String str) {
if (str == null || str.length() == 0) {
return str;
}
int firstCodepoint = str.codePointAt(0);
int newCodePoint = Character.toTitleCase(firstCodepoint);
if (firstCodepoint == newCodePoint) {
// already capitalized
return str;
}
int[] newCodePoints = new int[str.length()]; // cannot be longer than the char array
int outOffset = 0;
newCodePoints[outOffset++] = newCodePoint; // copy the first codepoint
for (int inOffset = Character.charCount(firstCodepoint); inOffset < str.length(); ) {
int codepoint = str.codePointAt(inOffset);
newCodePoints[outOffset++] = codepoint; // copy the remaining ones
inOffset += Character.charCount(codepoint);
}
return new String(newCodePoints, 0, outOffset);
}
/**
* Uncapitalizes a String, changing the first character to lower case as
* per {@link Character#toLowerCase(int)}. No other characters are changed.
*
*
* StringUtils.uncapitalize(null) = null
* StringUtils.uncapitalize("") = ""
* StringUtils.uncapitalize("cat") = "cat"
* StringUtils.uncapitalize("Cat") = "cat"
* StringUtils.uncapitalize("CAT") = "cAT"
*
*
* @param str the String to uncapitalize, may be null
* @return the uncapitalized String, {@code null} if null String input
* @see #capitalize(String)
* @since 2.0
*/
public static String uncapitalize(final String str) {
if (str == null || str.length() == 0) {
return str;
}
int firstCodepoint = str.codePointAt(0);
int newCodePoint = Character.toLowerCase(firstCodepoint);
if (firstCodepoint == newCodePoint) {
// already capitalized
return str;
}
int[] newCodePoints = new int[str.length()]; // cannot be longer than the char array
int outOffset = 0;
newCodePoints[outOffset++] = newCodePoint; // copy the first codepoint
for (int inOffset = Character.charCount(firstCodepoint); inOffset < str.length(); ) {
int codepoint = str.codePointAt(inOffset);
newCodePoints[outOffset++] = codepoint; // copy the remaining ones
inOffset += Character.charCount(codepoint);
}
return new String(newCodePoints, 0, outOffset);
}
/**
* Encode the given bytes as a string using the given charset
* @throws UncheckedIOException with a {@link CharacterCodingException} as the cause if the bytes cannot be encoded using the
* provided charset.
*/
public static String fromBytes(byte[] bytes, Charset charset) throws UncheckedIOException {
try {
return charset.newDecoder().decode(ByteBuffer.wrap(bytes)).toString();
} catch (CharacterCodingException e) {
throw new UncheckedIOException("Cannot encode string.", e);
}
}
/**
* Tests if this string starts with the specified prefix ignoring case considerations.
*
* @param str the string to be tested
* @param prefix the prefix
* @return true if the string starts with the prefix ignoring case
*/
public static boolean startsWithIgnoreCase(String str, String prefix) {
return str.regionMatches(true, 0, prefix, 0, prefix.length());
}
/**
* Replaces a String with another String inside a larger String, once.
*
* A {@code null} reference passed to this method is a no-op.
*
*
* 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"
*
*
* @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 static String replaceOnce(String text, String searchString, 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.
*
* A {@code null} reference passed to this method is a no-op.
*
*
* 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"
*
*
* @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 static String replace(String text, String searchString, 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.
*
* A {@code null} reference passed to this method is a no-op.
*
*
* 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"
*
*
* @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
*/
private static String replace(String text, String searchString, String replacement, int max) {
if (isEmpty(text) || isEmpty(searchString) || replacement == null || max == 0) {
return text;
}
int start = 0;
int end = indexOf(text, searchString, start);
if (end == -1) {
return text;
}
int replLength = searchString.length();
int increase = Math.max(replacement.length() - replLength, 0);
increase *= max < 0 ? 16 : Math.min(max, 64);
StringBuilder buf = new StringBuilder(text.length() + increase);
while (end != -1) {
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 all occurrences of Strings within another String.
*
*
*
* A {@code null} reference passed to this method is a no-op, or if
* any "search string" or "string to replace" is null, that replace will be
* ignored. This will not repeat. For repeating replaces, call the
* overloaded method.
*
*
*
* StringUtils.replaceEach(null, *, *) = null
* StringUtils.replaceEach("", *, *) = ""
* StringUtils.replaceEach("aba", null, null) = "aba"
* StringUtils.replaceEach("aba", new String[0], null) = "aba"
* StringUtils.replaceEach("aba", null, new String[0]) = "aba"
* StringUtils.replaceEach("aba", new String[]{"a"}, null) = "aba"
* StringUtils.replaceEach("aba", new String[]{"a"}, new String[]{""}) = "b"
* StringUtils.replaceEach("aba", new String[]{null}, new String[]{"a"}) = "aba"
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}) = "wcte"
* (example of how it does not repeat)
* StringUtils.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}) = "dcte"
*
*
* @param text
* text to search and replace in, no-op if null
* @param searchList
* the Strings to search for, no-op if null
* @param replacementList
* the Strings to replace them with, no-op if null
* @return the text with any replacements processed, {@code null} if
* null String input
* @throws IllegalArgumentException
* if the lengths of the arrays are not the same (null is ok,
* and/or size 0)
* @since 2.4
*/
public static String replaceEach(String text, String[] searchList, String[] replacementList) {
// mchyzer Performance note: This creates very few new objects (one major goal)
// let me know if there are performance requests, we can create a harness to measure
if (isEmpty(text)) {
return text;
}
int searchLength = searchList.length;
int replacementLength = replacementList.length;
// make sure lengths are ok, these need to be equal
if (searchLength != replacementLength) {
throw new IllegalArgumentException("Search and Replace array lengths don't match: "
+ searchLength
+ " vs "
+ replacementLength);
}
// keep track of which still have matches
boolean[] noMoreMatchesForReplIndex = new boolean[searchLength];
// index on index that the match was found
int textIndex = -1;
int replaceIndex = -1;
int tempIndex;
// index of replace array that will replace the search string found
// NOTE: logic duplicated below START
for (int i = 0; i < searchLength; i++) {
if (noMoreMatchesForReplIndex[i] || isEmpty(searchList[i]) || replacementList[i] == null) {
continue;
}
tempIndex = text.indexOf(searchList[i]);
// see if we need to keep searching for this
if (tempIndex == -1) {
noMoreMatchesForReplIndex[i] = true;
} else if (textIndex == -1 || tempIndex < textIndex) {
textIndex = tempIndex;
replaceIndex = i;
}
}
// NOTE: logic mostly below END
// no search strings found, we are done
if (textIndex == -1) {
return text;
}
int start = 0;
// get a good guess on the size of the result buffer so it doesn't have to double if it goes over a bit
int increase = 0;
// count the replacement text elements that are larger than their corresponding text being replaced
for (int i = 0; i < searchList.length; i++) {
if (searchList[i] == null || replacementList[i] == null) {
continue;
}
int greater = replacementList[i].length() - searchList[i].length();
if (greater > 0) {
increase += 3 * greater; // assume 3 matches
}
}
// have upper-bound at 20% increase, then let Java take over
increase = Math.min(increase, text.length() / 5);
StringBuilder buf = new StringBuilder(text.length() + increase);
while (textIndex != -1) {
for (int i = start; i < textIndex; i++) {
buf.append(text.charAt(i));
}
buf.append(replacementList[replaceIndex]);
start = textIndex + searchList[replaceIndex].length();
textIndex = -1;
replaceIndex = -1;
// find the next earliest match
// NOTE: logic mostly duplicated above START
for (int i = 0; i < searchLength; i++) {
if (noMoreMatchesForReplIndex[i] || searchList[i] == null ||
searchList[i].isEmpty() || replacementList[i] == null) {
continue;
}
tempIndex = text.indexOf(searchList[i], start);
// see if we need to keep searching for this
if (tempIndex == -1) {
noMoreMatchesForReplIndex[i] = true;
} else if (textIndex == -1 || tempIndex < textIndex) {
textIndex = tempIndex;
replaceIndex = i;
}
}
// NOTE: logic duplicated above END
}
int textLength = text.length();
for (int i = start; i < textLength; i++) {
buf.append(text.charAt(i));
}
return buf.toString();
}
/**
* Finds the first index within a CharSequence, handling {@code null}.
* This method uses {@link String#indexOf(String, int)} if possible.
*
* A {@code null} CharSequence will return {@code -1}.
*
*
* StringUtils.indexOf(null, *) = -1
* StringUtils.indexOf(*, null) = -1
* StringUtils.indexOf("", "") = 0
* StringUtils.indexOf("", *) = -1 (except when * = "")
* StringUtils.indexOf("aabaabaa", "a") = 0
* StringUtils.indexOf("aabaabaa", "b") = 2
* StringUtils.indexOf("aabaabaa", "ab") = 1
* StringUtils.indexOf("aabaabaa", "") = 0
*
*
* @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
* @since 2.0
* @since 3.0 Changed signature from indexOf(String, String) to indexOf(CharSequence, CharSequence)
*/
private static int indexOf(String seq, String searchSeq, int start) {
if (seq == null || searchSeq == null) {
return -1;
}
return seq.indexOf(searchSeq, start);
}
/**
* Replace the prefix of the string provided ignoring case considerations.
*
*
* The unmatched part is unchanged.
*
*
* @param str the string to replace
* @param prefix the prefix to find
* @param replacement the replacement
* @return the replaced string
*/
public static String replacePrefixIgnoreCase(String str, String prefix, String replacement) {
return str.replaceFirst("(?i)" + prefix, replacement);
}
/**
* Searches a string for the first occurrence of a character specified by a list of characters.
* @param s The string to search.
* @param charsToMatch A list of characters to search the string for.
* @return The character that was first matched in the string or null if none of the characters were found.
*/
public static Character findFirstOccurrence(String s, char... charsToMatch) {
int lowestIndex = Integer.MAX_VALUE;
for (char toMatch : charsToMatch) {
int currentIndex = s.indexOf(toMatch);
if (currentIndex != -1 && currentIndex < lowestIndex) {
lowestIndex = currentIndex;
}
}
return lowestIndex == Integer.MAX_VALUE ? null : s.charAt(lowestIndex);
}
/**
* Convert a string to boolean safely (as opposed to the less strict {@link Boolean#parseBoolean(String)}). If a customer
* specifies a boolean value it should be "true" or "false" (case insensitive) or an exception will be thrown.
*/
public static boolean safeStringToBoolean(String value) {
if (value.equalsIgnoreCase("true")) {
return true;
} else if (value.equalsIgnoreCase("false")) {
return false;
}
throw new IllegalArgumentException("Value was defined as '" + value + "', but should be 'false' or 'true'");
}
/**
* Returns a string whose value is the concatenation of this string repeated {@code count} times.
*
* If this string is empty or count is zero then the empty string is returned.
*
* Logical clone of JDK11's {@link String#repeat(int)}.
*
* @param value the string to repeat
* @param count number of times to repeat
* @return A string composed of this string repeated {@code count} times or the empty string if this string is empty or count
* is zero
* @throws IllegalArgumentException if the {@code count} is negative.
*/
public static String repeat(String value, int count) {
if (count < 0) {
throw new IllegalArgumentException("count is negative: " + count);
}
if (value == null || value.length() == 0 || count == 1) {
return value;
}
if (count == 0) {
return "";
}
if (value.length() > Integer.MAX_VALUE / count) {
throw new OutOfMemoryError("Repeating " + value.length() + " bytes String " + count +
" times will produce a String exceeding maximum size.");
}
int len = value.length();
int limit = len * count;
char[] array = new char[limit];
value.getChars(0, len, array, 0);
int copied;
for (copied = len; copied < limit - copied; copied <<= 1) {
System.arraycopy(array, 0, array, copied, copied);
}
System.arraycopy(array, 0, array, copied, limit - copied);
return new String(array);
}
}