Strings.java

package no.motif;

import static no.motif.Base.all;
import static no.motif.Base.always;
import static no.motif.Base.alwaysThrow;
import static no.motif.Base.both;
import static no.motif.Base.equalTo;
import static no.motif.Base.exists;
import static no.motif.Base.not;
import static no.motif.Base.notNull;
import static no.motif.Base.when;
import static no.motif.Base.where;
import static no.motif.Chars.digit;
import static no.motif.Chars.letter;
import static no.motif.Chars.letterOrDigit;
import static no.motif.Chars.whitespace;
import static no.motif.Exceptions.asRuntimeException;
import static no.motif.Ints.add;
import static no.motif.Iterate.on;
import static no.motif.Singular.optional;
import static no.motif.f.Apply.argsReversed;

import java.io.UnsupportedEncodingException;
import java.util.Collections;

import no.motif.f.Apply;
import no.motif.f.Fn;
import no.motif.f.Fn2;
import no.motif.f.Predicate;
import no.motif.f.Predicate.Always;
import no.motif.f.base.FalseIfNull;
import no.motif.iter.SplitOnCharacter;
import no.motif.iter.SplitOnSubstring;
import no.motif.single.Optional;

/**
 * Functions operating on {@link String strings}.
 */
public final class Strings {

    /**
     * Converts a string to a <code>int</code> value using {@link Integer#valueOf(String)}.
     * If the string is <code>null</code>, 0 is yielded.
     */
    public static final Fn<String, Integer> toInt = new Fn<String, Integer>() {
        @Override public Integer $(String numeric) { return numeric != null ? Integer.valueOf(numeric) : 0; }};


    /**
     * Converts a string to a <code>long</code> value using {@link Long#valueOf(String)}.
     * If the string is <code>null</code>, 0 is yielded.
     */
    public static final Fn<String, Long> toLong = new Fn<String, Long>() {
        @Override public Long $(String numeric) { return numeric != null ? Long.valueOf(numeric) : 0; }};


    /**
     * Converts a string to a <code>double</code> value using {@link Double#valueOf(String)}.
     * If the string is <code>null</code>, 0 is yielded.
     */
    public static final Fn<String, Double> toDouble = new Fn<String, Double>() {
        @Override public Double $(String decimalValue) { return decimalValue != null ? Double.valueOf(decimalValue) : 0; }};


    /**
     * Splits a string into characters.
     */
    public static final Fn<String, Iterable<Character>> toChars = new Fn<String, Iterable<Character>>() {
        @Override public Iterable<Character> $(String value) { return Iterate.on(value); }};


    /**
     * Yields the bytes of a String.
     * @see String#getBytes()
     */
    public static final Fn<String, Iterable<Byte>> bytes = when(notNull, new Fn<String, Iterable<Byte>>() {
        @Override public Iterable<Byte> $(String s) {
            try {
                return Iterate.on(s.getBytes(Implicits.getEncoding()));
            } catch (UnsupportedEncodingException e) {
                throw asRuntimeException(e);
            }
        }
    }).orElse(Iterate.<Byte>none());




    /**
     * A blank string is either <code>null</code>, empty, or
     * all characters {@link Chars#whitespace are whitespace}.
     */
    public static final Predicate<String> blank = where(toChars, all(whitespace));


    /**
     * A nonblank string has at least one character, and must contain at least
     * one character which is {@link Chars#whitespace not whitespace}.
     */
    public static final Predicate<String> nonblank = where(toChars, exists(not(whitespace)));


    /**
     * A numeric string must have at least one character, and all of them
     * must be {@link Chars#digit digits}.
     */
    public static final Predicate<String> numeric = nonblankAllChars(digit);


    /**
     * Alphanumeric strings are at least one character, and all
     * {@link Chars#digit digits} and/or {@link Chars#letter letters}.
     */
    public static final Predicate<String> alphanumeric = nonblankAllChars(letterOrDigit);


    /**
     * Alphabetic strings are at least one character, and all {@link Chars#letter letters}.
     */
    public static final Predicate<String> alphabetic = nonblankAllChars(letter);


    /**
     * Predicate verifying that all characters in a string satifies a given predicate.
     *
     * @param valid The predicate evaluating all characters in a string.
     */
    public static final Predicate<String> allChars(Predicate<Character> valid) {
        return where(toChars, all(valid)); }

