Strings.java
// Generated by delombok at Mon Nov 18 07:27:48 UTC 2024
package de.larssh.utils.text;
import static de.larssh.utils.Collectors.toLinkedHashMap;
import static de.larssh.utils.Finals.constant;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableMap;
import static java.util.stream.Collectors.joining;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.Formatter;
import java.util.IllegalFormatException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import de.larssh.utils.Optionals;
import de.larssh.utils.collection.Maps;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* This class contains helper methods for {@link String}.
*/
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ExcessiveImports", "PMD.GodClass"})
@SuppressFBWarnings(value = "POTENTIAL_XML_INJECTION", justification = "false positive caused by named groups")
public final class Strings {
/**
* Character to separate strings inside regular expressions
*/
private static final String PATTERN_STRING_SEPARATOR = "|";
/**
* Map of binary units to their factor
*
* <ul>
* <li>{@code K}: {@code 1024}
* <li>{@code M}: {@code 1024 * 1024}
* <li>{@code G}: {@code 1024 * 1024 * 1024}
* <li>...
* </ul>
*/
public static final Map<String, BigDecimal> BINARY_UNITS = unmodifiableMap(getBinaryUnits());
/**
* Pattern for parsing binary unit strings
*/
private static final Pattern BINARY_UNIT_PATTERN = Pattern.compile("(?i)^\\s*(?<value>[+-]?\\s*(\\d([\\d_]*\\d)?)?\\.?\\d([\\d_]*\\d)?)\\s*((?<unit>" + BINARY_UNITS.keySet().stream().map(Pattern::quote).collect(joining(PATTERN_STRING_SEPARATOR)) + ")i?)?\\s*$");
/**
* Map of decimal units to their power of ten
*
* <ul>
* <li>...
* <li>{@code m}: {@code -3}
* <li>{@code k}: {@code 3}
* <li>{@code M}: {@code 6}
* <li>...
* </ul>
*/
@SuppressWarnings({"checkstyle:MagicNumber", "checkstyle:MultipleStringLiterals"})
@SuppressFBWarnings(value = "PSC_PRESIZE_COLLECTIONS", justification = "this method is called just once (in static initializer); keep code simple")
public static final Map<String, Integer> DECIMAL_UNITS = Maps.<String, Integer>builder(new LinkedHashMap<>()).put("y", -24).put("z", -21).put("a", -18).put("f", -15).put("p", -12).put("n", -9).put("u", -6).put("μ", -6).put("m", -3).put("c", -2).put("d", -1).put("da", 1).put("h", 2).put("k", 3).put("M", 6).put("G", 9).put("T", 12).put("P", 15).put("E", 18).put("Z", 21).put("Y", 24).unmodifiable();
/**
* Pattern for parsing decimal unit strings
*/
private static final Pattern DECIMAL_UNIT_PATTERN = Pattern.compile("(?i)^\\s*(?<value>[+-]?\\s*(\\d([\\d_]*\\d)?)?\\.?\\d([\\d_]*\\d)?)\\s*(?<unit>" + DECIMAL_UNITS.keySet().stream().map(Pattern::quote).collect(joining(PATTERN_STRING_SEPARATOR)) + ")?\\s*$");
/**
* Constant UTF-8 for usage as default char set.
*
* <p>
* Using {@link java.nio.charset.Charset#defaultCharset()} leads to unexpected
* compatibility problems. While the new {@link java.nio.file.Files} API has
* been changed to use UTF-8 by default, old and third-party implementations
* still depend on the default char set or require a custom char set.
*
* <p>
* <b>Why shouldn't I use {@link StandardCharsets#UTF_8} directly?</b><br>
* Using {@link StandardCharsets#UTF_8} is bad practice, as it holds
* implementation specific information.
*/
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
/**
* Constant locale as default locale where no language and country is required.
*
* <p>
* Using {@link Locale#getDefault()} leads to unexpected behavior for technical
* formatting operations. Therefore this locale specifies a default value.
*
* <p>
* The returned value is {@link Locale#ROOT}.
*/
public static final Locale DEFAULT_LOCALE = Locale.ROOT;
/**
* Constant {@code "\n"} <b>for output</b>. Remember to accept {@code "\r\n"}
* and {@code "\r"} on the input side, too!
*
* <p>
* Using {@link System#lineSeparator()} leads to files, which cannot (should
* not) be transferred between UNIX and Microsoft Windows systems. To avoid
* problems, some developers decided to depend on the UNIX line separator only.
*
* <p>
* <b>Why shouldn't I use {@code "\n"} directly?</b><br>
* Using {@code "\n"} is bad practice, as it holds implementation specific
* information.
*/
public static final String NEW_LINE = constant("\n");
/**
* Pattern to retrieve white space characters in binary and decimal unit strings
*/
private static final Pattern UNIT_WHITE_SPACE_PATTERN = Pattern.compile("[\\s_]+");
/**
* Compares two strings, ignoring case differences in the ASCII range.
*
* @param first the first string to compare
* @param second the second string to compare
* @return a positive integer, zero, or a negative integer as {@code first} is
* greater than, equal to, or less than {@code second}, ignoring case
* considerations in the ASCII range.
*/
public static int compareIgnoreCaseAscii(final CharSequence first, final CharSequence second) {
final int firstLength = first.length();
final int secondLength = second.length();
final int minLength = Math.min(firstLength, secondLength);
for (int index = 0; index < minLength; index += 1) {
final int compare = Characters.compareIgnoreCaseAscii(first.charAt(index), second.charAt(index));
if (compare != 0) {
return compare;
}
}
if (firstLength == secondLength) {
return 0;
}
return firstLength < secondLength ? -1 : 1;
}
/**
* Tests if {@code value} contains {@code substring}, ignoring case
* considerations.
*
* @param string the string to search in
* @param substring the sequence to search for
* @return {@code true} if {@code value} contains {@code substring}, ignoring
* case considerations, else {@code false}
*/
public static boolean containsIgnoreCase(final CharSequence string, final CharSequence substring) {
return indexOfIgnoreCase(string, substring) != -1;
}
/**
* Tests if {@code value} contains {@code substring}, ignoring case
* considerations in the ACII range.
*
* @param string the string to search in
* @param substring the sequence to search for
* @return {@code true} if {@code value} contains {@code substring}, ignoring
* case considerations in the ACII range, else {@code false}
*/
public static boolean containsIgnoreCaseAscii(final CharSequence string, final CharSequence substring) {
return indexOfIgnoreCaseAscii(string, substring) != -1;
}
/**
* Compares {@code first} to {@code second}, ignoring case considerations in the
* ASCII range. Two strings are considered equal ignoring case if they are of
* the same length and corresponding characters in the two strings are equal
* ignoring case in the ASCII range.
*
* @param first the first of both strings to compare
* @param second the second of both strings to compare
* @return {@code true} if {@code first} and {@code second} are considered
* equal, ignoring case in the ACII range, else {@code false}
*/
@SuppressWarnings("PMD.CompareObjectsWithEquals")
public static boolean equalsIgnoreCaseAscii(@Nullable final CharSequence first, @Nullable final CharSequence second) {
if (first == null || second == null) {
return first == second;
}
return compareIgnoreCaseAscii(first, second) == 0;
}
/**
* Tests if {@code value} ends with the specified suffix, ignoring case
* considerations.
*
* @param value string to compare against
* @param suffix the suffix
* @return {@code true} if {@code prefix} is a suffix of the character sequence
* represented by {@code value}, ignoring case considerations, else
* {@code false}
*/
public static boolean endsWithIgnoreCase(final CharSequence value, final CharSequence suffix) {
return startsWithIgnoreCase(value, suffix, value.length() - suffix.length());
}
/**
* Tests if {@code value} ends with the specified suffix, ignoring case
* considerations in the ASCII range.
*
* @param value string to compare against
* @param suffix the suffix
* @return {@code true} if {@code prefix} is a suffix of the character sequence
* represented by {@code value}, ignoring case considerations in the
* ASCII range, else {@code false}
*/
public static boolean endsWithIgnoreCaseAscii(final CharSequence value, final CharSequence suffix) {
return startsWithIgnoreCaseAscii(value, suffix, value.length() - suffix.length());
}
/**
* Tells whether or not a subsequence {@code input} matches {@code pattern}.
*
* <p>
* Use {@link Patterns#find(Pattern, CharSequence)} if you need a matcher.
*
* @param input the input sequence to find the pattern in
* @param pattern the matching pattern
* @return {@code true} if, and only if, {@code input} matches {@code pattern}
*/
public static boolean find(final CharSequence input, final Pattern pattern) {
return pattern.matcher(input).find();
}
/**
* Returns a formatted string using the specified format string and
* {@code arguments}. In that way this method works <i>similar</i> to
* {@link String#format(String, Object...)}.
*
* <p>
* Here are the differences:
* <ul>
* <li>In case {@code arguments} is empty, no checks and formatting is
* performed. {@code format} is returned immediately and without modification.
* <li>{@link #DEFAULT_LOCALE} is used instead of the systems default
* {@link Locale}.
* <li>On formatting failure no exception is thrown. When used for error
* messages this often hides the original error message. Instead a failure
* message is returned, containing {@code format}.
* </ul>
*
* @param format <a href="../util/Formatter.html#syntax">format string</a>
* @param arguments arguments referenced by format specifiers in {@code format}
* @return formatted string
*/
@SuppressFBWarnings(value = "FORMAT_STRING_MANIPULATION", justification = "formatting exceptions are catched and handled accordingly")
public static String format(final String format, final Object... arguments) {
if (arguments.length < 1) {
return format;
}
try (Formatter formatter = new Formatter(DEFAULT_LOCALE)) {
return formatter.format(format, arguments).toString();
} catch (final IllegalFormatException e) {
return "Failed formatting string [" + format + "]: " + e.getMessage();
}
}
/**
* Returns a map of binary units to their factor for static initialization.
*
* @return map map of binary units to their factor
*/
@SuppressWarnings("checkstyle:MultipleStringLiterals")
private static Map<String, BigDecimal> getBinaryUnits() {
final List<String> binaryUnits = asList("K", "M", "G", "T", "P", "E", "Z", "Y");
final BigDecimal oneThousandTwentyFour = new BigDecimal(1024);
return IntStream.range(0, binaryUnits.size()).boxed().collect(toLinkedHashMap(binaryUnits::get, index -> oneThousandTwentyFour.pow(index + 1)));
}
/**
* Returns the index within {@code string} of the first occurrence of
* {@code substring}, ignoring e.g. case considerations using a custom character
* comparison method, starting at the specified index.
*
* @param string the string to search in
* @param substring the substring to search for
* @param fromIndex the index from which to start the search
* @param equals the character comparison method
* @return the index of the first occurrence of {@code substring}, starting at
* the specified index, or {@code -1} if there is no such occurrence
*/
private static int indexOf(final CharSequence string, final CharSequence substring, final int fromIndex, final BiCharPredicate equals) {
final int stringLength = string.length();
// Fix index
int index;
if (fromIndex < 0) {
index = 0;
} else if (fromIndex >= stringLength) {
index = stringLength;
} else {
index = fromIndex;
}
// Get first substring character
if (substring.length() == 0) {
return index;
}
final char substringFirst = substring.charAt(0);
// Perform the search
final int maxIndex = stringLength - substring.length();
for (; index <= maxIndex; index += 1) {
if (equals.test(string.charAt(index), substringFirst) && startsWith(string, substring, index, equals)) {
return index;
}
}
return -1;
}
/**
* Returns the index within {@code string} of the first occurrence of
* {@code substring}, ignoring case considerations.
*
* @param string the string to search in
* @param substring the substring to search for
* @return the index of the first occurrence of {@code substring}, ignoring case
* considerations, or {@code -1} if there is no such occurrence
*/
public static int indexOfIgnoreCase(final CharSequence string, final CharSequence substring) {
return indexOfIgnoreCase(string, substring, 0);
}
/**
* Returns the index within {@code string} of the first occurrence of
* {@code substring}, ignoring case considerations, starting at the specified
* index.
*
* @param string the string to search in
* @param substring the substring to search for
* @param fromIndex the index from which to start the search
* @return the index of the first occurrence of {@code substring}, ignoring case
* considerations, starting at the specified index, or {@code -1} if
* there is no such occurrence
*/
public static int indexOfIgnoreCase(final CharSequence string, final CharSequence substring, final int fromIndex) {
return indexOf(string, substring, fromIndex, Characters::equalsIgnoreCase);
}
/**
* Returns the index within {@code string} of the first occurrence of
* {@code substring}, ignoring case considerations in the ASCII range.
*
* @param string the string to search in
* @param substring the substring to search for
* @return the index of the first occurrence of {@code substring}, ignoring case
* considerations in the ASCII range, or {@code -1} if there is no such
* occurrence
*/
public static int indexOfIgnoreCaseAscii(final CharSequence string, final CharSequence substring) {
return indexOfIgnoreCaseAscii(string, substring, 0);
}
/**
* Returns the index within {@code string} of the first occurrence of
* {@code substring}, ignoring case considerations in the ASCII range, starting
* at the specified index.
*
* @param string the string to search in
* @param substring the substring to search for
* @param fromIndex the index from which to start the search
* @return the index of the first occurrence of {@code substring}, ignoring case
* considerations in the ASCII range, starting at the specified index,
* or {@code -1} if there is no such occurrence
*/
public static int indexOfIgnoreCaseAscii(final CharSequence string, final CharSequence substring, final int fromIndex) {
return indexOf(string, substring, fromIndex, Characters::equalsIgnoreCaseAscii);
}
/**
* Returns {@code true} if {@code value} consists of whitespace only or equals
* {@code null}.
*
* <p>
* This is an optimized way of {@code value.trim().isEmpty()}.
*
* @param value string
* @return {@code true} if {@code value} consists of whitespace only or equals
* {@code null}
*/
public static boolean isBlank(@Nullable final CharSequence value) {
if (value == null) {
return true;
}
final int length = value.length();
for (int index = 0; index < length; index += 1) {
if (!Characters.isAsciiWhitespace(value.charAt(index))) {
return false;
}
}
return true;
}
/**
* Tells whether or not {@code input} matches {@code pattern}.
*
* <p>
* Use {@link Patterns#matches(Pattern, CharSequence)} if you need a matcher.
*
* @param input the value to match
* @param pattern the matching pattern
* @return {@code true} if, and only if, {@code input} matches {@code pattern}
*/
public static boolean matches(final CharSequence input, final Pattern pattern) {
return pattern.matcher(input).matches();
}
/**
* A {@link Comparator} that orders alpha numeric strings in <i>natural</i>
* order, either case sensitive or case insensitive. It is appreciated to use
* this kind of ordering for user output.
*
* <p>
* Because numeric values are not deserialized into numeric data types, their
* length is not limited. Fractions are not supported and will be handled as two
* separate numeric values.
*
* <p>
* Numeric values can have a leading plus or minus sign when following a
* whitespace character or at a strings start.
*
* <p>
* The following lists some example values to demonstrate the ordering.
* <ul>
* <li>Banana -12 Circus
* <li>Banana -5 Circus
* <li>Banana +5 Circus
* <li>Banana 5 Circus
* <li>Banana +5 Dolphin
* <li>Banana 8 Circus
* <li>Banana 12 Circus
* <li>Banana-5 Circus
* <li>Banana-12 Circus
* <li>Banana--5 Circus
* <li>Elephant 5 Circus
* </ul>
*
* <p>
* This comparator permits null values.
*
* <p>
* Note that this Comparator does <em>not</em> take locale into account, and
* will result in an unsatisfactory ordering for certain locales.
*
* @param caseInsensitive {@code true} if comparison should take place case
* insensitive
* @return a {@link Comparator} that orders alpha numeric strings in
* <i>natural</i> order, either case sensitive or case insensitive. It
* is appreciated to use this kind of ordering for user output.
*/
public static Comparator<String> numericTextComparator(final boolean caseInsensitive) {
return caseInsensitive ? NumericTextComparator.COMPARATOR_CASE_INSENSITIVE : NumericTextComparator.COMPARATOR_CASE_SENSITIVE;
}
/**
* Parses {@code binaryValue} as binary value with
* <ul>
* <li>optional sign,
* <li>optional fraction
* <li>and optional binary unit (case insensitive matching).
* </ul>
*
* <p>
* Binary units multiply by 1024. Though strings with fractions can be parsed,
* the resulting value must not contain a fraction part. Numeric values can be
* formatted using underscore, just as numeric Java literals can be formatted.
*
* <p>
* <b>Examples:</b>
* <table>
* <caption>Examples</caption>
* <tr>
* <th>Parameter</th>
* <th>Calculation</th>
* <th>Return Value</th>
* </tr>
* <tr>
* <td>-2m</td>
* <td>-2 * 1024 * 1024</td>
* <td>-2 097 152</td>
* </tr>
* <tr>
* <td>0</td>
* <td></td>
* <td>0</td>
* </tr>
* <tr>
* <td>5.4k</td>
* <td>5.4 * 1024</td>
* <td>5529.6 -> ArithmeticException</td>
* </tr>
* <tr>
* <td>+5_432</td>
* <td></td>
* <td>5 432</td>
* </tr>
* </table>
*
* @param binaryValue binary string
* @return binary value
* @throws StringParseException on parse failure
*/
@SuppressWarnings("checkstyle:MultipleStringLiterals")
public static BigInteger parseBinaryUnit(final CharSequence binaryValue) throws StringParseException {
final Optional<Matcher> matcher = Patterns.matches(BINARY_UNIT_PATTERN, binaryValue);
if (!matcher.isPresent()) {
throw new StringParseException("Value [%s] does not match binary unit pattern.", binaryValue);
}
final String value = matcher.get().group("value");
if (value == null) {
throw new StringParseException("No binary unit value given in string [%s].", binaryValue);
}
final String unit = matcher.get().group("unit");
final BigDecimal multiplicator = unit == null ? BigDecimal.ONE : BINARY_UNITS.get(toUpperCaseAscii(unit));
return new BigDecimal(replaceAll(value, UNIT_WHITE_SPACE_PATTERN, "")).multiply(multiplicator).toBigIntegerExact();
}
/**
* Parses {@code decimalValue} as decimal value with
* <ul>
* <li>optional sign,
* <li>optional fraction
* <li>and optional decimal unit (case insensitive matching - where possible).
* </ul>
*
* <p>
* Numeric values can be formatted using underscore, just as numeric Java
* literals can be formatted.
*
* <p>
* <b>Examples:</b>
* <table>
* <caption>Examples</caption>
* <tr>
* <th>Parameter</th>
* <th>Return Value</th>
* </tr>
* <tr>
* <td>-2m</td>
* <td>-0.002</td>
* </tr>
* <tr>
* <td>0</td>
* <td>0</td>
* </tr>
* <tr>
* <td>5.4k</td>
* <td>5 400</td>
* </tr>
* <tr>
* <td>+5_432</td>
* <td>5 432</td>
* </tr>
* </table>
*
* @param decimalValue decimal string
* @return decimal value
* @throws StringParseException on parse failure
*/
@SuppressWarnings("checkstyle:MultipleStringLiterals")
public static BigDecimal parseDecimalUnit(final CharSequence decimalValue) throws StringParseException {
final Optional<Matcher> matcher = Patterns.matches(DECIMAL_UNIT_PATTERN, decimalValue);
if (!matcher.isPresent()) {
throw new StringParseException("Value [%s] does not match decimal unit pattern.", decimalValue);
}
final String value = matcher.get().group("value");
if (value == null) {
throw new StringParseException("No decimal unit value given in string [%s].", decimalValue);
}
final String unit = matcher.get().group("unit");
final int powerOfTen = Optionals.getFirst(Objects::nonNull, () -> unit == null ? 0 : null, () -> DECIMAL_UNITS.get(unit), () -> DECIMAL_UNITS.get(toUpperCaseAscii(unit)), () -> DECIMAL_UNITS.get(toLowerCaseAscii(unit))).orElseThrow(() -> new StringParseException("Found unexpected decimal unit [%s].", unit));
return new BigDecimal(replaceAll(value, UNIT_WHITE_SPACE_PATTERN, "")).scaleByPowerOfTen(powerOfTen);
}
/**
* Replaces the first subsequence of {@code input} that matches {@code pattern}
* with {@code replacement}.
*
* <p>
* Note that {@code replacement} is not a literal replacement string.
* Backslashes ({@code \}) and dollar signs ({@code $}) may be treated
* differently. Quote unknown literal replacement strings using
* {@link Matcher#quoteReplacement(String)}.
*
* @param input the value to match
* @param pattern the matching pattern
* @param replacement the replacement string
* @return the string constructed by replacing each matching subsequence by the
* replacement string, substituting captured subsequences as needed
*/
public static String replaceFirst(final CharSequence input, final Pattern pattern, final String replacement) {
return pattern.matcher(input).replaceFirst(replacement);
}
/**
* Replaces every subsequence of {@code input} that matches {@code pattern} with
* {@code replacement}.
*
* <p>
* Note that {@code replacement} is not a literal replacement string.
* Backslashes ({@code \}) and dollar signs ({@code $}) may be treated
* differently. Quote unknown literal replacement strings using
* {@link Matcher#quoteReplacement(String)}.
*
* @param input the value to match
* @param pattern the matching pattern
* @param replacement the replacement string
* @return the string constructed by replacing each matching subsequence by the
* replacement string, substituting captured subsequences as needed
*/
public static String replaceAll(final CharSequence input, final Pattern pattern, final String replacement) {
return pattern.matcher(input).replaceAll(replacement);
}
/**
* Tests if the substring of {@code value} beginning at {@code offset} starts
* with {@code prefix}, ignoring e.g. case considerations using a custom
* character comparison method.
*
* @param value string to compare against
* @param prefix the prefix
* @param offset where to begin looking in {@code value}
* @param equals the character comparison method
* @return {@code true} if {@code prefix} is a prefix of the substring of
* {@code value} starting at {@code offset}, else {@code false}. The
* result is {@code false} if {@code offset} is negative or greater than
* the length of {@code value}.
*/
private static boolean startsWith(final CharSequence value, final CharSequence prefix, final int offset, final BiCharPredicate equals) {
final int prefixLength = prefix.length();
if (offset < 0 || offset + prefixLength > value.length()) {
return false;
}
for (int index = 0; index < prefixLength; index += 1) {
if (!equals.test(value.charAt(offset + index), prefix.charAt(index))) {
return false;
}
}
return true;
}
/**
* Tests if {@code value} starts with {@code prefix} ignoring case
* considerations.
*
* @param value string to compare against
* @param prefix the prefix
* @return {@code true} if {@code prefix} is a prefix of {@code value}, ignoring
* case considerations, else {@code false}
*/
public static boolean startsWithIgnoreCase(final CharSequence value, final CharSequence prefix) {
return startsWithIgnoreCase(value, prefix, 0);
}
/**
* Tests if the substring of {@code value} beginning at {@code offset} starts
* with {@code prefix}, ignoring case considerations.
*
* @param value string to compare against
* @param prefix the prefix
* @param offset where to begin looking in {@code value}
* @return {@code true} if {@code prefix} is a prefix of the substring of
* {@code value} starting at {@code offset}, ignoring case
* considerations, else {@code false}. The result is {@code false} if
* {@code offset} is negative or greater than the length of
* {@code value}.
*/
public static boolean startsWithIgnoreCase(final CharSequence value, final CharSequence prefix, final int offset) {
return startsWith(value, prefix, offset, Characters::equalsIgnoreCase);
}
/**
* Tests if {@code value} starts with the specified prefix, ignoring case
* considerations in the ASCII range.
*
* @param value string to compare against
* @param prefix the prefix
* @return {@code true} if {@code prefix} is a prefix of the character sequence
* represented by {@code value}, ignoring case considerations in the
* ASCII range, else {@code false}
*/
public static boolean startsWithIgnoreCaseAscii(final CharSequence value, final CharSequence prefix) {
return startsWithIgnoreCaseAscii(value, prefix, 0);
}
/**
* Tests if the substring of {@code value} beginning at the specified index
* starts with the specified prefix, ignoring case considerations in the ASCII
* range.
*
* @param value string to compare against
* @param prefix the prefix
* @param offset where to begin looking in {@code value}.
* @return {@code true} if {@code prefix} is a prefix of the substring of
* {@code value} starting at index {@code offset}, ignoring case
* considerations in the ASCII range, else {@code false}. The result is
* {@code false} if {@code offset} is negative or greater than the
* length of this {@code String} object.
*/
public static boolean startsWithIgnoreCaseAscii(final CharSequence value, final CharSequence prefix, final int offset) {
return startsWith(value, prefix, offset, Characters::equalsIgnoreCaseAscii);
}
/**
* Converts all of the ASCII upper case characters in {@code value} to lower
* case.
*
* @param value string to convert
* @return converted string
*/
public static String toLowerCaseAscii(final CharSequence value) {
final int length = value.length();
final StringBuilder builder = new StringBuilder(length);
for (int index = 0; index < length; index += 1) {
builder.append(Characters.toLowerCaseAscii(value.charAt(index)));
}
return builder.toString();
}
/**
* Converts all of the characters in {@code value} to lower case using
* {@link #DEFAULT_LOCALE}. This method is equivalent to
* {@code toLowerCase(Locale.ROOT)}.
*
* @param value string to convert
* @return converted string
*/
public static String toLowerCaseNeutral(final String value) {
return value.toLowerCase(DEFAULT_LOCALE);
}
/**
* Converts {@code value} to title case by converting its first character using
* {@link Character#toTitleCase(char)} and following to lower case using
* {@link #DEFAULT_LOCALE}.
*
* @param value string to convert
* @return converted string
*/
public static String toTitleCaseNeutral(final String value) {
if (value.isEmpty()) {
return value;
}
final int length = value.length();
if (length == 1) {
return Character.toString(Character.toTitleCase(value.charAt(0)));
}
final String lowerCase = value.toLowerCase(DEFAULT_LOCALE);
return new StringBuilder(length).append(Character.toTitleCase(value.charAt(0))).append(lowerCase, 1, lowerCase.length()).toString();
}
/**
* Converts all of the ASCII lower case characters in {@code value} to upper
* case.
*
* @param value string to convert
* @return converted string
*/
public static String toUpperCaseAscii(final CharSequence value) {
final int length = value.length();
final StringBuilder builder = new StringBuilder(length);
for (int index = 0; index < length; index += 1) {
builder.append(Characters.toUpperCaseAscii(value.charAt(index)));
}
return builder.toString();
}
/**
* Converts all of the characters in {@code value} to upper case using
* {@link #DEFAULT_LOCALE}. This method is equivalent to
* {@code toUpperCase(Locale.ROOT)}.
*
* @param value string to convert
* @return converted string
*/
public static String toUpperCaseNeutral(final String value) {
return value.toUpperCase(DEFAULT_LOCALE);
}
/**
* Returns a string similar to {@code value}, but with any leading whitespace
* removed.
*
* <p>
* This method works similar to {@link String#trim()}, though it handles only
* the strings start.
*
* <p>
* Whitespace characters are recognized using
* {@link Characters#isAsciiWhitespace(char)}.
*
* @param value value
* @return left trimmed value
*/
public static String trimStart(final CharSequence value) {
final int length = value.length();
int start = 0;
while (start < length && Characters.isAsciiWhitespace(value.charAt(start))) {
start += 1;
}
return value.subSequence(start, length).toString();
}
/**
* Returns a string similar to {@code value}, but with any trailing whitespace
* removed.
*
* <p>
* This method works similar to {@link String#trim()}, though it handles only
* the strings end.
*
* <p>
* Whitespace characters are recognized using
* {@link Characters#isAsciiWhitespace(char)}.
*
* @param value value
* @return left trimmed value
*/
public static String trimEnd(final CharSequence value) {
int end = value.length();
while (end > 0 && Characters.isAsciiWhitespace(value.charAt(end - 1))) {
end -= 1;
}
return value.subSequence(0, end).toString();
}
/**
* Represents a predicate (boolean-valued function) of two primitive
* {@code char} arguments.
*
* <p>
* This is a functional interface whose functional method is
* {@link #test(char, char)}.
*/
@FunctionalInterface
@SuppressWarnings("PMD.UnnecessaryModifier")
private static interface BiCharPredicate {
/**
* Evaluates this predicate on the given arguments.
*
* @param first the first input argument
* @param second the second input argument
* @return {@code true} if the input arguments match the predicate, otherwise
* {@code false}
*/
boolean test(char first, char second);
}
@java.lang.SuppressWarnings("all")
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(justification = "generated code")
@lombok.Generated
private Strings() {
throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated");
}
}