package com.github.czyzby.kiwi.util.common; /** Utility class for strings and CharSequences (sometimes expected or returned by Scene2D API). * * @author MJ */ public class Strings extends UtilitiesClass { /** A string with length of 0, not null. */ public static final String EMPTY_STRING = ""; /** Common regex. Allows to determine if string contains no characters or only whitespaces. */ public static final String WHITESPACE_REGEX = "\\s*"; /** Common regex. Allows to split a sentence into trimmed words. */ public static final String WHITESPACE_SPLITTER_REGEX = "\\s+"; /** Empty immutable array of strings. Might be used as utility for methods returning empty arrays to avoid object * allocation. */ public static final String[] EMPTY_ARRAY = new String[] {}; /** If this value is returned by {@link String#indexOf(int)}, the character was not found. This value aims to reduce * the amount of magic numbers in string-handling methods. * * @see #isCharacterPresent(int) * @see #isCharacterAbsent(int) */ public static final int CHARACTER_UNAVAILABLE = -1; private Strings() { } /** @return true if passed char sequence is null or has no characters. */ public static boolean isEmpty(final CharSequence charSequence) { return charSequence == null || charSequence.length() == 0; } /** @return true if passed char sequence is not null and has at least one character. */ public static boolean isNotEmpty(final CharSequence charSequence) { return charSequence != null && charSequence.length() > 0; } /** @param charSequence will be checked. * @return true if the passed sequence is null or contains only whitespace characters. * @see Strings#isWhitespace(CharSequence) */ public static boolean isBlank(final CharSequence charSequence) { return isWhitespace(charSequence); } /** @return true if the passed sequence is null or contains only whitespace characters. */ public static boolean isWhitespace(final CharSequence charSequence) { if (isEmpty(charSequence)) { return true; } for (int index = 0, length = charSequence.length(); index < length; index++) { final char character = charSequence.charAt(index); if (isNotWhitespace(character)) { return false; } } return true; } /** @param charSequence can contain whitespace characters. * @return the passed sequence without any whitespace characters. Never null; might be an empty string. */ public static String stripWhitespaces(final CharSequence charSequence) { if (isEmpty(charSequence)) { return EMPTY_STRING; } final StringBuilder builder = new StringBuilder(charSequence.length()); for (int index = 0, length = charSequence.length(); index < length; index++) { final char character = charSequence.charAt(index); if (isNotWhitespace(character)) { builder.append(character); } } if (isEmpty(builder)) { return EMPTY_STRING; } return builder.toString(); } /** @param resultOfIndexOf result of {@link String#indexOf(int)} or its overloaded methods. * @return true if the index is valid and the character was found in the string. */ public static boolean isCharacterPresent(final int resultOfIndexOf) { return resultOfIndexOf > CHARACTER_UNAVAILABLE; } /** @param resultOfIndexOf result of {@link String#indexOf(int)} or its overloaded methods. * @return true is invalid and the character was not found in the string. */ public static boolean isCharacterAbsent(final int resultOfIndexOf) { return resultOfIndexOf <= CHARACTER_UNAVAILABLE; } /** @param charSequence will be checked. * @return false if the passed sequence is null or contains only whitespace characters. * @see Strings#isNotWhitespace(CharSequence) */ public static boolean isNotBlank(final CharSequence charSequence) { return isNotWhitespace(charSequence); } /** @return true if the passed sequence is not null and contains at least one non-whitespace character. */ public static boolean isNotWhitespace(final CharSequence charSequence) { if (isEmpty(charSequence)) { return false; } for (int index = 0, length = charSequence.length(); index < length; index++) { final char character = charSequence.charAt(index); if (isNotWhitespace(character)) { return true; } } return false; } /** GWT utility. Character.isWhitespace not supported. * * @return true if character is a whitespace. */ public static boolean isWhitespace(final char character) { return character == ' ' || character == '\t' || character == '\n' || character == '\r' || character == '\f'; } /** GWT utility. Character.isWhitespace not supported. * * @return true if character is not a whitespace. */ public static boolean isNotWhitespace(final char character) { return character != ' ' && character != '\t' && character != '\n' && character != '\r' && character != '\f'; } /** @return true if passed character is a new line char. */ public static boolean isNewLine(final char character) { return character == '\n' || character == '\r'; } /** @return true if passed character is not a new line char. */ public static boolean isNotNewLine(final char character) { return character != '\n' && character != '\r'; } /** @param charSequence will be validated. Can be null. * @param length required length. * @return true if passed sequence is not null and its length is lower than passed value. */ public static boolean isShortherThan(final CharSequence charSequence, final int length) { return charSequence != null && charSequence.length() < length; } /** @param charSequence will be validated. Can be null. * @param length required length. * @return true if passed sequence is not null and its length is higher than passed value. */ public static boolean isLongerThan(final CharSequence charSequence, final int length) { return charSequence != null && charSequence.length() > length; } /** @return true if the passed sequence starts with the given character. */ public static boolean startsWith(final CharSequence charSequence, final char character) { return charSequence != null && charSequence.length() > 0 && charSequence.charAt(0) == character; } /** @return true if the passed sequence starts with the given characters. */ public static boolean startsWith(final CharSequence charSequence, final char character0, final char character1) { return charSequence != null && charSequence.length() > 1 && charSequence.charAt(0) == character0 && charSequence.charAt(1) == character1; } /** @return true if the passed sequence starts with the given characters. */ public static boolean startsWith(final CharSequence charSequence, final char character0, final char character1, final char character2) { return charSequence != null && charSequence.length() > 2 && charSequence.charAt(0) == character0 && charSequence.charAt(1) == character1 && charSequence.charAt(2) == character2; } /** @return true if the passed sequence starts with the given characters. */ public static boolean startsWith(final CharSequence charSequence, final char character0, final char character1, final char character2, final char character3) { return charSequence != null && charSequence.length() > 3 && charSequence.charAt(0) == character0 && charSequence.charAt(1) == character1 && charSequence.charAt(2) == character2 && charSequence.charAt(3) == character3; } /** @return true if the passed sequence starts with the given characters. */ public static boolean startsWith(final CharSequence charSequence, final char... characters) { if (charSequence == null) { return false; } if (charSequence.length() >= characters.length) { for (int index = 0; index < characters.length; index++) { if (charSequence.charAt(index) != characters[index]) { return false; } } return true; } return false; } /** @return true if the passed sequence ends with the given character. */ public static boolean endsWith(final CharSequence charSequence, final char character) { return charSequence != null && charSequence.length() > 0 && charSequence.charAt(charSequence.length() - 1) == character; } /** @return true if the passed sequence ends with the given characters. */ public static boolean endsWith(final CharSequence charSequence, final char character0, final char character1) { return charSequence != null && charSequence.length() > 1 && charSequence.charAt(charSequence.length() - 1) == character1 && charSequence.charAt(charSequence.length() - 2) == character0; } /** @return true if the passed sequence ends with the given characters. */ public static boolean endsWith(final CharSequence charSequence, final char character0, final char character1, final char character2) { return charSequence != null && charSequence.length() > 2 && charSequence.charAt(charSequence.length() - 1) == character2 && charSequence.charAt(charSequence.length() - 2) == character1 && charSequence.charAt(charSequence.length() - 3) == character0; } /** @return true if the passed sequence ends with the given characters. */ public static boolean endsWith(final CharSequence charSequence, final char character0, final char character1, final char character2, final char character3) { return charSequence != null && charSequence.length() > 3 && charSequence.charAt(charSequence.length() - 1) == character3 && charSequence.charAt(charSequence.length() - 2) == character2 && charSequence.charAt(charSequence.length() - 3) == character1 && charSequence.charAt(charSequence.length() - 4) == character0; } /** @return true if the passed sequence ends with the given characters. */ public static boolean endsWith(final CharSequence charSequence, final char... characters) { if (charSequence == null) { return false; } if (charSequence.length() >= characters.length) { for (int index = 0, modifier = charSequence.length() - characters.length; index < characters.length; index++) { if (charSequence.charAt(index + modifier) != characters[index]) { return false; } } return true; } return false; } /** @param charSequence cannot be null. * @return last character index in the passed sequence. */ public static int getLastIndex(final CharSequence charSequence) { return charSequence.length() - 1; } /** @return true if the given sequence contains selected character. */ public static boolean contains(final CharSequence charSequence, final char character) { for (int index = 0, length = charSequence.length(); index < length; index++) { if (charSequence.charAt(index) == character) { return true; } } return false; } /** @param stringBuilder will have its length set as 0. */ public static void clearBuilder(final com.badlogic.gdx.utils.StringBuilder stringBuilder) { stringBuilder.setLength(0); } /** @param stringBuilder will have its length set as 0. */ public static void clearBuilder(final StringBuilder stringBuilder) { stringBuilder.setLength(0); } /** As opposed to string's split, this method allows to split a char sequence without a regex, provided that the * separator is a single character. Since it does not require pattern compiling and is a simple iteration, this * method is preferred if it can be used. * * @param charSequence will be split. * @param separator character that will be used to split the sequence. * @return array of strings. Is never null - if an empty or null sequence is passed, empty array will be * returned. */ public static String[] split(final CharSequence charSequence, final char separator) { if (isEmpty(charSequence)) { return EMPTY_ARRAY; } final int originalSeparatorsCount = countSeparatedCharAppearances(charSequence, separator); int separatorsCount = originalSeparatorsCount; // If the sequence starts or ends with the separator (and its not the same char index), we don't need as many // characters. if (startsWith(charSequence, separator)) { separatorsCount--; } if (charSequence.length() > 1 && endsWith(charSequence, separator)) { // Length has to be at least 2, as we want to check two different chars and subtract count only once. separatorsCount--; } if (separatorsCount <= 0) { if (originalSeparatorsCount == 0) { // No separators at all. return new String[] { charSequence.toString() }; } else if (isSameChar(charSequence)) { // We've confirmed that the sequence contains a separator and consists of only 1 type of character. It // means that the whole string is made of separators, so we're returning empty array. return EMPTY_ARRAY; } // No separators inside the sequence, but found on edges. Returning the whole string, possibly with stripped // separators. return new String[] { removeCharacter(charSequence.toString(), separator) }; } final String[] result = new String[separatorsCount + 1]; int currentResultIndex = 0; final StringBuilder builder = new StringBuilder(); for (int index = 0, length = charSequence.length(); index < length; index++) { final char character = charSequence.charAt(index); if (character == separator) { if (isNotEmpty(builder)) { result[currentResultIndex++] = builder.toString(); clearBuilder(builder); } } else { builder.append(character); } } if (isNotEmpty(builder)) { // The whole thing may not end with a separator, so we're appending whatever we got left. result[currentResultIndex++] = builder.toString(); } return result; } /** @param charSequence can contain the separators. * @param separator will be searched for in the sequence. * @return sequence separated into parts between separators. As opposed to {@link #split(CharSequence, char)}, this * method does not merge multiple separators next to each other - instead, it will add empty strings to the * result. This can be useful to separate files by new lines, for example. */ public static String[] separate(final CharSequence charSequence, final char separator) { if (isEmpty(charSequence)) { return EMPTY_ARRAY; } final int separatorsCount = countCharAppearances(charSequence, separator); if (separatorsCount == 0) { // No separators at all. return new String[] { charSequence.toString() }; } final String[] result = new String[separatorsCount + 1]; int currentResultIndex = 0; final StringBuilder builder = new StringBuilder(); for (int index = 0, length = charSequence.length(); index < length; index++) { final char character = charSequence.charAt(index); if (character == separator) { result[currentResultIndex++] = builder.toString(); clearBuilder(builder); } else { builder.append(character); } } result[currentResultIndex++] = builder.toString(); return result; } /** @param firstChar first character to compare. * @param secondChar second character to compare. * @return true if characters are equal, ignoring case. */ public static boolean compareIgnoreCase(final char firstChar, final char secondChar) { return Character.toLowerCase(firstChar) == Character.toLowerCase(secondChar); } /** @param firstCharSequence first sequence to compare. * @param secondCharSequence second sequence to compare. * @return true if characters represented by the sequences are equal, ignoring case. */ public static boolean compareIgnoreCase(final CharSequence firstCharSequence, final CharSequence secondCharSequence) { if (firstCharSequence == secondCharSequence) { return true; } if (isEmpty(firstCharSequence)) { return isEmpty(secondCharSequence); } else if (isEmpty(secondCharSequence)) { return false; } if (firstCharSequence.length() != secondCharSequence.length()) { return false; } for (int index = 0, length = firstCharSequence.length(); index < length; index++) { if (!compareIgnoreCase(firstCharSequence.charAt(index), secondCharSequence.charAt(index))) { return false; } } return true; } /** @param charSequence might contain undesired character. * @param characterToRemove all appearances of this character will be removed. * @return a string which does not contain the passed character. */ public static String stripCharacter(final CharSequence charSequence, final char characterToRemove) { if (contains(charSequence, characterToRemove)) { return removeCharacter(charSequence, characterToRemove); } return charSequence.toString(); } private static String removeCharacter(final CharSequence charSequence, final char characterToRemove) { final StringBuilder builder = new StringBuilder(charSequence.length()); for (int index = 0, length = charSequence.length(); index < length; index++) { final char character = charSequence.charAt(index); if (character != characterToRemove) { builder.append(character); } } return builder.toString(); } /** @param charSequence may consist of a single repeated character. * @return true if all characters in the sequence are equals or the sequence is empty. */ public static boolean isSameChar(final CharSequence charSequence) { if (Strings.isEmpty(charSequence)) { return true; } int index = 0; final char character = charSequence.charAt(index++); for (final int length = charSequence.length(); index < length; index++) { if (charSequence.charAt(index) != character) { return false; } } return true; } /** @param charSequence may contain the passed character. * @param value character to look for. * @return amount of appearances of the selected character in the passed sequence. */ public static int countCharAppearances(final CharSequence charSequence, final char value) { int count = 0; for (int index = 0, length = charSequence.length(); index < length; index++) { if (charSequence.charAt(index) == value) { count++; } } return count; } /** @param charSequence may contain the passed character. * @param value character to look for. * @return amount of appearances of the selected character in the passed sequence. If multiple characters with the * same value are neighbors to each other, only one of them is counted. This method might be useful for * splitting strings by characters rather than regexes. */ public static int countSeparatedCharAppearances(final CharSequence charSequence, final char value) { int count = 0; for (int index = 0, length = charSequence.length(); index < length; index++) { if (charSequence.charAt(index) == value && (index == 0 || charSequence.charAt(index - 1) != value)) { count++; } } return count; } /** @param separator will be used to separate joined strings from each other. Passing null or empty string will * result in merging strings without any separation. * @param objectsToJoin will be converted to strings and joined using the selected separator. Nulls are converted * into "null" strings. * @return passed objects as strings joined into one object. Note that this string will never be null - if * objectsToJoin are empty, an empty string is returned. */ public static String join(final CharSequence separator, final Object... objectsToJoin) { if (objectsToJoin == null || objectsToJoin.length == 0) { return EMPTY_STRING; } if (objectsToJoin.length == 1) { // Avoiding unnecessary operations. return objectsToJoin[0] == null ? Nullables.DEFAULT_NULL_STRING : objectsToJoin[0].toString(); } final StringBuilder stringBuilder = new StringBuilder(); if (isEmpty(separator)) { // No separator - merging strings. for (final Object element : objectsToJoin) { stringBuilder.append(element); } return stringBuilder.toString(); } // A separator is selected - joining strings with selected separator. int index = 0; stringBuilder.append(objectsToJoin[index++]); for (; index < objectsToJoin.length; index++) { stringBuilder.append(separator); stringBuilder.append(objectsToJoin[index]); } return stringBuilder.toString(); } /** @param separator will be used to separate joined strings from each other. Passing null or empty string will * result in merging strings without any separation. * @param objectsToJoin will be converted to strings and joined using the selected separator. Nulls are converted * into "null" strings. * @return passed objects as strings joined into one object. Note that this string will never be null - if * objectsToJoin are empty, an empty string is returned. */ public static String join(final CharSequence separator, final Iterable<?> objectsToJoin) { if (objectsToJoin == null) { return EMPTY_STRING; } final StringBuilder stringBuilder = new StringBuilder(); if (isEmpty(separator)) { // No separator - merging strings. for (final Object element : objectsToJoin) { stringBuilder.append(element); } return stringBuilder.toString(); } // A separator is selected - joining strings with selected separator. int index = 0; for (final Object object : objectsToJoin) { if (index++ > 0) { stringBuilder.append(separator); } stringBuilder.append(object); } return stringBuilder.toString(); } /** @param objectsToMerge will be converted and merged into one string, without using any separator. Note that nulls * will be added as "null" strings. * @return merged objects as strings. Is never null - for empty objectsToMerge, returns empty string. Equivalent to * using join method with null or empty separator. */ public static String merge(final Object... objectsToMerge) { return join(null, objectsToMerge); } /** @param separator will be used to separate joined strings from each other. Passing null or empty string will * result in merging strings without any separation. * @param objectsToJoin will be converted to strings and joined using the selected separator. Nulls are completely * ignored - they are not added to the string and do not invoke adding separators. * @return passed objects joined as strings into one object. Note that this string will never be null - if * objectsToJoin are empty, an empty string is returned. */ public static String joinIgnoringNulls(final CharSequence separator, final Object... objectsToJoin) { if (objectsToJoin == null || objectsToJoin.length == 0) { return EMPTY_STRING; } if (objectsToJoin.length == 1) { // Avoiding unnecessary operations. return objectsToJoin[0] == null ? EMPTY_STRING : objectsToJoin[0].toString(); } final StringBuilder stringBuilder = new StringBuilder(); if (isEmpty(separator)) { // No separator - merging strings. for (final Object element : objectsToJoin) { if (element != null) { stringBuilder.append(element); } } return stringBuilder.toString(); } // A separator is selected - joining strings with selected separator. int index = 0; for (; index < objectsToJoin.length; index++) { if (objectsToJoin[index] != null) { stringBuilder.append(objectsToJoin[index]); break; } } for (; index < objectsToJoin.length; index++) { if (objectsToJoin[index] == null) { continue; } stringBuilder.append(separator); stringBuilder.append(objectsToJoin[index]); } return stringBuilder.toString(); } /** @param objectsToMerge will be converted and merged into one string, without using any separator. Note that nulls * will not be added at all. * @return merged objects as strings. Is never null - for empty objectsToMerge, returns empty string. Equivalent to * using joinIgnoringNulls method with null or empty separator. */ public static String mergeIgnoringNulls(final Object... objectsToMerge) { return joinIgnoringNulls(null, objectsToMerge); } /** @return nullable object converted to string. If parameter is null, empty string is returned. As long as toString * is properly implemented in the object, this method never returns null. */ public static String toString(final Object nullable) { return nullable == null ? EMPTY_STRING : nullable.toString(); } /** @return nullable object converted to string. If first parameter is null, onNull parameter is returned. */ public static String toString(final Object nullable, final String onNull) { return nullable == null ? onNull : nullable.toString(); } /** @return true if the passed charSequence contains legal characters for an int. Note that the value of int is not * validated and can be too big or small. */ public static boolean isInt(final CharSequence charSequence) { if (isEmpty(charSequence)) { return false; } int index = 0; if (charSequence.charAt(0) == '-') { if (charSequence.length() > 1) { index++; } else { return false; } } for (final int length = charSequence.length(); index < length; index++) { final char character = charSequence.charAt(index); if (character < '0' || character > '9') { return false; } } return true; } /** @return true if the passed charSequence contains legal characters for a float. Note that the value of float is * not validated and can be too big or small. */ public static boolean isFloat(final CharSequence charSequence) { if (Strings.isEmpty(charSequence)) { return false; } boolean foundDot = false; for (int index = 0, length = charSequence.length(); index < length; index++) { final char character = charSequence.charAt(index); if (character < '0' || character > '9') { if (length > 1) { if (index == 0 && character == '-') { continue; } else if (!foundDot && character == '.') { foundDot = true; continue; } else if ((character == 'f' || character == 'F') && index + 1 == length) { return true; } } return false; } } return true; } /** @param value can be null. * @return true only if the value matches "true" or "equals" ignoring case. If the value has at least one whitespace * char, false will be returned. This check should be generally cheaper than * {@link String#equalsIgnoreCase(String)}, as it does not iterate over chars and works directly on char * sequence methods. */ public static boolean isBoolean(final CharSequence value) { if (value == null) { return false; } if (value.length() == 4) { final char t = value.charAt(0); final char r = value.charAt(1); final char u = value.charAt(2); final char e = value.charAt(3); return (t == 't' || t == 'T') && (r == 'r' || r == 'R') && (u == 'u' || u == 'U') && (e == 'e' || e == 'E'); } else if (value.length() == 5) { final char f = value.charAt(0); final char a = value.charAt(1); final char l = value.charAt(2); final char s = value.charAt(3); final char e = value.charAt(4); return (f == 'f' || f == 'F') && (a == 'a' || a == 'A') && (l == 'l' || l == 'L') && (s == 's' || s == 'S') && (e == 'e' || e == 'E'); } return false; } /** @return a new array with the passed values. Can be empty, but is never null. */ public static String[] newArray(final String... values) { if (values == null) { return new String[0]; } return values; } /** Null-safe compare. * * @param first first value to check. * @param second second value to check. * @return if both values are null or if values are equal to each other. */ public static boolean equals(final CharSequence first, final CharSequence second) { return first == second || first != null && first.equals(second); } }