    /**
     * Predicate verifying that strings are {@link #nonblank not blank} and
     * each char satisfies a given predicate.
     *
     * @param valid
     */
    public static final Predicate<String> nonblankAllChars(Predicate<Character> valid) {
        return both(nonblank).and(allChars(valid)); }


    /**
     * Trims a string, removing all leading and trailing whitespace.
     */
    public static final Fn<String, String> trimmed = when(notNull, new Fn<String, String>() {
        @Override public String $(String s) { return s.trim(); }});


    /**
     * Convert a string to {@link String#toLowerCase() lower case}.
     */
    public static final Fn<String, String> lowerCased = when(notNull, new Fn<String, String>() {
        @Override public String $(String s) { return s.toLowerCase(Implicits.getLocale()); }});


    /**
     * Convert a string to {@link String#toUpperCase() upper case}
     */
    public static final Fn<String, String> upperCased = when(notNull, new Fn<String, String>() {
        @Override public String $(String s) { return s.toUpperCase(Implicits.getLocale()); }});


    /**
     * Gives the length of a string, i.e. the amount of characters. <code>null</code>
     * yields length 0.
     */
    public static final Fn<String, Integer> length = when(notNull, new Fn<String, Integer>() {
        @Override public Integer $(String s) { return s.length(); }}).orElse(0);


    /**
     * Evaluate if strings are of a exact length.
     * <code>null<code>s are considered to have length zero.
     */
    public static final Predicate<String> hasLength(int exactLength) { return hasLength(equalTo(exactLength)); }


    /**
     * Evaluate if strings have accepted lengths.
     * <code>null<code>s are considered to have length zero.
     *
     * @param accepted The predicate evaluating accepted length.
     * @return The predicate evaluating string length.
     */
    public static final Predicate<String> hasLength(Predicate<? super Integer> accepted) { return where(length, accepted); }


    /**
     * Concatenate a string with the {@link Object#toString() string representation}
     * of an arbitrary object, i.e. <em>reduces</em> two strings to one.
     */
    public static final Fn2<Object, Object, String> concat = new Fn2<Object, Object, String>() {
        String emptyIfNull(Object o) { return (o != null? String.valueOf(o) : ""); }
        @Override public String $(Object acc, Object c) { return emptyIfNull(acc) + emptyIfNull(c); }};



    /**
     * Yields the given string with its characters in reversed order.
     */
    public static final Fn<String, String> reversed = when(notNull, new Fn<String, String>() {
        @Override public String $(String s) { return new StringBuilder(s).reverse().toString(); }});




    /**
     * Determines if a substring is present in a string. A string never contains <code>null</code>,
     * nor does <code>null</code> contain any substring.
     *
     * @param charSequence The substring to find.
     * @return The predicate.
     */
    public static Predicate<String> contains(final CharSequence charSequence) {
        return charSequence == null ? Always.<String>no() : new FalseIfNull<String>() {
        @Override protected boolean orElse(String string) { return string.contains(charSequence); }}; }


    /**
     * Determines if a string starts with a given prefix string.
     *
     * @param prefix The prefix.
     * @return The predicate.
     */
    public static Predicate<String> startsWith(final String prefix) {
        return prefix == null ? Always.<String>no() : new FalseIfNull<String>() {
        @Override protected boolean orElse(String string) { return string.startsWith(prefix); }}; }


    /**
     * Determines if a string ends with a given suffix string.
     *
     * @param suffix The suffix.
     * @return The predicate.
     */
    public static Predicate<String> endsWith(final String suffix) {
        return suffix == null ? Always.<String>no() : new FalseIfNull<String>() {
        @Override protected boolean orElse(String string) { return string.endsWith(suffix); }}; }


    /**
     * Does a {@link String#matches(String) regular expression match} on strings.
     *
     * @param regex The regular expression to use for matching.
     * @return the predicate.
     */
    public static Predicate<String> matches(final String regex) {
        return regex == null ? Always.<String>no() : new FalseIfNull<String>() {
        @Override protected boolean orElse(String string) { return string.matches(regex); }};}


    /**
     * Create a new strings by prepending a prefix.
     * @param prefix the prefix to prepend
     */
    public static Fn<Object, String> prepend(String prefix) { return Apply.partially(concat).of(prefix); }


