Strings.java
package org.codefilarete.tool;
import javax.annotation.Nonnegative;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.codefilarete.tool.bean.Objects;
import static java.lang.Character.isLowerCase;
import static java.lang.Character.isUpperCase;
import static java.lang.Character.toLowerCase;
import static java.lang.Character.toUpperCase;
/**
* @author Guillaume Mary
*/
public abstract class Strings {
public static boolean isEmpty(CharSequence charSequence) {
return charSequence == null || charSequence.length() == 0;
}
public static <C extends CharSequence> C preventEmpty(C charSequence, C replacement) {
return isEmpty(charSequence) ? replacement : charSequence;
}
public static String capitalize(final CharSequence cs) {
return (String) doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
@Override
public CharSequence onNotNullNotEmpty(CharSequence cs) {
return toUpperCase(cs.charAt(0)) + cs.subSequence(1, cs.length()).toString();
}
});
}
public static String uncapitalize(final CharSequence cs) {
return (String) doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
@Override
public CharSequence onNotNullNotEmpty(CharSequence cs) {
return toLowerCase(cs.charAt(0)) + cs.subSequence(1, cs.length()).toString();
}
});
}
/**
* Converts the given input to a snake case string. For instance, "HelloWorld" becomes "hello_world".
* Continuous uppercase characters are converted to lowercase with an underscore prefix: "HelloWORLD" becomes "hello_world".
*
* @param input any {@link String}
* @return a snake case version of given input {@link String}.
*/
public static String snakeCase(String input) {
if (input.isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
char[] charArray = input.toCharArray();
char firstChar = charArray[0];
result.append(isUpperCase(firstChar) ? toLowerCase(firstChar) : firstChar);
for (int i = 1, charArrayLength = charArray.length; i < charArrayLength; i++) {
char c = charArray[i];
if (isUpperCase(c)) {
// we add '_' only if the previous character is lower case, and we avoid duplicating '_'
if (isLowerCase(charArray[i - 1]) && charArray[i - 1] != '_') {
result.append("_");
}
result.append(toLowerCase(c));
} else {
result.append(c);
}
}
if (result.length() > 0 && result.charAt(0) == '_') {
result.deleteCharAt(0);
}
return result.toString();
}
/**
* Concatenate count (positive) times parameter s.
* Optional Strings in prebuildStrings are used to speed concatenation for large count numbers if you already have
* large snippets of s pre-concatenated. For instance, you want 3456 times "a" and you already got constants with
* a*500, a*100, a*10, then this method will only cat 6*a*500, 4*a*100, 5*a*10 and 6*a. Instead of 3456 times "a".
*
* @param count expected repetition of s
* @param s the String to be concatenated
* @param prebuiltStrings optional pre-concatenated "s" strings, <b>in descent size order</b>.
* @return s repeated count times
*/
public static StringBuilder repeat(int count, CharSequence s, String... prebuiltStrings) {
StringBuilder result = new StringBuilder(count * s.length());
repeat(result, count, s, prebuiltStrings);
return result;
}
/**
* Concatenates count (positive) times parameter s.
* Optional Strings in prebuildStrings are used to speed concatenation for large count numbers if you already have
* large snippets of s pre-concatenated. For instance, you want 3456 times "a" and you already got constants with
* a*500, a*100, a*10, then this method will only cat 6*a*500, 4*a*100, 5*a*10 and 6*a. Instead of 3456 times "a".
*
* @param result destination of the concatenation
* @param count expected repetition of s
* @param s the {@link CharSequence} to be concatenated to result
* @param prebuiltStrings optional pre-concatenated "s" strings, <strong>in descent size order</strong>.
* @return result with s repeated count times appended
*/
public static StringBuilder repeat(StringBuilder result, int count, CharSequence s, CharSequence... prebuiltStrings) {
result.ensureCapacity(result.length() + count * s.length()); // to avoid extra allocation cycles
int snippetCount, remainer = count;
for (CharSequence snippet : prebuiltStrings) {
int snippetLength = snippet.length();
snippetCount = remainer / snippetLength;
for (int i = 0; i < snippetCount; i++) {
result.append(snippet);
}
remainer = remainer % snippetLength;
}
for (int i = 0; i < remainer; i++) {
result.append(s);
}
return result;
}
/**
* Equivalent to {@link String#split(String)} without regexp which is unnecessary for char separator
* @param stringToBeSplit
* @param separator
* @param keepSeparatorInResult
* @return a {@link String} split into pieces each time separator is found in it
*/
public static List<String> split(String stringToBeSplit, char separator, boolean keepSeparatorInResult) {
List<String> result = new ArrayList<>();
int separatorIndex = stringToBeSplit.indexOf(separator);
int previousSeparatorIndex = 0;
int substringPadding = keepSeparatorInResult ? 1 : 0;
while (separatorIndex != -1) {
result.add(stringToBeSplit.substring(previousSeparatorIndex, separatorIndex + substringPadding));
previousSeparatorIndex = separatorIndex + 1;
separatorIndex = stringToBeSplit.indexOf(separator, previousSeparatorIndex);
}
if (previousSeparatorIndex < stringToBeSplit.length()) {
result.add(stringToBeSplit.substring(previousSeparatorIndex));
}
return result;
}
public static CharSequence head(@Nullable CharSequence cs, @Nonnegative int headSize) {
return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
@Override
public CharSequence onNotNullNotEmpty(CharSequence cs) {
return cs.subSequence(0, Math.min(cs.length(), headSize));
}
});
}
public static CharSequence head(@Nullable String cs, String untilIncluded) {
return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
@Override
public CharSequence onNotNullNotEmpty(CharSequence cs) {
int index = ((String) cs).indexOf(untilIncluded);
return cs.subSequence(0, Math.min(cs.length(), Objects.fallback(index, -1, 0)));
}
});
}
public static CharSequence cutHead(@Nullable CharSequence cs, @Nonnegative int headSize) {
return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
@Override
public CharSequence onNotNullNotEmpty(CharSequence cs) {
return cs.subSequence(Math.min(headSize, cs.length()), cs.length());
}
});
}
public static CharSequence tail(@Nullable CharSequence cs, @Nonnegative int tailSize) {
return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
@Override
public CharSequence onNotNullNotEmpty(CharSequence cs) {
return cs.subSequence(Math.max(0, cs.length() - tailSize), cs.length());
}
});
}
/**
* Gives the tailing {@link String} of a {@link CharSequence} that occurs after the last occurrence of given {@link String}
* @param cs any {@link CharSequence}, even null
* @param afterLast the occurring {@link String} to be found near the end
* @return the tailing {@link String} of given {@link CharSequence} that occurs after the last occurrence of given {@link String}
*/
public static CharSequence tail(@Nullable CharSequence cs, String afterLast) {
return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
@Override
public CharSequence onNotNullNotEmpty(CharSequence cs) {
return cs.subSequence(Math.max(0, cs.toString().lastIndexOf(afterLast) + afterLast.length()), cs.length());
}
});
}
public static CharSequence cutTail(@Nullable CharSequence cs, @Nonnegative int tailSize) {
return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
@Override
public CharSequence onNotNullNotEmpty(CharSequence cs) {
return cs.subSequence(0, preventNegative(cs.length() - tailSize));
}
});
}
/**
* Cuts a {@link CharSequence} and appends 3 dots ("...") at the end if its length is strictly greater than length
*
* @param cs any {@link CharSequence}
* @param length length at which given {@link CharSequence} must be cut and appended "..."
* @return length-firsts characters of given {@link CharSequence} appended with "...", therefore its size is length+3
*/
public static CharSequence ellipsis(@Nullable CharSequence cs, @Nonnegative int length) {
return doWithDelegate(cs, new DefaultNullOrEmptyDelegate() {
@Override
public CharSequence onNotNullNotEmpty(CharSequence cs) {
if (cs.length() > length) {
return cs.subSequence(0, length) + "...";
} else {
return cs;
}
}
});
}
/**
* @param i any integer
* @return 0 if i < 0
*/
private static int preventNegative(int i) {
return Math.max(i, 0);
}
private static CharSequence doWithDelegate(@Nullable CharSequence cs, INullOrEmptyDelegate emptyDelegate) {
if (cs == null) {
return emptyDelegate.onNull();
} else if (cs.length() == 0) {
return emptyDelegate.onEmpty();
} else {
return emptyDelegate.onNotNullNotEmpty(cs);
}
}
/**
* Give a printable view of an object through method references of any of its properties. These will be concatenated to each other
* with comma (", ").
* Result of method references are printed by {@link StringBuilder#append(Object)} contract.
*
* @param object any object (not null)
* @param printableProperties functions that give a properties to be concatenated
* @param <O> object type
* @return the concatenation of the results of functions invocation on the given object
*/
@SafeVarargs
public static <O> String footPrint(O object, Function<O, ?> ... printableProperties) {
StringAppender result = new StringAppender();
for (Function<O, ?> printableProperty : printableProperties) {
result.cat(printableProperty.apply(object), ", ");
}
return result.cutTail(2).toString();
}
private interface INullOrEmptyDelegate {
CharSequence onNull();
CharSequence onEmpty();
CharSequence onNotNullNotEmpty(CharSequence cs);
}
private static abstract class DefaultNullOrEmptyDelegate implements INullOrEmptyDelegate {
@Override
public CharSequence onNull() {
return null;
}
@Override
public CharSequence onEmpty() {
return "";
}
}
private Strings() {}
}