StringKit.java

package space.sunqian.common.base.string;

import space.sunqian.annotations.Nonnull;
import space.sunqian.annotations.Nullable;
import space.sunqian.common.Check;
import space.sunqian.common.base.chars.CharsKit;
import space.sunqian.common.base.exception.UnknownArrayTypeException;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Objects;

/**
 * Utilities kit for string related.
 *
 * @author sunqian
 */
public class StringKit {

    /**
     * Returns whether the given string is {@code null} or empty.
     *
     * @param str the given string
     * @return whether the given string is {@code null} or empty
     */
    public static boolean isEmpty(@Nullable CharSequence str) {
        return str == null || str.length() == 0;
    }

    /**
     * Returns whether the given string is not {@code null} nor empty.
     *
     * @param str the given string
     * @return whether the given string is not {@code null} nor empty
     */
    public static boolean isNonEmpty(@Nullable CharSequence str) {
        return !isEmpty(str);
    }

    /**
     * Returns whether the given string is blank ({@code null}, empty or whitespace).
     *
     * @param str the given string
     * @return whether the given string is blank
     */
    public static boolean isBlank(@Nullable CharSequence str) {
        if (isEmpty(str)) {
            return true;
        }
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (!Character.isWhitespace(c)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns whether the given string is not blank ({@code null} nor empty nor whitespace).
     *
     * @param str the given string
     * @return whether the given string is not blank
     */
    public static boolean isNonBlank(@Nullable CharSequence str) {
        return !isBlank(str);
    }

    /**
     * Returns {@code true} if any of the given strings is {@code null} or empty, otherwise {@code false}.
     *
     * @param strings the given strings
     * @return {@code true} if any of the given strings is {@code null} or empty, otherwise {@code false}
     */
    public static boolean anyEmpty(@Nullable CharSequence @Nonnull ... strings) {
        for (CharSequence str : strings) {
            if (isEmpty(str)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns {@code true} if all the given strings are {@code null} or empty, otherwise {@code false}.
     *
     * @param strings the given strings
     * @return {@code true} if all the given strings are {@code null} or empty, otherwise {@code false}
     */
    public static boolean allEmpty(@Nullable CharSequence @Nonnull ... strings) {
        for (CharSequence str : strings) {
            if (!isEmpty(str)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns {@code true} if any of the given strings is blank, otherwise {@code false}. This method uses
     * {@link #isBlank(CharSequence)} to check whether a string is blank.
     *
     * @param strings the given strings
     * @return {@code true} if any of the given strings is blank, otherwise {@code false}
     */
    public static boolean anyBlank(@Nullable CharSequence @Nonnull ... strings) {
        for (CharSequence str : strings) {
            if (isBlank(str)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns {@code true} if all the given strings are blank, otherwise {@code false}. This method uses
     * {@link #isBlank(CharSequence)} to check whether a string is blank.
     *
     * @param strings the given strings
     * @return {@code true} if all the given strings are blank, otherwise {@code false}
     */
    public static boolean allBlank(@Nullable CharSequence @Nonnull ... strings) {
        for (CharSequence str : strings) {
            if (!isBlank(str)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Encodes the given char array into a new byte array using {@link CharsKit#defaultCharset()}.
     *
     * @param chars the given char array
     * @return a new byte array contains the encoded bytes
     */
    public static byte @Nonnull [] toBytes(char @Nonnull [] chars) {
        return toBytes(chars, CharsKit.defaultCharset());
    }

    /**
     * Encodes the given char array into a new byte array using the specified charset.
     *
     * @param chars   the given char array
     * @param charset the specified charset
     * @return a new byte array contains the encoded bytes
     */
    public static byte @Nonnull [] toBytes(char @Nonnull [] chars, Charset charset) {
        return new String(chars).getBytes(charset);
    }

    /**
     * Encodes the given char sequence into a new byte array using {@link CharsKit#defaultCharset()}.
     *
     * @param chars the given char sequence
     * @return a new byte array contains the encoded bytes
     */
    public static byte @Nonnull [] toBytes(@Nonnull CharSequence chars) {
        return toBytes(chars, CharsKit.defaultCharset());
    }

    /**
     * Encodes the given char sequence into a new byte array using the specified charset.
     *
     * @param chars   the given char sequence
     * @param charset the specified charset
     * @return a new byte array contains the encoded bytes
     */
    public static byte @Nonnull [] toBytes(@Nonnull CharSequence chars, Charset charset) {
        return chars.toString().getBytes(charset);
    }

    /**
     * Returns whether all chars of the given string are upper case. Note if the string is empty, returns {@code true}.
     *
     * @param str the given string
     * @return whether all chars of the given string are upper case
     */
    public static boolean allUpperCase(@Nonnull CharSequence str) {
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (!Character.isUpperCase(c)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns whether all chars of the given string are lower case. Note if the string is empty, returns {@code true}.
     *
     * @param str the given string
     * @return whether all chars of the given string are lower case
     */
    public static boolean allLowerCase(@Nonnull CharSequence str) {
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (!Character.isLowerCase(c)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Capitalizes the given string.
     *
     * @param str the given string
     * @return the capitalized string
     */
    public static @Nonnull String capitalize(@Nonnull CharSequence str) {
        if (str.length() == 0) {
            return str.toString();
        }
        if (str.length() == 1) {
            char c = str.charAt(0);
            if (Character.isLowerCase(c)) {
                return String.valueOf(Character.toUpperCase(c));
            }
        }
        char c = str.charAt(0);
        if (Character.isLowerCase(c)) {
            StringBuilder sb = new StringBuilder(str.length());
            sb.append(Character.toUpperCase(c));
            sb.append(str, 1, str.length());
            return sb.toString();
        }
        return str.toString();
    }

    /**
     * Uncapitalizes the given string. Specifically, if the given string's length {@code >= 2} and its chars are all
     * upper case, the original string is also returned.
     *
     * @param str the given string
     * @return the uncapitalized string
     */
    public static @Nonnull String uncapitalize(@Nonnull CharSequence str) {
        if (str.length() == 1) {
            char c = str.charAt(0);
            if (Character.isUpperCase(c)) {
                return String.valueOf(Character.toLowerCase(c));
            }
        }
        if (allUpperCase(str)) {
            return str.toString();
        }
        char c = str.charAt(0);
        if (Character.isUpperCase(c)) {
            StringBuilder sb = new StringBuilder(str.length());
            sb.append(Character.toLowerCase(c));
            sb.append(str, 1, str.length());
            return sb.toString();
        }
        return str.toString();
    }

    /**
     * Returns a String of which content is upper case of the given string.
     *
     * @param str the given string
     * @return the converted string
     */
    public static @Nonnull String upperCase(@Nonnull CharSequence str) {
        if (str.length() == 0) {
            return str.toString();
        }
        char[] cs = new char[str.length()];
        for (int i = 0; i < str.length(); i++) {
            cs[i] = Character.toUpperCase(str.charAt(i));
        }
        return new String(cs);
    }

    /**
     * Returns a String of which content is lower case of the given string.
     *
     * @param str the given string
     * @return the converted string
     */
    public static @Nonnull String lowerCase(@Nonnull CharSequence str) {
        if (str.length() == 0) {
            return str.toString();
        }
        char[] cs = new char[str.length()];
        for (int i = 0; i < str.length(); i++) {
            cs[i] = Character.toLowerCase(str.charAt(i));
        }
        return new String(cs);
    }

    /**
     * Returns the first index of the specified char in the specified string. The returned value is the smallest value
     * {@code k} such that:
     * <pre>{@code
     * (str.charAt(k) == ch) && (k >= 0)
     * }</pre>
     * If no such {@code k} is found, returns {@code -1}.
     *
     * @param str the specified string
     * @param c   the specified char
     * @return the first index of the specified char in the specified string, or {@code -1} if not found
     */
    public static int indexOf(CharSequence str, char c) {
        return indexOf(str, c, 0);
    }

    /**
     * Returns the first index of the specified char in the specified string, starting at the specified index. The
     * returned value is the smallest value {@code k} such that:
     * <pre>{@code
     * (str.charAt(k) == ch) && (k >= index)
     * }</pre>
     * If no such {@code k} is found, returns {@code -1}.
     * <p>
     * There is no restriction on the {@code index}. If it is negative, it has the same effect as if it were {@code 0};
     * if it is greater than {@code str.length()}, it has the same effect as if it were {@code str.length()} ({@code -1}
     * is returned).
     *
     * @param str   the specified string
     * @param c     the specified char
     * @param index the specified index
     * @return the first index of the specified char in the specified string, starting at the specified index, or
     * {@code -1} if not found
     */
    public static int indexOf(CharSequence str, char c, int index) {
        if (str instanceof String) {
            return ((String) str).indexOf(c, index);
        }
        if (index >= str.length()) {
            return -1;
        }
        int s = Math.max(0, index);
        for (int i = s; i < str.length(); i++) {
            if (str.charAt(i) == c) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Returns the last index of the specified char in the specified string, searching backward. The returned value is
     * the largest value {@code k} such that:
     * <pre>{@code
     * (str.charAt(k) == ch) && (k <= str.length() - 1)
     * }</pre>
     * If no such {@code k} is found, returns {@code -1}.
     *
     * @param str the specified string
     * @param c   the specified char
     * @return the last index of the specified char in the specified string, searching backward, or {@code -1} if not
     * found
     */
    public static int lastIndexOf(CharSequence str, char c) {
        return lastIndexOf(str, c, str.length() - 1);
    }

    /**
     * Returns the last index of the specified char in the specified string, searching backward starting at the
     * specified index. The returned value is the largest value {@code k} such that:
     * <pre>{@code
     * (str.charAt(k) == ch) && (k <= index)
     * }</pre>
     * If no such {@code k} is found, returns {@code -1}.
     * <p>
     * There is no restriction on the {@code index}. If it is negative, it has the same effect as if it were {@code -1}
     * ({@code -1} is returned); if it is greater than or equal to {@code str.length()}, it has the same effect as if it
     * were {@code str.length() - 1}.
     *
     * @param str   the specified string
     * @param c     the specified char
     * @param index the specified index
     * @return the last index of the specified char in the specified string, searching backward starting at the
     * specified index, or {@code -1} if not found
     */
    public static int lastIndexOf(CharSequence str, char c, int index) {
        if (str instanceof String) {
            return ((String) str).lastIndexOf(c, index);
        }
        if (index < 0) {
            return -1;
        }
        int s = Math.min(str.length() - 1, index);
        for (int i = s; i >= 0; i--) {
            if (str.charAt(i) == c) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Returns the first index of the specified substring in the specified string. The returned value is the smallest
     * value {@code k} for which:
     * <pre>{@code
     * (k >= 0) && str.startsWith(sub, k)
     * }</pre>
     * If no such {@code k} is found, returns {@code -1}.
     * <p>
     * The behavior of this method is the same as {@link String#indexOf(String)}.
     *
     * @param str the specified string
     * @param sub the specified substring
     * @return the first index of the specified substring in the specified string, or {@code -1} if not found
     */
    public static int indexOf(CharSequence str, CharSequence sub) {
        return indexOf(str, sub, 0);
    }

    /**
     * Returns the first index of the specified substring in the specified string, starting at the specified index. The
     * returned value is the smallest value {@code k} for which:
     * <pre>{@code
     * (k >= index) && str.startsWith(sub, k)
     * }</pre>
     * If no such {@code k} is found, returns {@code -1}.
     * <p>
     * The behavior of this method is the same as {@link String#indexOf(String, int)}.
     *
     * @param str   the specified string
     * @param sub   the specified substring
     * @param index the specified index
     * @return the first index of the specified substring in the specified string, starting at the specified index, or
     * {@code -1} if not found
     */
    public static int indexOf(CharSequence str, CharSequence sub, int index) {
        if ((str instanceof String) && (sub instanceof String)) {
            return ((String) str).indexOf((String) sub, index);
        }
        int maxIndex = str.length() - sub.length();
        if (index > maxIndex) {
            return (sub.length() == 0 ? str.length() : -1);
        }
        int s = Math.max(index, 0);
        if (sub.length() == 0) {
            return s;
        }
        for (int i = s; i <= maxIndex; i++) {
            if (startsWith(str, sub, i)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Returns the last index of the specified substring in the specified string, searching backward. The returned value
     * is the smallest value {@code k} for which:
     * <pre>{@code
     * (k <= str.length() - 1) && str.startsWith(sub, k)
     * }</pre>
     * If no such {@code k} is found, returns {@code -1}.
     * <p>
     * The behavior of this method is the same as {@link String#lastIndexOf(String)}.
     *
     * @param str the specified string
     * @param sub the specified substring
     * @return the last index of the specified substring in the specified string, searching backward, or {@code -1} if
     * not found
     */
    public static int lastIndexOf(CharSequence str, CharSequence sub) {
        return lastIndexOf(str, sub, str.length());
    }

    /**
     * Returns the last index of the specified substring in the specified string, searching backward starting at the
     * specified index. The returned value is the largest value {@code k} for which:
     * <pre>{@code
     * (k <= index) && str.startsWith(sub, k)
     * }</pre>
     * If no such {@code k} is found, returns {@code -1}.
     * <p>
     * The behavior of this method is the same as {@link String#lastIndexOf(String, int)}.
     *
     * @param str   the specified string
     * @param sub   the specified substring
     * @param index the specified index
     * @return the last index of the specified substring in the specified string, searching backward starting at the
     * specified index, or {@code -1} if not found
     */
    public static int lastIndexOf(CharSequence str, CharSequence sub, int index) {
        if ((str instanceof String) && (sub instanceof String)) {
            return ((String) str).lastIndexOf((String) sub, index);
        }
        if (index < 0) {
            return -1;
        }
        int maxIndex = str.length() - sub.length();
        int s = Math.min(index, maxIndex);
        if (sub.length() == 0) {
            return s;
        }
        for (int i = s; i >= 0; i--) {
            if (startsWith(str, sub, i)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Returns whether the specified string is starts with the specified substring.
     * <p>
     * The behavior of this method is the same as {@link String#startsWith(String)}.
     *
     * @param str the specified string
     * @param sub the specified substring
     * @return whether the specified string is starts with the specified substring
     */
    public static boolean startsWith(@Nonnull CharSequence str, @Nonnull CharSequence sub) {
        return startsWith(str, sub, 0);
    }

    /**
     * Returns whether the specified string is starts with the specified substring, the comparing starting at the
     * specified index.
     * <p>
     * The behavior of this method is the same as {@link String#startsWith(String, int)}.
     *
     * @param str   the specified string
     * @param sub   the specified substring
     * @param index the specified index
     * @return whether the specified string is starts with the specified substring
     */
    public static boolean startsWith(@Nonnull CharSequence str, @Nonnull CharSequence sub, int index) {
        int strLen = str.length();
        int subLen = sub.length();
        if (index < 0 || index > strLen - subLen) {
            return false;
        }
        int l = subLen + index;
        for (int i = index, j = 0; i < l; i++, j++) {
            if (str.charAt(i) != sub.charAt(j)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns whether the specified string is ends with the specified substring.
     * <p>
     * The behavior of this method is the same as {@link String#endsWith(String)}.
     *
     * @param str the specified string
     * @param sub the specified substring
     * @return whether the specified string is ends with the specified substring
     */
    public static boolean endsWith(@Nonnull CharSequence str, @Nonnull CharSequence sub) {
        return startsWith(str, sub, str.length() - sub.length());
    }

    /**
     * Returns the {@code toString} of the given object. If the given object is array, uses {@code Arrays.toString} or
     * {@link Arrays#deepToString(Object[])} if necessary.
     * <p>
     * This method is equivalent to ({@link #toStringWith(Object, boolean, boolean)}):
     * {@code toStringWith(obj, true, true)}.
     *
     * @param obj the given object
     * @return the {@code toString} of the given object
     */
    public static @Nonnull String toString(@Nullable Object obj) {
        return toStringWith(obj, true, true);
    }

    /**
     * Returns the {@code toString} of the given objects via {@link Arrays#deepToString(Object[])}.
     *
     * @param objs the given objects
     * @return the {@code toString} of the given objects via {@link Arrays#deepToString(Object[])}
     */
    public static @Nonnull String toStringAll(@Nullable Object @Nonnull ... objs) {
        return Arrays.deepToString(objs);
    }

    /**
     * Returns the {@code toString} of the given object. This method follows the following logic:
     * <ul>
     *     <li>
     *         If the given object is not an array, returns {@link Objects#toString(Object)}.
     *     </li>
     *     <li>
     *         If the {@code arrayToString} is {@code true}:
     *         <ul>
     *             <li>
     *                 If the {@code deep} is {@code true}, uses {@link Arrays#deepToString(Object[])} for them.
     *                 Otherwise, uses {@code Arrays.toString}.
     *             </li>
     *         </ul>
     *     </li>
     *     <li>
     *         Returns {@link Objects#toString(Object)} otherwise.
     *     </li>
     * </ul>
     *
     * @param obj           the given object
     * @param arrayToString the arrayToString option
     * @param deep          the deep option
     * @return the {@code toString} of the given object
     */
    public static @Nonnull String toStringWith(@Nullable Object obj, boolean arrayToString, boolean deep) {
        if (obj == null || !arrayToString) {
            return Objects.toString(obj);
        }
        Class<?> cls = obj.getClass();
        if (cls.isArray()) {
            return toStringArray(obj, deep);
        }
        return obj.toString();
    }

    private static @Nonnull String toStringArray(@Nonnull Object obj, boolean deep) {
        if (obj instanceof Object[]) {
            return deep ? Arrays.deepToString((Object[]) obj) : Arrays.toString((Object[]) obj);
        }
        if (obj instanceof boolean[]) {
            return Arrays.toString((boolean[]) obj);
        }
        if (obj instanceof byte[]) {
            return Arrays.toString((byte[]) obj);
        }
        if (obj instanceof short[]) {
            return Arrays.toString((short[]) obj);
        }
        if (obj instanceof char[]) {
            return Arrays.toString((char[]) obj);
        }
        if (obj instanceof int[]) {
            return Arrays.toString((int[]) obj);
        }
        if (obj instanceof long[]) {
            return Arrays.toString((long[]) obj);
        }
        if (obj instanceof float[]) {
            return Arrays.toString((float[]) obj);
        }
        if (obj instanceof double[]) {
            return Arrays.toString((double[]) obj);
        }
        throw new UnknownArrayTypeException(obj.getClass());
    }

    /**
     * Compares the given two {@link CharSequence} instances, returns {@code true} if they have same length and two
     * chars at the same index in two sequences are equal, otherwise {@code false}.
     *
     * @param cs1 the first {@link CharSequence} to compare
     * @param cs2 the second {@link CharSequence} to compare
     * @return {@code true} if they have same length and two chars at the same index in two sequences are equal,
     * otherwise {@code false}
     */
    public static boolean charEquals(CharSequence cs1, CharSequence cs2) {
        if (cs1.length() != cs2.length()) {
            return false;
        }
        for (int i = 0; i < cs1.length(); i++) {
            if (cs1.charAt(i) != cs2.charAt(i)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Copies chars from the given string into the specified destination char array. The chars copied starting at the
     * specified start index inclusive, and ending at the specified end index exclusive. The destination array receives
     * starting at the specified offset.
     * <p>
     * The behavior of this method is the same as {@link String#getChars(int, int, char[], int)}.
     *
     * @param str   the given string of which chars will be copied
     * @param start the specified start index, inclusive
     * @param end   the specified end index, exclusive
     * @param dst   the specified destination char array
     * @param off   the specified offset
     * @throws IndexOutOfBoundsException if there exists an arguments is out of bounds
     */
    public static void charsCopy(
        @Nonnull CharSequence str, int start, int end, char @Nonnull [] dst, int off
    ) throws IndexOutOfBoundsException {
        if (str instanceof String) {
            ((String) str).getChars(start, end, dst, off);
        } else {
            Check.checkInBounds(start, end, 0, str.length());
            Check.checkInBounds(off, off + end - start, 0, dst.length);
            if (start == end) {
                return;
            }
            for (int i = 0; i < end - start; i++) {
                dst[off + i] = str.charAt(start + i);
            }
        }
    }

    /**
     * Copies the specified length of chars from the given source, starting at the specified source offset, to the given
     * destination array, starting at the specified destination offset.
     * <p>
     * The behavior of this method is the same as {@link System#arraycopy(Object, int, Object, int, int)}.
     *
     * @param src    the given source
     * @param srcOff the specified source offset
     * @param dst    the given destination array
     * @param dstOff the specified destination offset
     * @param len    the specified copy length
     * @throws IndexOutOfBoundsException if there exists an arguments is out of bounds
     */
    public static void charsCopy(
        @Nonnull CharSequence src,
        int srcOff,
        char @Nonnull [] dst,
        int dstOff,
        int len
    ) throws IndexOutOfBoundsException {
        if (src instanceof String) {
            ((String) src).getChars(srcOff, srcOff + len, dst, dstOff);
        } else {
            Check.checkOffLen(srcOff, len, src.length());
            Check.checkOffLen(dstOff, len, dst.length);
            if (len == 0) {
                return;
            }
            for (int i = 0; i < len; i++) {
                dst[dstOff + i] = src.charAt(srcOff + i);
            }
        }
    }

    private StringKit() {
    }
}