    /**
     * Create a new strings by appending a suffix.
     * @param suffix the suffix to append
     */
    public static Fn<Object, String> append(String suffix) { return Apply.partially(argsReversed(concat)).of(suffix); }


    /**
     * Extract substring from strings. As
     * this function simply delegates to {@link String#substring(int, int)}, it
     * may throw an {@link IndexOutOfBoundsException} if the given indexes
     * are invalid.
     *
     * @param beginIndex The index of the first character to include.
     * @param endIndex The index to end the extraction.
     */
    public static Fn<String, String> substring(final int beginIndex, final int endIndex) {
        if (beginIndex < 0 || endIndex < 0)
            return alwaysThrow(new StringIndexOutOfBoundsException(
                    "Cannot extract substring using negative index. " +
                    "beginIndex: " + beginIndex + ", endIndex: " + endIndex));
        return substring(always(beginIndex), always(endIndex));
    }

    public static Fn<String, String> substring(final Fn<? super String, Integer> beginIndex, final Fn<? super String, Integer> endIndex) {
        return when(notNull, new Fn<String, String>() { @Override public String $(String s) {
            return optional(s).map(before(endIndex)).map(from(beginIndex)).orNull();
        }});
    }


    /**
     * Get at most a given amount of the first characters of strings.
     * If the string is shorter than the amount, the original string is returned.
     */
    public static Fn<String, String> first(final int charAmount) {
        return when(notNull, new Fn<String, String>() { @Override public String $(String s) {
                    return (charAmount > s.length()) ? s : s.substring(0, charAmount);
                }}).orElse("");
    }


    /**
     * Get at most a given amount of the last characters of strings.
     * If the string is shorter than the amount, the original string is returned.
     */
    public static Fn<String, String> last(final int charAmount) {
        return when(notNull, new Fn<String, String>() { @Override public String $(String s) {
                    return (charAmount > s.length()) ? s : s.substring(s.length() - charAmount, s.length());
                }}).orElse("");
    }


    /**
     * Insert strings in between a prefix and a suffix.
     *
     * @param prefix The prefix to appear before the string.
     * @param suffix The suffix to appear after the string
     */
    public static Fn<Object,String> inBetween(final String prefix, final String suffix) {
        return Base.first(prepend(prefix)).then(append(suffix)); }


    /**
     * Repeats a string a given amount of times.
     *
     * @param times The amount of times to repeat the string.
     */
    public static Fn<String, String> repeat(final int times) { return when(notNull, new Fn<String, String>() {
        @Override public String $(String s) { return on((Object) s).repeat(times).join(); }}); }


    /**
     * Repeats a string, insterting given separator, a given amount of times.
     *
     * @param times The amount of times to repeat the string.
     * @param separator The separator string to insert between the repeating strings.
     */
    public static Fn<String, String> repeat(final int times, final String separator) {
        return when(notNull, new Fn<String, String>() {
            @Override public String $(String s) { return on((Object) s).repeat(times).join(separator); }}); }



    /**
     * Inside strings, searches for the <em>first</em> occurence of a substring, and yields the
     * rest of the string <em>after</em> the substring occurence, not including the
     * substring itself.
     * <p>
     * Passing <code>null</code> to the {@link Fn} always yields <code>null</code>
     * </p>
     * <p>
     * If the substring is not found (or it is <code>null</code>), then <code>null</code> is returned.
     * </p><p>
     * If the substring is the empty string, the original string is returned.
     * </p>
     *
     * @param substring the substring to search for.
     */
    public static Fn<String, String> after(final String substring) {
        if (substring == null || substring.isEmpty()) return NOP.fn();
        return from(Base.first(indexOf(substring)).then(add(substring.length())));
    }


    /**
     * Inside strings, searches for the <em>last</em> occurence of a substring, and yields the
     * rest of the string <em>after</em> the substring occurence, not including the
     * substring itself.
     * <p>
     * Passing <code>null</code> to the {@link Fn} always yields <code>null</code>
     * </p>
     * <p>
     * If the substring is not found, or if it is <code>null</code>, then <code>null</code> is returned.
     * If the substring is empty, the empty string is returned.
     * </p>
     *
     * @param substring the substring to search for.
     */
    public static Fn<String, String> afterLast(final String substring) {
        if (substring == null || substring.isEmpty()) return when(notNull, Base.<String, String, String>always(""));
        return from(Base.first(lastIndexOf(substring)).then(add(substring.length())));
    }



