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() {}
}