    /**
     * Yields substrings <em>from</em> a position index.
     * @see #from(Fn)
     */
    public static Fn<String, String> from(int index) { return from(always(index)); }



    /**
     * Yields substrings <em>from</em> a position index. If the given index
     * {@link Fn} yields <code>null</code>, then <code>null</code> is returned.
     * If a positive index out of bounds with the length of the string
     * is yielded, the empty string is returned.
     *
     * <p>A negative index value is invalid and will throw an {@link StringIndexOutOfBoundsException}.</p>
     * <p>Passing the <code>null</code>-String always yields <code>null</code>.</p>
     *
     * @param index The {@link Fn} to resolve the index.
     */
    public static Fn<String, String> from(final Fn<? super String, Integer> index) {
        return when(notNull, new Fn<String, String>() {
            @Override
            public String $(String s) {
                Integer idx = index.$(s);
                if (idx == null) return null;
                if (idx >= s.length()) return "";
                return s.substring(idx);
            }});
    }





    /**
     * Inside strings, searches for the <em>first</em> occurence of a substring, and yields the
     * the string <em>before</em> the substring occurence, not including the
     * substring itself.
     * <p>
     * Passing <code>null</code> to the {@link Fn} always yields <code>null</code>
     * </p>
     * <p>
     * If the substring is <code>null</code>, the original string is returned.
     * If the substring is not found, <code>null</code> is returned.
     * </p><p>
     * If the substring is the empty string, or found from the beginning of the string, the empty string is returned.
     * </p>
     *
     * @param substring the substring to search for.
     */
    public static Fn<String, String> before(final String substring) {
        return substring == null ? NOP.<String>fn() : before(indexOf(substring));
    }


    /**
     * Inside strings, searches for the <em>last</em> occurence of a substring, and yields the
     * the string <em>before</em> the substring occurence, not including the
     * substring itself.
     * <p>
     * Passing <code>null</code> to the {@link Fn} always yields <code>null</code>
     * </p><p>
     * If the substring is <code>null</code> or empty, the original string is returned.
     * If the substring is not found, <code>null</code> is returned.
     * </p>
     *
     * @param substring the substring to search for.
     */
    public static Fn<String, String> beforeLast(final String substring) {
        return substring != null ? before(lastIndexOf(substring)) : NOP.<String>fn();
    }


    /**
     * Yields substrings <em>before</em> a position index.
     * @see #before(Fn)
     */
    public static Fn<String, String> before(int index) { return before(always(index)); }


    /**
     * Yields substrings <em>before</em> a position index. If the given index
     * {@link Fn} yields <code>null</code>, or a positive index out of bounds
     * with the length of the string, the original string is returned.
     *
     * <p>A negative index is invalid and will throw an {@link StringIndexOutOfBoundsException}.</p>
     * <p>Passing the <code>null</code>-String always yields <code>null</code>.</p>
     *
     * @param index The {@link Fn} to resolve the index.
     */
    public static Fn<String, String> before(final Fn<? super String, Integer> index) {
        return when(notNull, new Fn<String, String>() {
            @Override
            public String $(String s) {
                Integer idx = index.$(s);
                if (idx == null) return null;
                if (idx >= s.length()) return s;
                return s.substring(0, idx);
            }});
    }


    /**
     * Yield the string _between_ two substrings. The substrings will be the first possible
     * matches, which means <code>between("x", "y")</code> applied to the string
     * <code>"xxyy"</code> will yield <code>"x"</code>.
     *
     * @param openSubstring
     * @param closeSubstring
     */
    public static Fn<String,String> between(String openSubstring, String closeSubstring) {
        if (openSubstring == null || closeSubstring == null) return always(null);
        return Base.first(after(openSubstring)).then(before(closeSubstring));
    }


    /**
     * Yield the string _between_ two outermost substrings. This means
     * <code>betweenOuter("x", "y")</code> applied to the string
     * <code>"xxyy"</code> will yield <code>"xy"</code>.
     *
     * @param openSubstring
     * @param closeSubstring
     */
    public static Fn<String,String> betweenOuter(String openSubstring, String closeSubstring) {
        if (openSubstring == null || closeSubstring == null) return always(null);
        return Base.first(after(openSubstring)).then(beforeLast(closeSubstring));
    }


    /**
     * Yield all strings occurring _between_ two substrings.
     *
     * @param openSubstring
     * @param closeSubstring
     */
    public static Fn<String, Iterable<String>> allBetween(final String openSubstring, final String closeSubstring) {
        if (openSubstring == null || closeSubstring == null) return always((Iterable<String>) Collections.<String>emptySet());
        if ("".equals(openSubstring) && "".equals(closeSubstring)) return alwaysThrow(new IllegalArgumentException(
                "Extracting all strings between two empty strings would yield an infinite amount of empty strings!"));
        return when(notNull, new Fn<String, Iterable<String>>() {
            final Fn<String, String> firstSubstring = between(openSubstring, closeSubstring);
            @Override
            public Iterable<String> $(String s) {
                Optional<String> original = optional(s);
                Optional<String> first = original.map(firstSubstring);
                if (!first.isSome()) return Collections.emptySet();
                Optional<String> rest = original.map(nonblank, after(first.map(inBetween(openSubstring, closeSubstring)).orElse(null)));
                return first.append(this.$(rest.orElse("")));
            }});
    }



    /**
     * Yields index position of first occurence of a <code>char</code>, or <code>null</code>
     * if the <code>char</code> cannot be found.
     *
     * @see String#indexOf(int)
     */
    public static Fn<String, Integer> indexOf(final char c) {
        return when(notNull, new Fn<String, Integer>() {
            @Override public Integer $(String s) {
                int index = s.indexOf(c);
                return index >= 0 ? index : null;
            }});
    }


    /**
     * Yields index position of last occurence of a <code>char</code>, or <code>null</code>
     * if the <code>char</code> cannot be found.
     *
     * @see String#indexOf(int)
     */
    public static Fn<String, Integer> lastIndexOf(final char c) { return when(notNull, new Fn<String, Integer>() {
        @Override public Integer $(String s) {
            int index = s.lastIndexOf(c);
            return index >= 0 ? index : null;
        }});
    }


    /**
     * Yields index position of first occurence of a substring, or <code>null</code>
     * if the substring cannot be found.
     *
     * @see String#indexOf(String)
     */
    public static Fn<String, Integer> indexOf(final String substring) {
        if (substring == null) return always(null);
        return when(notNull, new Fn<String, Integer>() {
            @Override public Integer $(String s) {
                int index = s.indexOf(substring);
                return index >= 0 ? index : null;
            }});
    }



    /**
     * Yields index position of last occurence of a substring, or <code>null</code>
     * if the substring cannot be found.
     *
     * @see String#indexOf(String)
     */
    public static Fn<String, Integer> lastIndexOf(final String substring) {
        if (substring == null) return always(null);
        return when(notNull, new Fn<String, Integer>() {
            @Override public Integer $(String s) {
                int index = s.lastIndexOf(substring);
                return index >= 0 ? index : null;
            }});
    }


    /**
     * Split a string on each occurence of a substring.
     * The substring is not included in the resulting strings, and any
     * consecutive substring are treated as one delimiter instance.
     *
     * @param substring
     */
    public static Fn<String, Iterable<String>> splittingOn(final String substring) {
        return new Fn<String, Iterable<String>>() {
            @Override public Iterable<String> $(String string) {
                return new SplitOnSubstring(string, substring);
            }};
    }


    /**
     * Split a string on each occurence of a <code>char</code> delimiter.
     * The splitting character is not included in the resulting strings, and any
     * consecutive occurrences of the character are treated as one delimiter instance.
     *
     * @param character the delimiter character
     */
    public static Fn<String, Iterable<String>> splittingOn(char character) { return splittingOn(equalTo(character)); }


    /**
     * Split a string into several on any character passing the given
     * <code>Character</code> predicate.
     * The characters accepted by the predicate is not included in the
     * resulting strings, and any consecutive accepted characters are
     * treated as one delimiter instance.
     *
     * @param character the predicate which decides if a character is
     *                  a delimiter.
     */
    public static Fn<String, Iterable<String>> splittingOn(final Predicate<? super Character> character) {
        return new Fn<String, Iterable<String>>() {
            @Override public Iterable<String> $(String string) {
                return new SplitOnCharacter(string, character);
            }};
    }


    private Strings() {}
}