All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.underscore.Underscore Maven / Gradle / Ivy

There is a newer version: 1.9
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright 2015-2024 Valentyn Kolesnikov
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.github.underscore;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

/**
 * Underscore-java is a java port of Underscore.js.
 *
 * @author Valentyn Kolesnikov
 */
@SuppressWarnings({
    "java:S106",
    "java:S2189",
    "java:S2272",
    "java:S2789",
    "java:S3740",
    "java:S5852"
})
public class Underscore {
    private static final Map> FUNCTIONS = new LinkedHashMap<>();
    private static final Map TEMPLATE_SETTINGS = new HashMap<>();
    private static final int MIN_PASSWORD_LENGTH_8 = 8;
    private static final long CAPACITY_SIZE_5 = 5L;
    private static final long CAPACITY_COEFF_2 = 2L;
    private static final long CAPACITY_SIZE_16 = 16L;
    private static final java.util.concurrent.atomic.AtomicInteger UNIQUE_ID =
            new java.util.concurrent.atomic.AtomicInteger(0);
    private static final String ALL_SYMBOLS = "([\\s\\S]+?)";
    private static final String EVALUATE = "evaluate";
    private static final String INTERPOLATE = "interpolate";
    private static final String ESCAPE = "escape";
    private static final String S_Q = "\\s*\\Q";
    private static final String E_S = "\\E\\s*";
    private static final java.util.regex.Pattern FORMAT_PATTERN =
            java.util.regex.Pattern.compile("\\{\\s*(\\d*)\\s*\\}");
    private static final Map ESCAPES = new HashMap<>();
    private final Iterable iterable;
    private final Optional string;

    static {
        TEMPLATE_SETTINGS.put(EVALUATE, "<%([\\s\\S]+?)%>");
        TEMPLATE_SETTINGS.put(INTERPOLATE, "<%=([\\s\\S]+?)%>");
        TEMPLATE_SETTINGS.put(ESCAPE, "<%-([\\s\\S]+?)%>");
        ESCAPES.put('&', "&");
        ESCAPES.put('<', "<");
        ESCAPES.put('>', ">");
        ESCAPES.put('"', """);
        ESCAPES.put('\'', "'");
        ESCAPES.put('`', "`");
    }

    public Underscore(final Iterable iterable) {
        this.iterable = iterable;
        this.string = Optional.empty();
    }

    public Underscore(final String string) {
        this.iterable = null;
        this.string = Optional.of(string);
    }

    private static void setTemplateKey(
            final Map templateSettings, final String key) {
        if (templateSettings.containsKey(key) && templateSettings.get(key).contains(ALL_SYMBOLS)) {
            TEMPLATE_SETTINGS.put(key, templateSettings.get(key));
        }
    }

    public static void templateSettings(final Map templateSettings) {
        setTemplateKey(templateSettings, EVALUATE);
        setTemplateKey(templateSettings, INTERPOLATE);
        setTemplateKey(templateSettings, ESCAPE);
    }

    private static final class WherePredicate implements Predicate {
        private final List> properties;

        private WherePredicate(List> properties) {
            this.properties = properties;
        }

        @Override
        public boolean test(final E elem) {
            for (Map.Entry prop : properties) {
                try {
                    if (!elem.getClass()
                            .getField(prop.getKey())
                            .get(elem)
                            .equals(prop.getValue())) {
                        return false;
                    }
                } catch (Exception ex) {
                    try {
                        if (!elem.getClass()
                                .getMethod(prop.getKey())
                                .invoke(elem)
                                .equals(prop.getValue())) {
                            return false;
                        }
                    } catch (Exception ignored) {
                        // ignored
                    }
                }
            }
            return true;
        }
    }

    private static final class TemplateImpl implements Template> {
        private final String template;

        private TemplateImpl(String template) {
            this.template = template;
        }

        @Override
        public String apply(Map value) {
            final String evaluate = TEMPLATE_SETTINGS.get(EVALUATE);
            final String interpolate = TEMPLATE_SETTINGS.get(INTERPOLATE);
            final String escape = TEMPLATE_SETTINGS.get(ESCAPE);
            String result = template;
            for (final Map.Entry element : value.entrySet()) {
                final String value1 =
                        String.valueOf(element.getValue())
                                .replace("\\", "\\\\")
                                .replace("$", "\\$");
                result =
                        java.util.regex.Pattern.compile(
                                        interpolate.replace(
                                                ALL_SYMBOLS, S_Q + element.getKey() + E_S))
                                .matcher(result)
                                .replaceAll(value1);
                result =
                        java.util.regex.Pattern.compile(
                                        escape.replace(ALL_SYMBOLS, S_Q + element.getKey() + E_S))
                                .matcher(result)
                                .replaceAll(escape(value1));
                result =
                        java.util.regex.Pattern.compile(
                                        evaluate.replace(ALL_SYMBOLS, S_Q + element.getKey() + E_S))
                                .matcher(result)
                                .replaceAll(value1);
            }
            return result;
        }

        @Override
        public List check(Map value) {
            final String evaluate = TEMPLATE_SETTINGS.get(EVALUATE);
            final String interpolate = TEMPLATE_SETTINGS.get(INTERPOLATE);
            final String escape = TEMPLATE_SETTINGS.get(ESCAPE);
            String result = template;
            final List notFound = new ArrayList<>();
            final List valueKeys = new ArrayList<>();
            for (final Map.Entry element : value.entrySet()) {
                final String key = "" + element.getKey();
                java.util.regex.Matcher matcher =
                        java.util.regex.Pattern.compile(
                                        interpolate.replace(ALL_SYMBOLS, S_Q + key + E_S))
                                .matcher(result);
                boolean isFound = matcher.find();
                result = matcher.replaceAll(String.valueOf(element.getValue()));
                matcher =
                        java.util.regex.Pattern.compile(
                                        escape.replace(ALL_SYMBOLS, S_Q + key + E_S))
                                .matcher(result);
                isFound |= matcher.find();
                result = matcher.replaceAll(escape(String.valueOf(element.getValue())));
                matcher =
                        java.util.regex.Pattern.compile(
                                        evaluate.replace(ALL_SYMBOLS, S_Q + key + E_S))
                                .matcher(result);
                isFound |= matcher.find();
                result = matcher.replaceAll(String.valueOf(element.getValue()));
                if (!isFound) {
                    notFound.add(key);
                }
                valueKeys.add(key);
            }
            final List templateVars = new ArrayList<>();
            java.util.regex.Matcher matcher =
                    java.util.regex.Pattern.compile(interpolate).matcher(result);
            while (matcher.find()) {
                templateVars.add(matcher.group(1).trim());
            }
            result = matcher.replaceAll("");
            matcher = java.util.regex.Pattern.compile(escape).matcher(result);
            while (matcher.find()) {
                templateVars.add(matcher.group(1).trim());
            }
            result = matcher.replaceAll("");
            matcher = java.util.regex.Pattern.compile(evaluate).matcher(result);
            while (matcher.find()) {
                templateVars.add(matcher.group(1).trim());
            }
            notFound.addAll(difference(templateVars, valueKeys));
            return notFound;
        }
    }

    private static final class MyIterable implements Iterable {
        private final UnaryOperator unaryOperator;
        private boolean firstRun = true;
        private T value;

        MyIterable(final T seed, final UnaryOperator unaryOperator) {
            this.value = seed;
            this.unaryOperator = unaryOperator;
        }

        public Iterator iterator() {
            return new Iterator<>() {
                @Override
                public boolean hasNext() {
                    return true;
                }

                @Override
                public T next() {
                    if (firstRun) {
                        firstRun = false;
                    } else {
                        value = unaryOperator.apply(value);
                    }
                    return value;
                }

                @Override
                public void remove() {
                    // ignored
                }
            };
        }
    }

    public static  Function, V> iteratee(final K key) {
        return item -> item.get(key);
    }

    /*
     * Documented, #each
     */
    public static  void each(final Iterable iterable, final Consumer func) {
        for (T element : iterable) {
            func.accept(element);
        }
    }

    public static  void eachIndexed(
            final Iterable iterable, final BiConsumer func) {
        int index = 0;
        for (T element : iterable) {
            func.accept(index, element);
            index += 1;
        }
    }

    public void each(final Consumer func) {
        each(iterable, func);
    }

    public static  void eachRight(final Iterable iterable, final Consumer func) {
        each(reverse(iterable), func);
    }

    public void eachRight(final Consumer func) {
        eachRight(iterable, func);
    }

    public static  void forEach(final Iterable iterable, final Consumer func) {
        each(iterable, func);
    }

    public static  void forEachIndexed(
            final Iterable iterable, final BiConsumer func) {
        eachIndexed(iterable, func);
    }

    public void forEach(final Consumer func) {
        each(iterable, func);
    }

    public void forEachIndexed(final BiConsumer func) {
        eachIndexed(iterable, func);
    }

    public static  void forEachRight(
            final Iterable iterable, final Consumer func) {
        eachRight(iterable, func);
    }

    public void forEachRight(final Consumer func) {
        eachRight(iterable, func);
    }

    /*
     * Documented, #map
     */
    public static  List map(final List list, final Function func) {
        final List transformed = newArrayListWithExpectedSize(list.size());
        for (E element : list) {
            transformed.add(func.apply(element));
        }
        return transformed;
    }

    public static  List mapMulti(
            final List list, final BiConsumer> mapper) {
        final List transformed = newArrayListWithExpectedSize(list.size());
        for (E element : list) {
            Consumer value = transformed::add;
            mapper.accept(element, value);
        }
        return transformed;
    }

    public  List map(final Function func) {
        return map(newArrayList(iterable), func);
    }

    public static  List map(final int[] array, final Function func) {
        final List transformed = newArrayListWithExpectedSize(array.length);
        for (int element : array) {
            transformed.add(func.apply(element));
        }
        return transformed;
    }

    public static  Set map(final Set set, final Function func) {
        final Set transformed = newLinkedHashSetWithExpectedSize(set.size());
        for (E element : set) {
            transformed.add(func.apply(element));
        }
        return transformed;
    }

    public static  List mapIndexed(
            final List list, final BiFunction func) {
        final List transformed = newArrayListWithExpectedSize(list.size());
        int index = 0;
        for (E element : list) {
            transformed.add(func.apply(index, element));
            index += 1;
        }
        return transformed;
    }

    public static  List replace(
            final Iterable iter, final Predicate pred, final T value) {
        List list = newArrayList(iter);
        if (pred == null) {
            return list;
        }
        ListIterator itera = list.listIterator();
        while (itera.hasNext()) {
            if (pred.test(itera.next())) {
                itera.set(value);
            }
        }
        return list;
    }

    public List replace(final Predicate pred, final T value) {
        return replace(value(), pred, value);
    }

    public static  List replaceIndexed(
            final Iterable iter, final PredicateIndexed pred, final T value) {
        List list = newArrayList(iter);
        if (pred == null) {
            return list;
        }
        ListIterator itera = list.listIterator();
        int index = 0;
        while (itera.hasNext()) {
            if (pred.test(index, itera.next())) {
                itera.set(value);
            }
            index++;
        }
        return list;
    }

    public List replaceIndexed(final PredicateIndexed pred, final T value) {
        return replaceIndexed(value(), pred, value);
    }

    public  List mapIndexed(final BiFunction func) {
        return mapIndexed(newArrayList(iterable), func);
    }

    public static  List collect(final List list, final Function func) {
        return map(list, func);
    }

    public static  Set collect(final Set set, final Function func) {
        return map(set, func);
    }

    /*
     * Documented, #reduce
     */
    public static  E reduce(
            final Iterable iterable, final BiFunction func, final E zeroElem) {
        E accum = zeroElem;
        for (T element : iterable) {
            accum = func.apply(accum, element);
        }
        return accum;
    }

    public static  Optional reduce(final Iterable iterable, final BinaryOperator func) {
        boolean foundAny = false;
        T accum = null;
        for (T element : iterable) {
            if (foundAny) {
                accum = func.apply(accum, element);
            } else {
                foundAny = true;
                accum = element;
            }
        }
        return foundAny ? Optional.of(accum) : Optional.empty();
    }

    public static  E reduce(
            final int[] array, final BiFunction func, final E zeroElem) {
        E accum = zeroElem;
        for (int element : array) {
            accum = func.apply(accum, element);
        }
        return accum;
    }

    public static  E reduce(
            final T[] array, final BiFunction func, final E zeroElem) {
        E accum = zeroElem;
        for (T element : array) {
            accum = func.apply(accum, element);
        }
        return accum;
    }

    public static  E foldl(
            final Iterable iterable, final BiFunction func, final E zeroElem) {
        return reduce(iterable, func, zeroElem);
    }

    public static  E inject(
            final Iterable iterable, final BiFunction func, final E zeroElem) {
        return reduce(iterable, func, zeroElem);
    }

    /*
     * Documented, #reduceRight
     */
    public static  E reduceRight(
            final Iterable iterable, final BiFunction func, final E zeroElem) {
        return reduce(reverse(iterable), func, zeroElem);
    }

    public static  Optional reduceRight(
            final Iterable iterable, final BinaryOperator func) {
        return reduce(reverse(iterable), func);
    }

    public static  E reduceRight(
            final int[] array, final BiFunction func, final E zeroElem) {
        E accum = zeroElem;
        for (Integer element : reverse(array)) {
            accum = func.apply(accum, element);
        }
        return accum;
    }

    public static  E reduceRight(
            final T[] array, final BiFunction func, final E zeroElem) {
        return reduce(reverse(array), func, zeroElem);
    }

    public static  E foldr(
            final Iterable iterable, final BiFunction func, final E zeroElem) {
        return reduceRight(iterable, func, zeroElem);
    }

    /*
     * Documented, #find
     */
    public static  Optional find(final Iterable iterable, final Predicate pred) {
        for (E element : iterable) {
            if (pred.test(element)) {
                return isNull(element) ? null : Optional.of(element);
            }
        }
        return Optional.empty();
    }

    public static  Optional detect(final Iterable iterable, final Predicate pred) {
        return find(iterable, pred);
    }

    public static  Optional findLast(final Iterable iterable, final Predicate pred) {
        return find(reverse(iterable), pred);
    }

    /*
     * Documented, #filter
     */
    public static  List filter(final Iterable iterable, final Predicate pred) {
        final List filtered = new ArrayList<>();
        for (E element : iterable) {
            if (pred.test(element)) {
                filtered.add(element);
            }
        }
        return filtered;
    }

    public static  List filter(final List list, final Predicate pred) {
        final List filtered = new ArrayList<>();
        for (E element : list) {
            if (pred.test(element)) {
                filtered.add(element);
            }
        }
        return filtered;
    }

    public List filter(final Predicate pred) {
        final List filtered = new ArrayList<>();
        for (final T element : value()) {
            if (pred.test(element)) {
                filtered.add(element);
            }
        }
        return filtered;
    }

    public static  List filterIndexed(final List list, final PredicateIndexed pred) {
        final List filtered = new ArrayList<>();
        int index = 0;
        for (E element : list) {
            if (pred.test(index, element)) {
                filtered.add(element);
            }
            index += 1;
        }
        return filtered;
    }

    public static  Set filter(final Set set, final Predicate pred) {
        final Set filtered = new LinkedHashSet<>();
        for (E element : set) {
            if (pred.test(element)) {
                filtered.add(element);
            }
        }
        return filtered;
    }

    public static  List select(final List list, final Predicate pred) {
        return filter(list, pred);
    }

    public static  Set select(final Set set, final Predicate pred) {
        return filter(set, pred);
    }

    /*
     * Documented, #reject
     */
    public static  List reject(final List list, final Predicate pred) {
        return filter(list, input -> !pred.test(input));
    }

    public List reject(final Predicate pred) {
        return filter(input -> !pred.test(input));
    }

    public static  List rejectIndexed(final List list, final PredicateIndexed pred) {
        return filterIndexed(list, (index, input) -> !pred.test(index, input));
    }

    public static  Set reject(final Set set, final Predicate pred) {
        return filter(set, input -> !pred.test(input));
    }

    public static  List filterFalse(final List list, final Predicate pred) {
        return reject(list, pred);
    }

    public List filterFalse(final Predicate pred) {
        return reject(pred);
    }

    public static  Set filterFalse(final Set set, final Predicate pred) {
        return reject(set, pred);
    }

    public static  boolean every(final Iterable iterable, final Predicate pred) {
        for (E item : iterable) {
            if (!pred.test(item)) {
                return false;
            }
        }
        return true;
    }

    public boolean every(final Predicate pred) {
        return every(iterable, pred);
    }

    /*
     * Documented, #all
     */
    public static  boolean all(final Iterable iterable, final Predicate pred) {
        return every(iterable, pred);
    }

    public boolean all(final Predicate pred) {
        return every(iterable, pred);
    }

    public static  boolean some(final Iterable iterable, final Predicate pred) {
        Optional optional = find(iterable, pred);
        return optional == null || optional.isPresent();
    }

    public boolean some(final Predicate pred) {
        return some(iterable, pred);
    }

    /*
     * Documented, #any
     */
    public static  boolean any(final Iterable iterable, final Predicate pred) {
        return some(iterable, pred);
    }

    public boolean any(final Predicate pred) {
        return some(iterable, pred);
    }

    public static  int count(final Iterable iterable, final Predicate pred) {
        int result = 0;
        for (E item : iterable) {
            if (pred.test(item)) {
                result += 1;
            }
        }
        return result;
    }

    public int count(final Predicate pred) {
        return count(iterable, pred);
    }

    public static  boolean contains(final Iterable iterable, final E elem) {
        return some(iterable, e -> Objects.equals(elem, e));
    }

    public boolean contains(final T elem) {
        return contains(iterable, elem);
    }

    public static  boolean containsWith(final Iterable iterable, final E elem) {
        return some(
                iterable,
                e -> elem == null ? e == null : String.valueOf(e).contains(String.valueOf(elem)));
    }

    public boolean containsWith(final T elem) {
        return containsWith(iterable, elem);
    }

    public static  boolean contains(
            final Iterable iterable, final E elem, final int fromIndex) {
        final List list = newArrayList(iterable);
        return contains(list.subList(fromIndex, list.size()), elem);
    }

    public boolean containsAtLeast(final T value, final int count) {
        return Underscore.containsAtLeast(this.iterable, value, count);
    }

    public boolean containsAtMost(final T value, final int count) {
        return Underscore.containsAtMost(this.iterable, value, count);
    }

    public static  boolean containsAtLeast(
            final Iterable iterable, final E value, final int count) {
        int foundItems = 0;
        for (E element : iterable) {
            if (Objects.equals(element, value)) {
                foundItems += 1;
            }
            if (foundItems >= count) {
                break;
            }
        }
        return foundItems >= count;
    }

    public static  boolean containsAtMost(
            final Iterable iterable, final E value, final int count) {
        int foundItems = size(iterable);
        for (E element : iterable) {
            if (!(Objects.equals(element, value))) {
                foundItems -= 1;
            }
            if (foundItems <= count) {
                break;
            }
        }
        return foundItems <= count;
    }

    /*
     * Documented, #include
     */
    public static  boolean include(final Iterable iterable, final E elem) {
        return contains(iterable, elem);
    }

    /*
     * Documented, #invoke
     */
    @SuppressWarnings("unchecked")
    public static  List invoke(
            final Iterable iterable, final String methodName, final List args) {
        final List result = new ArrayList<>();
        final List> argTypes = map(args, Object::getClass);
        try {
            final Method method =
                    iterable.iterator()
                            .next()
                            .getClass()
                            .getMethod(methodName, argTypes.toArray(new Class[0]));
            for (E arg : iterable) {
                doInvoke(args, result, method, arg);
            }
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException(e);
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    private static  void doInvoke(List args, List result, Method method, E arg) {
        try {
            result.add((E) method.invoke(arg, args.toArray(new Object[0])));
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    public List invoke(final String methodName, final List args) {
        return invoke(iterable, methodName, args);
    }

    public static  List invoke(final Iterable iterable, final String methodName) {
        return invoke(iterable, methodName, Collections.emptyList());
    }

    public List invoke(final String methodName) {
        return invoke(iterable, methodName);
    }

    /*
     * Documented, #pluck
     */
    public static  List pluck(final List list, final String propertyName) {
        if (list.isEmpty()) {
            return Collections.emptyList();
        }
        return map(
                list,
                elem -> {
                    try {
                        return elem.getClass().getField(propertyName).get(elem);
                    } catch (Exception e) {
                        try {
                            return elem.getClass().getMethod(propertyName).invoke(elem);
                        } catch (Exception ex) {
                            throw new IllegalArgumentException(ex);
                        }
                    }
                });
    }

    public List pluck(final String propertyName) {
        return pluck(newArrayList(iterable), propertyName);
    }

    public static  Set pluck(final Set set, final String propertyName) {
        if (set.isEmpty()) {
            return Collections.emptySet();
        }
        return map(
                set,
                elem -> {
                    try {
                        return elem.getClass().getField(propertyName).get(elem);
                    } catch (Exception e) {
                        try {
                            return elem.getClass().getMethod(propertyName).invoke(elem);
                        } catch (Exception ex) {
                            throw new IllegalArgumentException(ex);
                        }
                    }
                });
    }

    /*
     * Documented, #where
     */
    public static  List where(
            final List list, final List> properties) {
        return filter(list, new WherePredicate<>(properties));
    }

    public  List where(final List> properties) {
        return where(newArrayList(iterable), properties);
    }

    public static  Set where(
            final Set set, final List> properties) {
        return filter(set, new WherePredicate<>(properties));
    }

    /*
     * Documented, #findWhere
     */
    public static  Optional findWhere(
            final Iterable iterable, final List> properties) {
        return find(iterable, new WherePredicate<>(properties));
    }

    public  Optional findWhere(final List> properties) {
        return findWhere(iterable, properties);
    }

    /*
     * Documented, #max
     */
    public static > E max(final Collection collection) {
        return Collections.max(collection);
    }

    @SuppressWarnings("unchecked")
    public T max() {
        return (T) max((Collection) iterable);
    }

    @SuppressWarnings("unchecked")
    public static  E max(
            final Collection collection, final Function func) {
        return Collections.max(collection, (o1, o2) -> func.apply(o1).compareTo(func.apply(o2)));
    }

    @SuppressWarnings("unchecked")
    public > T max(final Function func) {
        return (T) max((Collection) iterable, func);
    }

    /*
     * Documented, #min
     */
    public static > E min(final Collection collection) {
        return Collections.min(collection);
    }

    @SuppressWarnings("unchecked")
    public T min() {
        return (T) min((Collection) iterable);
    }

    @SuppressWarnings("unchecked")
    public static  E min(
            final Collection collection, final Function func) {
        return Collections.min(collection, (o1, o2) -> func.apply(o1).compareTo(func.apply(o2)));
    }

    @SuppressWarnings("unchecked")
    public > T min(final Function func) {
        return (T) min((Collection) iterable, func);
    }

    /*
     * Documented, #shuffle
     */
    public static  List shuffle(final Iterable iterable) {
        final List shuffled = newArrayList(iterable);
        Collections.shuffle(shuffled);
        return shuffled;
    }

    public List shuffle() {
        return shuffle(iterable);
    }

    /*
     * Documented, #sample
     */
    public static  E sample(final Iterable iterable) {
        return newArrayList(iterable).get(new java.security.SecureRandom().nextInt(size(iterable)));
    }

    public T sample() {
        return sample(iterable);
    }

    public static  Set sample(final List list, final int howMany) {
        final int size = Math.min(howMany, list.size());
        final Set samples = newLinkedHashSetWithExpectedSize(size);
        while (samples.size() < size) {
            E sample = sample(list);
            samples.add(sample);
        }
        return samples;
    }

    public static > List sortWith(
            final Iterable iterable, final Comparator comparator) {
        final List sortedList = newArrayList(iterable);
        sortedList.sort(comparator);
        return sortedList;
    }

    @SuppressWarnings("unchecked")
    public > List sortWith(final Comparator comparator) {
        return sortWith((Iterable) iterable, comparator);
    }

    /*
     * Documented, #sortBy
     */
    public static > List sortBy(
            final Iterable iterable, final Function func) {
        final List sortedList = newArrayList(iterable);
        sortedList.sort(Comparator.comparing(func));
        return sortedList;
    }

    @SuppressWarnings("unchecked")
    public > List sortBy(final Function func) {
        return sortBy((Iterable) iterable, func);
    }

    public static > List> sortBy(
            final Iterable> iterable, final K key) {
        final List> sortedList = newArrayList(iterable);
        sortedList.sort(Comparator.comparing(o -> o.get(key)));
        return sortedList;
    }

    /*
     * Documented, #groupBy
     */
    public static  Map> groupBy(
            final Iterable iterable, final Function func) {
        final Map> retVal = new LinkedHashMap<>();
        for (E e : iterable) {
            final K key = func.apply(e);
            List val;
            if (retVal.containsKey(key)) {
                val = retVal.get(key);
            } else {
                val = new ArrayList<>();
            }
            val.add(e);
            retVal.put(key, val);
        }
        return retVal;
    }

    @SuppressWarnings("unchecked")
    public  Map> groupBy(final Function func) {
        return groupBy((Iterable) iterable, func);
    }

    public static  Map> groupBy(
            final Iterable iterable,
            final Function func,
            final BinaryOperator binaryOperator) {
        final Map> retVal = new LinkedHashMap<>();
        for (Map.Entry> entry : groupBy(iterable, func).entrySet()) {
            retVal.put(entry.getKey(), reduce(entry.getValue(), binaryOperator));
        }
        return retVal;
    }

    @SuppressWarnings("unchecked")
    public  Map> groupBy(
            final Function func, final BinaryOperator binaryOperator) {
        return groupBy((Iterable) iterable, func, binaryOperator);
    }

    public static  Map associateBy(
            final Iterable iterable, final Function func) {
        final Map retVal = new LinkedHashMap<>();
        for (E e : iterable) {
            final K key = func.apply(e);
            retVal.putIfAbsent(key, e);
        }
        return retVal;
    }

    @SuppressWarnings("unchecked")
    public  Map associateBy(final Function func) {
        return associateBy((Iterable) iterable, func);
    }

    @SuppressWarnings("unchecked")
    public static  Map> indexBy(
            final Iterable iterable, final String property) {
        return groupBy(
                iterable,
                elem -> {
                    try {
                        return (K) elem.getClass().getField(property).get(elem);
                    } catch (Exception e) {
                        return null;
                    }
                });
    }

    @SuppressWarnings("unchecked")
    public  Map> indexBy(final String property) {
        return indexBy((Iterable) iterable, property);
    }

    /*
     * Documented, #countBy
     */
    public static  Map countBy(final Iterable iterable, Function func) {
        final Map retVal = new LinkedHashMap<>();
        for (E e : iterable) {
            final K key = func.apply(e);
            if (retVal.containsKey(key)) {
                retVal.put(key, 1 + retVal.get(key));
            } else {
                retVal.put(key, 1);
            }
        }
        return retVal;
    }

    public static  Map countBy(final Iterable iterable) {
        final Map retVal = new LinkedHashMap<>();
        for (K key : iterable) {
            if (retVal.containsKey(key)) {
                retVal.put(key, 1 + retVal.get(key));
            } else {
                retVal.put(key, 1);
            }
        }
        return retVal;
    }

    @SuppressWarnings("unchecked")
    public  Map countBy(Function func) {
        return countBy((Iterable) iterable, func);
    }

    @SuppressWarnings("unchecked")
    public  Map countBy() {
        return countBy((Iterable) iterable);
    }

    /*
     * Documented, #toArray
     */
    @SuppressWarnings("unchecked")
    public static  E[] toArray(final Iterable iterable) {
        return (E[]) newArrayList(iterable).toArray();
    }

    @SuppressWarnings("unchecked")
    public  E[] toArray() {
        return toArray((Iterable) iterable);
    }

    /*
     * Documented, #toMap
     */
    public static  Map toMap(final Iterable> iterable) {
        final Map result = new LinkedHashMap<>();
        for (Map.Entry entry : iterable) {
            result.put(entry.getKey(), entry.getValue());
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    public  Map toMap() {
        return toMap((Iterable>) iterable);
    }

    public static  Map toMap(final List> tuples) {
        final Map result = new LinkedHashMap<>();
        for (final Map.Entry entry : tuples) {
            result.put(entry.getKey(), entry.getValue());
        }
        return result;
    }

    public Map toCardinalityMap() {
        return toCardinalityMap(iterable);
    }

    public static  Map toCardinalityMap(final Iterable iterable) {
        Iterator iterator = iterable.iterator();
        Map result = new LinkedHashMap<>();

        while (iterator.hasNext()) {
            K item = iterator.next();

            if (result.containsKey(item)) {
                result.put(item, result.get(item) + 1);
            } else {
                result.put(item, 1);
            }
        }
        return result;
    }

    /*
     * Documented, #size
     */
    public static int size(final Iterable iterable) {
        if (iterable instanceof Collection) {
            return ((Collection) iterable).size();
        }
        int size;
        final Iterator iterator = iterable.iterator();
        for (size = 0; iterator.hasNext(); size += 1) {
            iterator.next();
        }
        return size;
    }

    public int size() {
        return size(iterable);
    }

    @SuppressWarnings("unchecked")
    public static  int size(final E... array) {
        return array.length;
    }

    public static  List> partition(final Iterable iterable, final Predicate pred) {
        final List retVal1 = new ArrayList<>();
        final List retVal2 = new ArrayList<>();
        for (final E e : iterable) {
            if (pred.test(e)) {
                retVal1.add(e);
            } else {
                retVal2.add(e);
            }
        }
        return Arrays.asList(retVal1, retVal2);
    }

    @SuppressWarnings("unchecked")
    public static  List[] partition(final E[] iterable, final Predicate pred) {
        return partition(Arrays.asList(iterable), pred).toArray(new List[0]);
    }

    public T singleOrNull() {
        return singleOrNull(iterable);
    }

    public T singleOrNull(Predicate pred) {
        return singleOrNull(iterable, pred);
    }

    public static  E singleOrNull(final Iterable iterable) {
        Iterator iterator = iterable.iterator();
        if (!iterator.hasNext()) {
            return null;
        }
        E result = iterator.next();

        if (iterator.hasNext()) {
            result = null;
        }
        return result;
    }

    public static  E singleOrNull(final Iterable iterable, Predicate pred) {
        return singleOrNull(filter(iterable, pred));
    }

    /*
     * Documented, #first
     */
    public static  E first(final Iterable iterable) {
        return iterable.iterator().next();
    }

    @SuppressWarnings("unchecked")
    public static  E first(final E... array) {
        return array[0];
    }

    public static  List first(final List list, final int n) {
        return list.subList(0, Math.min(Math.max(n, 0), list.size()));
    }

    public T first() {
        return first(iterable);
    }

    public List first(final int n) {
        return first(newArrayList(iterable), n);
    }

    public static  E first(final Iterable iterable, final Predicate pred) {
        return filter(newArrayList(iterable), pred).iterator().next();
    }

    public static  List first(
            final Iterable iterable, final Predicate pred, final int n) {
        List list = filter(newArrayList(iterable), pred);
        return list.subList(0, Math.min(Math.max(n, 0), list.size()));
    }

    public T first(final Predicate pred) {
        return first(newArrayList(iterable), pred);
    }

    public List first(final Predicate pred, final int n) {
        return first(newArrayList(iterable), pred, n);
    }

    public static  E firstOrNull(final Iterable iterable) {
        final Iterator iterator = iterable.iterator();
        return iterator.hasNext() ? iterator.next() : null;
    }

    public T firstOrNull() {
        return firstOrNull(iterable);
    }

    public static  E firstOrNull(final Iterable iterable, final Predicate pred) {
        final Iterator iterator = filter(newArrayList(iterable), pred).iterator();
        return iterator.hasNext() ? iterator.next() : null;
    }

    public T firstOrNull(final Predicate pred) {
        return firstOrNull(iterable, pred);
    }

    public static  E head(final Iterable iterable) {
        return first(iterable);
    }

    @SuppressWarnings("unchecked")
    public static  E head(final E... array) {
        return first(array);
    }

    public static  List head(final List list, final int n) {
        return first(list, n);
    }

    public T head() {
        return first();
    }

    public List head(final int n) {
        return first(n);
    }

    /*
     * Documented, #initial
     */
    public static  List initial(final List list) {
        return initial(list, 1);
    }

    public static  List initial(final List list, final int n) {
        return list.subList(0, Math.max(0, list.size() - n));
    }

    @SuppressWarnings("unchecked")
    public static  E[] initial(final E... array) {
        return initial(array, 1);
    }

    public static  E[] initial(final E[] array, final int n) {
        return Arrays.copyOf(array, array.length - n);
    }

    public List initial() {
        return initial((List) iterable, 1);
    }

    public List initial(final int n) {
        return initial((List) iterable, n);
    }

    @SuppressWarnings("unchecked")
    public static  E last(final E... array) {
        return array[array.length - 1];
    }

    /*
     * Documented, #last
     */
    public static  E last(final List list) {
        return list.get(list.size() - 1);
    }

    public static  List last(final List list, final int n) {
        return list.subList(Math.max(0, list.size() - n), list.size());
    }

    public T last() {
        return last((List) iterable);
    }

    public List last(final int n) {
        return last((List) iterable, n);
    }

    public static  E last(final List list, final Predicate pred) {
        final List filteredList = filter(list, pred);
        return filteredList.get(filteredList.size() - 1);
    }

    public T last(final Predicate pred) {
        return last((List) iterable, pred);
    }

    public static  E lastOrNull(final List list) {
        return list.isEmpty() ? null : list.get(list.size() - 1);
    }

    public T lastOrNull() {
        return lastOrNull((List) iterable);
    }

    public static  E lastOrNull(final List list, final Predicate pred) {
        final List filteredList = filter(list, pred);
        return filteredList.isEmpty() ? null : filteredList.get(filteredList.size() - 1);
    }

    public T lastOrNull(final Predicate pred) {
        return lastOrNull((List) iterable, pred);
    }

    /*
     * Documented, #rest
     */
    public static  List rest(final List list) {
        return rest(list, 1);
    }

    public static  List rest(final List list, int n) {
        return list.subList(Math.min(n, list.size()), list.size());
    }

    @SuppressWarnings("unchecked")
    public static  E[] rest(final E... array) {
        return rest(array, 1);
    }

    @SuppressWarnings("unchecked")
    public static  E[] rest(final E[] array, final int n) {
        return (E[]) rest(Arrays.asList(array), n).toArray();
    }

    public List rest() {
        return rest((List) iterable);
    }

    public List rest(int n) {
        return rest((List) iterable, n);
    }

    public static  List tail(final List list) {
        return rest(list);
    }

    public static  List tail(final List list, final int n) {
        return rest(list, n);
    }

    @SuppressWarnings("unchecked")
    public static  E[] tail(final E... array) {
        return rest(array);
    }

    public static  E[] tail(final E[] array, final int n) {
        return rest(array, n);
    }

    public List tail() {
        return rest();
    }

    public List tail(final int n) {
        return rest(n);
    }

    public static  List drop(final List list) {
        return rest(list);
    }

    public static  List drop(final List list, final int n) {
        return rest(list, n);
    }

    @SuppressWarnings("unchecked")
    public static  E[] drop(final E... array) {
        return rest(array);
    }

    public static  E[] drop(final E[] array, final int n) {
        return rest(array, n);
    }

    /*
     * Documented, #compact
     */
    public static  List compact(final List list) {
        return filter(
                list,
                arg ->
                        !String.valueOf(arg).equals("null")
                                && !String.valueOf(arg).equals("0")
                                && !String.valueOf(arg).equals("false")
                                && !String.valueOf(arg).isEmpty());
    }

    @SuppressWarnings("unchecked")
    public static  E[] compact(final E... array) {
        return (E[]) compact(Arrays.asList(array)).toArray();
    }

    public static  List compactList(final List list, final E falsyValue) {
        return filter(list, arg -> !(Objects.equals(arg, falsyValue)));
    }

    @SuppressWarnings("unchecked")
    public static  E[] compact(final E[] array, final E falsyValue) {
        return (E[]) compactList(Arrays.asList(array), falsyValue).toArray();
    }

    public List compact() {
        return compact((List) iterable);
    }

    public List compact(final T falsyValue) {
        return compactList((List) iterable, falsyValue);
    }

    /*
     * Documented, #flatten
     */
    public static  List flatten(final List list) {
        List flattened = new ArrayList<>();
        flatten(list, flattened, -1);
        return flattened;
    }

    public static  List flatten(final List list, final boolean shallow) {
        List flattened = new ArrayList<>();
        flatten(list, flattened, shallow ? 1 : -1);
        return flattened;
    }

    @SuppressWarnings("unchecked")
    private static  void flatten(
            final List fromTreeList, final List toFlatList, final int shallowLevel) {
        for (Object item : fromTreeList) {
            if (item instanceof List && shallowLevel != 0) {
                flatten((List) item, toFlatList, shallowLevel - 1);
            } else {
                toFlatList.add((E) item);
            }
        }
    }

    public List flatten() {
        return flatten((List) iterable);
    }

    public List flatten(final boolean shallow) {
        return flatten((List) iterable, shallow);
    }

    /*
     * Documented, #without
     */
    @SuppressWarnings("unchecked")
    public static  List without(final List list, E... values) {
        final List valuesList = Arrays.asList(values);
        return filter(list, elem -> !contains(valuesList, elem));
    }

    @SuppressWarnings("unchecked")
    public static  E[] without(final E[] array, final E... values) {
        return (E[]) without(Arrays.asList(array), values).toArray();
    }

    /*
     * Documented, #uniq
     */
    public static  List uniq(final List list) {
        return newArrayList(newLinkedHashSet(list));
    }

    @SuppressWarnings("unchecked")
    public static  E[] uniq(final E... array) {
        return (E[]) uniq(Arrays.asList(array)).toArray();
    }

    public static  Collection uniq(final Iterable iterable, final Function func) {
        final Map retVal = new LinkedHashMap<>();
        for (final E e : iterable) {
            final K key = func.apply(e);
            retVal.put(key, e);
        }
        return retVal.values();
    }

    @SuppressWarnings("unchecked")
    public static  E[] uniq(final E[] array, final Function func) {
        return (E[]) uniq(Arrays.asList(array), func).toArray();
    }

    public static  List distinct(final List list) {
        return uniq(list);
    }

    @SuppressWarnings("unchecked")
    public static  E[] distinct(final E... array) {
        return uniq(array);
    }

    public static  Collection distinctBy(
            final Iterable iterable, final Function func) {
        return uniq(iterable, func);
    }

    public static  E[] distinctBy(final E[] array, final Function func) {
        return uniq(array, func);
    }

    /*
     * Documented, #union
     */
    @SuppressWarnings("unchecked")
    public static  List union(final List list, final List... lists) {
        final Set union = new LinkedHashSet<>(list);
        for (List localList : lists) {
            union.addAll(localList);
        }
        return newArrayList(union);
    }

    @SuppressWarnings("unchecked")
    public List unionWith(final List... lists) {
        return union(newArrayList(iterable), lists);
    }

    @SuppressWarnings("unchecked")
    public static  E[] union(final E[]... arrays) {
        final Set union = new LinkedHashSet<>();
        for (E[] array : arrays) {
            union.addAll(Arrays.asList(array));
        }
        return (E[]) newArrayList(union).toArray();
    }

    /*
     * Documented, #intersection
     */
    public static  List intersection(final List list1, final List list2) {
        final List result = new ArrayList<>();
        for (final E item : list1) {
            if (list2.contains(item)) {
                result.add(item);
            }
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    public static  List intersection(final List list, final List... lists) {
        final Deque> stack = new ArrayDeque<>();
        stack.push(list);
        for (List es : lists) {
            stack.push(intersection(stack.peek(), es));
        }
        return stack.peek();
    }

    @SuppressWarnings("unchecked")
    public List intersectionWith(final List... lists) {
        return intersection(newArrayList(iterable), lists);
    }

    @SuppressWarnings("unchecked")
    public static  E[] intersection(final E[]... arrays) {
        final Deque> stack = new ArrayDeque<>();
        stack.push(Arrays.asList(arrays[0]));
        for (int index = 1; index < arrays.length; index += 1) {
            stack.push(intersection(stack.peek(), Arrays.asList(arrays[index])));
        }
        return (E[]) stack.peek().toArray();
    }

    /*
     * Documented, #difference
     */
    public static  List difference(final List list1, final List list2) {
        final List result = new ArrayList<>();
        for (final E item : list1) {
            if (!list2.contains(item)) {
                result.add(item);
            }
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    public static  List difference(final List list, final List... lists) {
        final Deque> stack = new ArrayDeque<>();
        stack.push(list);
        for (List es : lists) {
            stack.push(difference(stack.peek(), es));
        }
        return stack.peek();
    }

    @SuppressWarnings("unchecked")
    public List differenceWith(final List... lists) {
        return difference(newArrayList(iterable), lists);
    }

    @SuppressWarnings("unchecked")
    public static  E[] difference(final E[]... arrays) {
        final Deque> stack = new ArrayDeque<>();
        stack.push(Arrays.asList(arrays[0]));
        for (int index = 1; index < arrays.length; index += 1) {
            stack.push(difference(stack.peek(), Arrays.asList(arrays[index])));
        }
        return (E[]) stack.peek().toArray();
    }

    /*
     * Documented, #zip
     */
    @SuppressWarnings("unchecked")
    public static  List> zip(final List... lists) {
        final List> zipped = new ArrayList<>();
        each(
                Arrays.asList(lists),
                list -> {
                    int index = 0;
                    for (T elem : list) {
                        final List nTuple;
                        nTuple = index >= zipped.size() ? new ArrayList<>() : zipped.get(index);
                        if (index >= zipped.size()) {
                            zipped.add(nTuple);
                        }
                        index += 1;
                        nTuple.add(elem);
                    }
                });
        return zipped;
    }

    @SuppressWarnings("unchecked")
    public static  List> unzip(final List... lists) {
        final List> unzipped = new ArrayList<>();
        for (int index = 0; index < lists[0].size(); index += 1) {
            final List nTuple = new ArrayList<>();
            for (List list : lists) {
                nTuple.add(list.get(index));
            }
            unzipped.add(nTuple);
        }
        return unzipped;
    }

    /*
     * Documented, #object
     */
    public static  List> object(final List keys, final List values) {
        return map(
                keys,
                new Function<>() {
                    private int index;

                    @Override
                    public Map.Entry apply(K key) {
                        return Map.entry(key, values.get(index++));
                    }
                });
    }

    public static  int findIndex(final List list, final Predicate pred) {
        for (int index = 0; index < list.size(); index++) {
            if (pred.test(list.get(index))) {
                return index;
            }
        }
        return -1;
    }

    public static  int findIndex(final E[] array, final Predicate pred) {
        return findIndex(Arrays.asList(array), pred);
    }

    public static  int findLastIndex(final List list, final Predicate pred) {
        for (int index = list.size() - 1; index >= 0; index--) {
            if (pred.test(list.get(index))) {
                return index;
            }
        }
        return -1;
    }

    public static  int findLastIndex(final E[] array, final Predicate pred) {
        return findLastIndex(Arrays.asList(array), pred);
    }

    public static > int binarySearch(
            final Iterable iterable, final E key) {
        if (key == null) {
            return first(iterable) == null ? 0 : -1;
        }
        int begin = 0;
        int end = size(iterable) - 1;
        int numberOfNullValues = 0;
        List list = new ArrayList<>();
        for (E item : iterable) {
            if (item == null) {
                numberOfNullValues++;
                end--;
            } else {
                list.add(item);
            }
        }
        while (begin <= end) {
            int middle = begin + (end - begin) / 2;
            if (key.compareTo(list.get(middle)) < 0) {
                end = middle - 1;
            } else if (key.compareTo(list.get(middle)) > 0) {
                begin = middle + 1;
            } else {
                return middle + numberOfNullValues;
            }
        }
        return -(begin + numberOfNullValues + 1);
    }

    public static > int binarySearch(final E[] array, final E key) {
        return binarySearch(Arrays.asList(array), key);
    }

    /*
     * Documented, #sortedIndex
     */
    public static > int sortedIndex(final List list, final E value) {
        int index = 0;
        for (E elem : list) {
            if (elem.compareTo(value) >= 0) {
                return index;
            }
            index += 1;
        }
        return -1;
    }

    public static > int sortedIndex(final E[] array, final E value) {
        return sortedIndex(Arrays.asList(array), value);
    }

    @SuppressWarnings("unchecked")
    public static > int sortedIndex(
            final List list, final E value, final String propertyName) {
        try {
            final Field property = value.getClass().getField(propertyName);
            final Object valueProperty = property.get(value);
            int index = 0;
            for (E elem : list) {
                if (((Comparable) property.get(elem)).compareTo(valueProperty) >= 0) {
                    return index;
                }
                index += 1;
            }
            return -1;
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static > int sortedIndex(
            final E[] array, final E value, final String propertyName) {
        return sortedIndex(Arrays.asList(array), value, propertyName);
    }

    /*
     * Documented, #indexOf
     */
    public static  int indexOf(final List list, final E value) {
        return list.indexOf(value);
    }

    public static  int indexOf(final E[] array, final E value) {
        return indexOf(Arrays.asList(array), value);
    }

    /*
     * Documented, #lastIndexOf
     */
    public static  int lastIndexOf(final List list, final E value) {
        return list.lastIndexOf(value);
    }

    public static  int lastIndexOf(final E[] array, final E value) {
        return lastIndexOf(Arrays.asList(array), value);
    }

    /*
     * Documented, #range
     */
    public static List range(int stop) {
        return range(0, stop, 1);
    }

    public static List range(int start, int stop) {
        return range(start, stop, start < stop ? 1 : -1);
    }

    public static List range(int start, int stop, int step) {
        List list = new ArrayList<>();
        if (step == 0) {
            return list;
        }
        if (start < stop) {
            for (int value = start; value < stop; value += step) {
                list.add(value);
            }
        } else {
            for (int value = start; value > stop; value += step) {
                list.add(value);
            }
        }
        return list;
    }

    public static List range(char stop) {
        return range('a', stop, 1);
    }

    public static List range(char start, char stop) {
        return range(start, stop, start < stop ? 1 : -1);
    }

    public static List range(char start, char stop, int step) {
        List list = new ArrayList<>();
        if (step == 0) {
            return list;
        }
        if (start < stop) {
            for (char value = start; value < stop; value += (char) step) {
                list.add(value);
            }
        } else {
            for (char value = start; value > stop; value += (char) step) {
                list.add(value);
            }
        }
        return list;
    }

    public static  List> chunk(final Iterable iterable, final int size) {
        if (size <= 0) {
            return new ArrayList<>();
        }
        return chunk(iterable, size, size);
    }

    public static  List> chunk(
            final Iterable iterable, final int size, final int step) {
        if (step <= 0 || size < 0) {
            return new ArrayList<>();
        }
        int index = 0;
        int length = size(iterable);
        final List> result = new ArrayList<>(size == 0 ? size : (length / size) + 1);
        while (index < length) {
            result.add(newArrayList(iterable).subList(index, Math.min(length, index + size)));
            index += step;
        }
        return result;
    }

    public static  List> chunkFill(
            final Iterable iterable, final int size, final T fillValue) {
        if (size <= 0) {
            return new ArrayList<>();
        }
        return chunkFill(iterable, size, size, fillValue);
    }

    public static  List> chunkFill(
            final Iterable iterable, final int size, final int step, final T fillValue) {
        if (step <= 0 || size < 0) {
            return new ArrayList<>();
        }
        final List> result = chunk(iterable, size, step);
        int difference = size - result.get(result.size() - 1).size();
        for (int i = difference; 0 < i; i--) {
            result.get(result.size() - 1).add(fillValue);
        }
        return result;
    }

    public List> chunk(final int size) {
        return chunk(getIterable(), size, size);
    }

    public List> chunk(final int size, final int step) {
        return chunk(getIterable(), size, step);
    }

    public List> chunkFill(final int size, final T fillvalue) {
        return chunkFill(getIterable(), size, size, fillvalue);
    }

    public List> chunkFill(final int size, final int step, T fillvalue) {
        return chunkFill(getIterable(), size, step, fillvalue);
    }

    public static  List cycle(final Iterable iterable, final int times) {
        int size = Math.abs(size(iterable) * times);
        if (size == 0) {
            return new ArrayList<>();
        }
        List list = newArrayListWithExpectedSize(size);
        int round = 0;
        if (times > 0) {
            while (round < times) {
                for (T element : iterable) {
                    list.add(element);
                }
                round++;
            }
        } else {
            list = cycle(Underscore.reverse(iterable), -times);
        }
        return list;
    }

    public List cycle(final int times) {
        return cycle(value(), times);
    }

    public static  List repeat(final T element, final int times) {
        if (times <= 0) {
            return new ArrayList<>();
        }
        List result = newArrayListWithExpectedSize(times);
        for (int i = 0; i < times; i++) {
            result.add(element);
        }
        return result;
    }

    public static  List interpose(final Iterable iterable, final T interElement) {
        if (interElement == null) {
            return newArrayList(iterable);
        }
        int size = size(iterable);
        int index = 0;
        List array = newArrayListWithExpectedSize(size * 2);
        for (T elem : iterable) {
            array.add(elem);
            if (index + 1 < size) {
                array.add(interElement);
                index++;
            }
        }
        return array;
    }

    public static  List interposeByList(
            final Iterable iterable, final Iterable interIter) {
        if (interIter == null) {
            return newArrayList(iterable);
        }
        List interList = newArrayList(interIter);
        if (isEmpty(interIter)) {
            return newArrayList(iterable);
        }
        int size = size(iterable);
        List array = newArrayListWithExpectedSize(size + interList.size());
        int index = 0;
        for (T element : iterable) {
            array.add(element);
            if (index < interList.size() && index + 1 < size) {
                array.add(interList.get(index));
                index++;
            }
        }
        return array;
    }

    public List interpose(final T element) {
        return interpose(value(), element);
    }

    public List interposeByList(final Iterable interIter) {
        return interposeByList(value(), interIter);
    }

    /*
     * Documented, #bind
     */
    public static  Function bind(final Function function) {
        return function;
    }

    /*
     * Documented, #memoize
     */
    public static  Function memoize(final Function function) {
        return new MemoizeFunction<>() {
            @Override
            public T calc(F arg) {
                return function.apply(arg);
            }
        };
    }

    /*
     * Documented, #delay
     */
    public static  java.util.concurrent.ScheduledFuture delay(
            final Supplier function, final int delayMilliseconds) {
        final java.util.concurrent.ScheduledExecutorService scheduler =
                java.util.concurrent.Executors.newSingleThreadScheduledExecutor();
        final java.util.concurrent.ScheduledFuture future =
                scheduler.schedule(
                        function::get,
                        delayMilliseconds,
                        java.util.concurrent.TimeUnit.MILLISECONDS);
        scheduler.shutdown();
        return future;
    }

    public static  java.util.concurrent.ScheduledFuture defer(final Supplier function) {
        return delay(function, 0);
    }

    public static java.util.concurrent.ScheduledFuture defer(final Runnable runnable) {
        return delay(
                () -> {
                    runnable.run();
                    return null;
                },
                0);
    }

    public static  Supplier throttle(final Supplier function, final int waitMilliseconds) {
        class ThrottleLater implements Supplier {
            private final Supplier localFunction;
            private java.util.concurrent.ScheduledFuture timeout;
            private long previous;

            ThrottleLater(final Supplier function) {
                this.localFunction = function;
            }

            @Override
            public T get() {
                previous = now();
                timeout = null;
                return localFunction.get();
            }

            java.util.concurrent.ScheduledFuture getTimeout() {
                return timeout;
            }

            void setTimeout(java.util.concurrent.ScheduledFuture timeout) {
                this.timeout = timeout;
            }

            long getPrevious() {
                return previous;
            }

            void setPrevious(long previous) {
                this.previous = previous;
            }
        }

        class ThrottleFunction implements Supplier {
            private final Supplier localFunction;
            private final ThrottleLater throttleLater;

            ThrottleFunction(final Supplier function) {
                this.localFunction = function;
                this.throttleLater = new ThrottleLater(function);
            }

            @Override
            public T get() {
                final long now = now();
                if (throttleLater.getPrevious() == 0L) {
                    throttleLater.setPrevious(now);
                }
                final long remaining = waitMilliseconds - (now - throttleLater.getPrevious());
                T result = null;
                if (remaining <= 0) {
                    throttleLater.setPrevious(now);
                    result = localFunction.get();
                } else if (throttleLater.getTimeout() == null) {
                    throttleLater.setTimeout(delay(throttleLater, waitMilliseconds));
                }
                return result;
            }
        }
        return new ThrottleFunction(function);
    }

    /*
     * Documented, #debounce
     */
    public static  Supplier debounce(
            final Supplier function, final int delayMilliseconds) {
        return new Supplier<>() {
            private java.util.concurrent.ScheduledFuture timeout;

            @Override
            public T get() {
                clearTimeout(timeout);
                timeout = delay(function, delayMilliseconds);
                return null;
            }
        };
    }

    /*
     * Documented, #wrap
     */
    public static  Function wrap(
            final UnaryOperator function, final Function, T> wrapper) {
        return arg -> wrapper.apply(function);
    }

    public static  Predicate negate(final Predicate pred) {
        return item -> !pred.test(item);
    }

    /*
     * Documented, #compose
     */
    @SuppressWarnings("unchecked")
    public static  Function compose(final Function... func) {
        return arg -> {
            T result = arg;
            for (int index = func.length - 1; index >= 0; index -= 1) {
                result = func[index].apply(result);
            }
            return result;
        };
    }

    /*
     * Documented, #after
     */
    public static  Supplier after(final int count, final Supplier function) {
        class AfterFunction implements Supplier {
            private final int count;
            private final Supplier localFunction;
            private int index;
            private E result;

            AfterFunction(final int count, final Supplier function) {
                this.count = count;
                this.localFunction = function;
            }

            public E get() {
                if (++index >= count) {
                    result = localFunction.get();
                }
                return result;
            }
        }
        return new AfterFunction(count, function);
    }

    /*
     * Documented, #before
     */
    public static  Supplier before(final int count, final Supplier function) {
        class BeforeFunction implements Supplier {
            private final int count;
            private final Supplier localFunction;
            private int index;
            private E result;

            BeforeFunction(final int count, final Supplier function) {
                this.count = count;
                this.localFunction = function;
            }

            public E get() {
                if (++index <= count) {
                    result = localFunction.get();
                }
                return result;
            }
        }
        return new BeforeFunction(count, function);
    }

    /*
     * Documented, #once
     */
    public static  Supplier once(final Supplier function) {
        return new Supplier<>() {
            private volatile boolean executed;
            private T result;

            @Override
            public T get() {
                if (!executed) {
                    executed = true;
                    result = function.get();
                }
                return result;
            }
        };
    }

    /*
     * Documented, #keys
     */
    public static  Set keys(final Map object) {
        return object.keySet();
    }

    /*
     * Documented, #values
     */
    public static  Collection values(final Map object) {
        return object.values();
    }

    public static  List> mapObject(
            final Map object, final Function func) {
        return map(
                newArrayList(object.entrySet()),
                entry -> Map.entry(entry.getKey(), func.apply(entry.getValue())));
    }

    /*
     * Documented, #pairs
     */
    public static  List> pairs(final Map object) {
        return map(
                newArrayList(object.entrySet()),
                entry -> Map.entry(entry.getKey(), entry.getValue()));
    }

    /*
     * Documented, #invert
     */
    public static  List> invert(final Map object) {
        return map(
                newArrayList(object.entrySet()),
                entry -> Map.entry(entry.getValue(), entry.getKey()));
    }

    /*
     * Documented, #functions
     */
    public static List functions(final Object object) {
        final List result = new ArrayList<>();
        for (final Method method : object.getClass().getDeclaredMethods()) {
            result.add(method.getName());
        }
        return sort(uniq(result));
    }

    public static List methods(final Object object) {
        return functions(object);
    }

    /*
     * Documented, #extend
     */
    @SuppressWarnings("unchecked")
    public static  Map extend(final Map destination, final Map... sources) {
        final Map result = new LinkedHashMap<>(destination);
        for (final Map source : sources) {
            result.putAll(source);
        }
        return result;
    }

    public static  E findKey(final List list, final Predicate pred) {
        for (E e : list) {
            if (pred.test(e)) {
                return e;
            }
        }
        return null;
    }

    public static  E findKey(final E[] array, final Predicate pred) {
        return findKey(Arrays.asList(array), pred);
    }

    public static  E findLastKey(final List list, final Predicate pred) {
        for (int index = list.size() - 1; index >= 0; index--) {
            if (pred.test(list.get(index))) {
                return list.get(index);
            }
        }
        return null;
    }

    public static  E findLastKey(final E[] array, final Predicate pred) {
        return findLastKey(Arrays.asList(array), pred);
    }

    /*
     * Documented, #pick
     */
    @SuppressWarnings("unchecked")
    public static  List> pick(final Map object, final K... keys) {
        return without(
                map(
                        newArrayList(object.entrySet()),
                        entry -> {
                            if (Arrays.asList(keys).contains(entry.getKey())) {
                                return Map.entry(entry.getKey(), entry.getValue());
                            } else {
                                return null;
                            }
                        }),
                (Map.Entry) null);
    }

    @SuppressWarnings("unchecked")
    public static  List> pick(
            final Map object, final Predicate pred) {
        return without(
                map(
                        newArrayList(object.entrySet()),
                        entry -> {
                            if (pred.test(object.get(entry.getKey()))) {
                                return Map.entry(entry.getKey(), entry.getValue());
                            } else {
                                return null;
                            }
                        }),
                (Map.Entry) null);
    }

    /*
     * Documented, #omit
     */
    @SuppressWarnings("unchecked")
    public static  List> omit(final Map object, final K... keys) {
        return without(
                map(
                        newArrayList(object.entrySet()),
                        entry -> {
                            if (Arrays.asList(keys).contains(entry.getKey())) {
                                return null;
                            } else {
                                return Map.entry(entry.getKey(), entry.getValue());
                            }
                        }),
                (Map.Entry) null);
    }

    @SuppressWarnings("unchecked")
    public static  List> omit(
            final Map object, final Predicate pred) {
        return without(
                map(
                        newArrayList(object.entrySet()),
                        entry -> {
                            if (pred.test(entry.getValue())) {
                                return null;
                            } else {
                                return Map.entry(entry.getKey(), entry.getValue());
                            }
                        }),
                (Map.Entry) null);
    }

    /*
     * Documented, #defaults
     */
    public static  Map defaults(final Map object, final Map defaults) {
        final Map result = new LinkedHashMap<>();
        result.putAll(defaults);
        result.putAll(object);
        return result;
    }

    /*
     * Documented, #clone
     */
    public static Object clone(final Object obj) {
        try {
            if (obj instanceof Cloneable) {
                for (final Method method : obj.getClass().getMethods()) {
                    if (method.getName().equals("clone")
                            && method.getParameterTypes().length == 0) {
                        return method.invoke(obj);
                    }
                }
            }
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
        throw new IllegalArgumentException("Cannot clone object");
    }

    @SuppressWarnings("unchecked")
    public static  E[] clone(final E... iterable) {
        return Arrays.copyOf(iterable, iterable.length);
    }

    public static  void tap(final Iterable iterable, final Consumer func) {
        each(iterable, func);
    }

    public static  boolean isMatch(final Map object, final Map properties) {
        for (final K key : keys(properties)) {
            if (!object.containsKey(key) || !object.get(key).equals(properties.get(key))) {
                return false;
            }
        }
        return true;
    }

    /*
     * Documented, #isEqual
     */
    public static boolean isEqual(final Object object, final Object other) {
        return Objects.equals(object, other);
    }

    public static  boolean isEmpty(final Map object) {
        return object == null || object.isEmpty();
    }

    /*
     * Documented, #isEmpty
     */
    public static  boolean isEmpty(final Iterable iterable) {
        return iterable == null || !iterable.iterator().hasNext();
    }

    public boolean isEmpty() {
        return iterable == null || !iterable.iterator().hasNext();
    }

    public static  boolean isNotEmpty(final Map object) {
        return object != null && !object.isEmpty();
    }

    public static  boolean isNotEmpty(final Iterable iterable) {
        return iterable != null && iterable.iterator().hasNext();
    }

    public boolean isNotEmpty() {
        return iterable != null && iterable.iterator().hasNext();
    }

    /*
     * Documented, #isArray
     */
    public static boolean isArray(final Object object) {
        return object != null && object.getClass().isArray();
    }

    /*
     * Documented, #isObject
     */
    public static boolean isObject(final Object object) {
        return object instanceof Map;
    }

    /*
     * Documented, #isFunction
     */
    public static boolean isFunction(final Object object) {
        return object instanceof Function;
    }

    /*
     * Documented, #isString
     */
    public static boolean isString(final Object object) {
        return object instanceof String;
    }

    /*
     * Documented, #isNumber
     */
    public static boolean isNumber(final Object object) {
        return object instanceof Number;
    }

    /*
     * Documented, #isDate
     */
    public static boolean isDate(final Object object) {
        return object instanceof Date;
    }

    public static boolean isRegExp(final Object object) {
        return object instanceof java.util.regex.Pattern;
    }

    public static boolean isError(final Object object) {
        return object instanceof Throwable;
    }

    /*
     * Documented, #isBoolean
     */
    public static boolean isBoolean(final Object object) {
        return object instanceof Boolean;
    }

    public static boolean isNull(final Object object) {
        return object == null;
    }

    /*
     * Documented, #has
     */
    public static  boolean has(final Map object, final K key) {
        return object.containsKey(key);
    }

    public static  E identity(final E value) {
        return value;
    }

    public static  Supplier constant(final E value) {
        return () -> value;
    }

    public static  Function, V> property(final K key) {
        return object -> object.get(key);
    }

    public static  Function propertyOf(final Map object) {
        return object::get;
    }

    public static  Predicate> matcher(final Map object) {
        return item -> {
            for (final K key : keys(object)) {
                if (!item.containsKey(key) || !item.get(key).equals(object.get(key))) {
                    return false;
                }
            }
            return true;
        };
    }

    /*
     * Documented, #times
     */
    public static void times(final int count, final Runnable runnable) {
        for (int index = 0; index < count; index += 1) {
            runnable.run();
        }
    }

    /*
     * Documented, #random
     */
    public static int random(final int min, final int max) {
        return min + new java.security.SecureRandom().nextInt(max - min + 1);
    }

    public static int random(final int max) {
        return new java.security.SecureRandom().nextInt(max + 1);
    }

    public static long now() {
        return new Date().getTime();
    }

    /*
     * Documented, #escape
     */
    public static String escape(final String value) {
        final StringBuilder builder = new StringBuilder();
        for (final char ch : value.toCharArray()) {
            builder.append(ESCAPES.containsKey(ch) ? ESCAPES.get(ch) : ch);
        }
        return builder.toString();
    }

    public static String unescape(final String value) {
        return value.replace("`", "`")
                .replace("'", "'")
                .replace("<", "<")
                .replace(">", ">")
                .replace(""", "\"")
                .replace("&", "&");
    }

    /*
     * Documented, #result
     */
    public static  Object result(final Iterable iterable, final Predicate pred) {
        for (E element : iterable) {
            if (pred.test(element)) {
                if (element instanceof Map.Entry) {
                    if (((Map.Entry) element).getValue() instanceof Supplier) {
                        return ((Supplier) ((Map.Entry) element).getValue()).get();
                    }
                    return ((Map.Entry) element).getValue();
                }
                return element;
            }
        }
        return null;
    }

    /*
     * Documented, #uniqueId
     */
    public static String uniqueId(final String prefix) {
        return (prefix == null ? "" : prefix) + UNIQUE_ID.incrementAndGet();
    }

    /*
     * Documented, #uniquePassword
     */
    public static String uniquePassword() {
        final String[] passwords =
                new String[] {
                    "ALKJVBPIQYTUIWEBVPQALZVKQRWORTUYOYISHFLKAJMZNXBVMNFGAHKJSDFALAPOQIERIUYTGSFGKMZNXBVJAHGFAKX",
                    "1234567890",
                    "qpowiealksdjzmxnvbfghsdjtreiuowiruksfhksajmzxncbvlaksjdhgqwetytopskjhfgvbcnmzxalksjdfhgbvzm",
                    ".@,-+/()#$%^&*!"
                };
        final StringBuilder result = new StringBuilder();
        final long passwordLength =
                Math.abs(UUID.randomUUID().getLeastSignificantBits() % MIN_PASSWORD_LENGTH_8)
                        + MIN_PASSWORD_LENGTH_8;
        for (int index = 0; index < passwordLength; index += 1) {
            final int passIndex = (int) (passwords.length * (long) index / passwordLength);
            final int charIndex =
                    (int)
                            Math.abs(
                                    UUID.randomUUID().getLeastSignificantBits()
                                            % passwords[passIndex].length());
            result.append(passwords[passIndex].charAt(charIndex));
        }
        return result.toString();
    }

    public static  Template> template(final String template) {
        return new TemplateImpl<>(template);
    }

    public static String format(final String template, final Object... params) {
        final java.util.regex.Matcher matcher = FORMAT_PATTERN.matcher(template);
        final StringBuilder buffer = new StringBuilder();
        int index = 0;
        while (matcher.find()) {
            if (matcher.group(1).isEmpty()) {
                matcher.appendReplacement(buffer, "<%" + index++ + "%>");
            } else {
                matcher.appendReplacement(buffer, "<%" + matcher.group(1) + "%>");
            }
        }
        matcher.appendTail(buffer);
        final String newTemplate = buffer.toString();
        final Map args = new LinkedHashMap<>();
        index = 0;
        for (Object param : params) {
            args.put(index, param.toString());
            index += 1;
        }
        return new TemplateImpl(newTemplate).apply(args);
    }

    public static  Iterable iterate(final T seed, final UnaryOperator unaryOperator) {
        return new MyIterable<>(seed, unaryOperator);
    }

    /*
     * Documented, #chain
     */
    public static  Chain chain(final List list) {
        return new Underscore.Chain<>(list);
    }

    public static Chain> chain(final Map map) {
        return new Underscore.Chain<>(map);
    }

    public static  Chain chain(final Iterable iterable) {
        return new Underscore.Chain<>(newArrayList(iterable));
    }

    public static  Chain chain(final Iterable iterable, int size) {
        return new Underscore.Chain<>(newArrayList(iterable, size));
    }

    @SuppressWarnings("unchecked")
    public static  Chain chain(final T... array) {
        return new Underscore.Chain<>(Arrays.asList(array));
    }

    public static Chain chain(final int[] array) {
        return new Underscore.Chain<>(newIntegerList(array));
    }

    public Chain chain() {
        return new Underscore.Chain<>(newArrayList(iterable));
    }

    public static  Chain of(final List list) {
        return new Underscore.Chain<>(list);
    }

    public static  Chain of(final Iterable iterable) {
        return new Underscore.Chain<>(newArrayList(iterable));
    }

    public static  Chain of(final Iterable iterable, int size) {
        return new Underscore.Chain<>(newArrayList(iterable, size));
    }

    @SuppressWarnings("unchecked")
    public static  Chain of(final T... array) {
        return new Underscore.Chain<>(Arrays.asList(array));
    }

    public static Chain of(final int[] array) {
        return new Underscore.Chain<>(newIntegerList(array));
    }

    public Chain of() {
        return new Underscore.Chain<>(newArrayList(iterable));
    }

    public static class Chain {
        private final T item;
        private final List list;
        private final Map map;

        public Chain(final T item) {
            this.item = item;
            this.list = null;
            this.map = null;
        }

        public Chain(final List list) {
            this.item = null;
            this.list = list;
            this.map = null;
        }

        public Chain(final Map map) {
            this.item = null;
            this.list = null;
            this.map = map;
        }

        public Chain first() {
            return new Chain<>(Underscore.first(list));
        }

        public Chain first(int n) {
            return new Chain<>(Underscore.first(list, n));
        }

        public Chain first(final Predicate pred) {
            return new Chain<>(Underscore.first(list, pred));
        }

        public Chain first(final Predicate pred, int n) {
            return new Chain<>(Underscore.first(list, pred, n));
        }

        public Chain firstOrNull() {
            return new Chain<>(Underscore.firstOrNull(list));
        }

        public Chain firstOrNull(final Predicate pred) {
            return new Chain<>(Underscore.firstOrNull(list, pred));
        }

        public Chain initial() {
            return new Chain<>(Underscore.initial(list));
        }

        public Chain initial(int n) {
            return new Chain<>(Underscore.initial(list, n));
        }

        public Chain last() {
            return new Chain<>(Underscore.last(list));
        }

        public Chain last(int n) {
            return new Chain<>(Underscore.last(list, n));
        }

        public Chain lastOrNull() {
            return new Chain<>(Underscore.lastOrNull(list));
        }

        public Chain lastOrNull(final Predicate pred) {
            return new Chain<>(Underscore.lastOrNull(list, pred));
        }

        public Chain rest() {
            return new Chain<>(Underscore.rest(list));
        }

        public Chain rest(int n) {
            return new Chain<>(Underscore.rest(list, n));
        }

        public Chain compact() {
            return new Chain<>(Underscore.compact(list));
        }

        public Chain compact(final T falsyValue) {
            return new Chain<>(Underscore.compactList(list, falsyValue));
        }

        @SuppressWarnings("unchecked")
        public Chain flatten() {
            return new Chain<>(Underscore.flatten(list));
        }

        public  Chain map(final Function func) {
            return new Chain<>(Underscore.map(list, func));
        }

        public  Chain mapMulti(final BiConsumer> mapper) {
            return new Chain<>(Underscore.mapMulti(list, mapper));
        }

        public  Chain mapIndexed(final BiFunction func) {
            return new Chain<>(Underscore.mapIndexed(list, func));
        }

        public Chain replace(final Predicate pred, final T value) {
            return new Chain<>(Underscore.replace(list, pred, value));
        }

        public Chain replaceIndexed(final PredicateIndexed pred, final T value) {
            return new Chain<>(Underscore.replaceIndexed(list, pred, value));
        }

        public Chain filter(final Predicate pred) {
            return new Chain<>(Underscore.filter(list, pred));
        }

        public Chain filterIndexed(final PredicateIndexed pred) {
            return new Chain<>(Underscore.filterIndexed(list, pred));
        }

        public Chain reject(final Predicate pred) {
            return new Chain<>(Underscore.reject(list, pred));
        }

        public Chain rejectIndexed(final PredicateIndexed pred) {
            return new Chain<>(Underscore.rejectIndexed(list, pred));
        }

        public Chain filterFalse(final Predicate pred) {
            return new Chain<>(Underscore.reject(list, pred));
        }

        public  Chain reduce(final BiFunction func, final F zeroElem) {
            return new Chain<>(Underscore.reduce(list, func, zeroElem));
        }

        public Chain> reduce(final BinaryOperator func) {
            return new Chain<>(Underscore.reduce(list, func));
        }

        public  Chain reduceRight(final BiFunction func, final F zeroElem) {
            return new Chain<>(Underscore.reduceRight(list, func, zeroElem));
        }

        public Chain> reduceRight(final BinaryOperator func) {
            return new Chain<>(Underscore.reduceRight(list, func));
        }

        public Chain> find(final Predicate pred) {
            return new Chain<>(Underscore.find(list, pred));
        }

        public Chain> findLast(final Predicate pred) {
            return new Chain<>(Underscore.findLast(list, pred));
        }

        @SuppressWarnings("unchecked")
        public Chain max() {
            return new Chain<>(Underscore.max((Collection) list));
        }

        public > Chain max(final Function func) {
            return new Chain<>(Underscore.max(list, func));
        }

        @SuppressWarnings("unchecked")
        public Chain min() {
            return new Chain<>(Underscore.min((Collection) list));
        }

        public > Chain min(final Function func) {
            return new Chain<>(Underscore.min(list, func));
        }

        @SuppressWarnings("unchecked")
        public Chain sort() {
            return new Chain<>(Underscore.sort((List) list));
        }

        @SuppressWarnings("unchecked")
        public > Chain sortWith(final Comparator comparator) {
            return new Chain<>(Underscore.sortWith((List) list, comparator));
        }

        public > Chain sortBy(final Function func) {
            return new Chain<>(Underscore.sortBy(list, func));
        }

        @SuppressWarnings("unchecked")
        public  Chain> sortBy(final K key) {
            return new Chain<>(Underscore.sortBy((List>) list, key));
        }

        public  Chain>> groupBy(final Function func) {
            return new Chain<>(Underscore.groupBy(list, func));
        }

        public  Chain> associateBy(final Function func) {
            return new Chain<>(Underscore.associateBy(list, func));
        }

        public  Chain>> groupBy(
                final Function func, final BinaryOperator binaryOperator) {
            return new Chain<>(Underscore.groupBy(list, func, binaryOperator));
        }

        public Chain>> indexBy(final String property) {
            return new Chain<>(Underscore.indexBy(list, property));
        }

        public  Chain> countBy(final Function func) {
            return new Chain<>(Underscore.countBy(list, func));
        }

        public Chain> countBy() {
            return new Chain<>(Underscore.countBy(list));
        }

        public Chain shuffle() {
            return new Chain<>(Underscore.shuffle(list));
        }

        public Chain sample() {
            return new Chain<>(Underscore.sample(list));
        }

        public Chain sample(final int howMany) {
            return new Chain<>(Underscore.newArrayList(Underscore.sample(list, howMany)));
        }

        public Chain tap(final Consumer func) {
            Underscore.each(list, func);
            return new Chain<>(list);
        }

        public Chain forEach(final Consumer func) {
            return tap(func);
        }

        public Chain forEachRight(final Consumer func) {
            Underscore.eachRight(list, func);
            return new Chain<>(list);
        }

        public Chain every(final Predicate pred) {
            return new Chain<>(Underscore.every(list, pred));
        }

        public Chain some(final Predicate pred) {
            return new Chain<>(Underscore.some(list, pred));
        }

        public Chain count(final Predicate pred) {
            return new Chain<>(Underscore.count(list, pred));
        }

        public Chain contains(final T elem) {
            return new Chain<>(Underscore.contains(list, elem));
        }

        public Chain containsWith(final T elem) {
            return new Chain<>(Underscore.containsWith(list, elem));
        }

        public Chain invoke(final String methodName, final List args) {
            return new Chain<>(Underscore.invoke(list, methodName, args));
        }

        public Chain invoke(final String methodName) {
            return new Chain<>(Underscore.invoke(list, methodName));
        }

        public Chain pluck(final String propertyName) {
            return new Chain<>(Underscore.pluck(list, propertyName));
        }

        public  Chain where(final List> properties) {
            return new Chain<>(Underscore.where(list, properties));
        }

        public  Chain> findWhere(final List> properties) {
            return new Chain<>(Underscore.findWhere(list, properties));
        }

        public Chain uniq() {
            return new Chain<>(Underscore.uniq(list));
        }

        public  Chain uniq(final Function func) {
            return new Chain<>(Underscore.newArrayList(Underscore.uniq(list, func)));
        }

        public Chain distinct() {
            return new Chain<>(Underscore.uniq(list));
        }

        @SuppressWarnings("unchecked")
        public  Chain distinctBy(final Function func) {
            return new Chain<>(Underscore.newArrayList((Iterable) Underscore.uniq(list, func)));
        }

        @SuppressWarnings("unchecked")
        public Chain union(final List... lists) {
            return new Chain<>(Underscore.union(list, lists));
        }

        @SuppressWarnings("unchecked")
        public Chain intersection(final List... lists) {
            return new Chain<>(Underscore.intersection(list, lists));
        }

        @SuppressWarnings("unchecked")
        public Chain difference(final List... lists) {
            return new Chain<>(Underscore.difference(list, lists));
        }

        public Chain range(final int stop) {
            return new Chain<>(Underscore.range(stop));
        }

        public Chain range(final int start, final int stop) {
            return new Chain<>(Underscore.range(start, stop));
        }

        public Chain range(final int start, final int stop, final int step) {
            return new Chain<>(Underscore.range(start, stop, step));
        }

        public Chain> chunk(final int size) {
            return new Chain<>(Underscore.chunk(value(), size, size));
        }

        public Chain> chunk(final int size, final int step) {
            return new Chain<>(Underscore.chunk(value(), size, step));
        }

        public Chain> chunkFill(final int size, final T fillValue) {
            return new Chain<>(Underscore.chunkFill(value(), size, size, fillValue));
        }

        public Chain> chunkFill(final int size, final int step, final T fillValue) {
            return new Chain<>(Underscore.chunkFill(value(), size, step, fillValue));
        }

        public Chain cycle(final int times) {
            return new Chain<>(Underscore.cycle(value(), times));
        }

        public Chain interpose(final T element) {
            return new Chain<>(Underscore.interpose(value(), element));
        }

        public Chain interposeByList(final Iterable interIter) {
            return new Chain<>(Underscore.interposeByList(value(), interIter));
        }

        @SuppressWarnings("unchecked")
        public Chain concat(final List... lists) {
            return new Chain<>(Underscore.concat(list, lists));
        }

        public Chain slice(final int start) {
            return new Chain<>(Underscore.slice(list, start));
        }

        public Chain slice(final int start, final int end) {
            return new Chain<>(Underscore.slice(list, start, end));
        }

        public Chain> splitAt(final int position) {
            return new Chain<>(Underscore.splitAt(list, position));
        }

        public Chain takeSkipping(final int stepSize) {
            return new Chain<>(Underscore.takeSkipping(list, stepSize));
        }

        public Chain reverse() {
            return new Chain<>(Underscore.reverse(list));
        }

        public Chain join() {
            return new Chain<>(Underscore.join(list));
        }

        public Chain join(final String separator) {
            return new Chain<>(Underscore.join(list, separator));
        }

        @SuppressWarnings("unchecked")
        public Chain push(final T... values) {
            return new Chain<>(Underscore.push(value(), values));
        }

        public Chain>> pop() {
            return new Chain<>(Underscore.pop(value()));
        }

        public Chain>> shift() {
            return new Chain<>(Underscore.shift(value()));
        }

        @SuppressWarnings("unchecked")
        public Chain unshift(final T... values) {
            return new Chain<>(Underscore.unshift(value(), values));
        }

        public Chain skip(final int numberToSkip) {
            return new Chain<>(list.subList(numberToSkip, list.size()));
        }

        public Chain limit(final int size) {
            return new Chain<>(Underscore.first(list, size));
        }

        @SuppressWarnings("unchecked")
        public  Chain> toMap() {
            return new Chain<>(Underscore.toMap((Iterable>) list));
        }

        public boolean isEmpty() {
            return Underscore.isEmpty(list);
        }

        public boolean isNotEmpty() {
            return Underscore.isNotEmpty(list);
        }

        public int size() {
            return Underscore.size(list);
        }

        public T item() {
            return item;
        }

        /*
         * Documented, #value
         */
        public List value() {
            return list;
        }

        public Map map() {
            return map;
        }

        public List toList() {
            return list;
        }

        public String toString() {
            return String.valueOf(list);
        }
    }

    /*
     * Documented, #mixin
     */
    public static void mixin(final String funcName, final UnaryOperator func) {
        FUNCTIONS.put(funcName, func);
    }

    public Optional call(final String funcName) {
        if (string.isPresent() && FUNCTIONS.containsKey(funcName)) {
            return Optional.of(FUNCTIONS.get(funcName).apply(string.get()));
        }
        return Optional.empty();
    }

    public static > List sort(final Iterable iterable) {
        final List localList = newArrayList(iterable);
        Collections.sort(localList);
        return localList;
    }

    @SuppressWarnings("unchecked")
    public static > T[] sort(final T... array) {
        final T[] localArray = array.clone();
        Arrays.sort(localArray);
        return localArray;
    }

    @SuppressWarnings("unchecked")
    public List sort() {
        return sort((Iterable) iterable);
    }

    /*
     * Documented, #join
     */
    public static  String join(final Iterable iterable, final String separator) {
        final StringBuilder sb = new StringBuilder();
        int index = 0;
        for (final T item : iterable) {
            if (index > 0) {
                sb.append(separator);
            }
            sb.append(item.toString());
            index += 1;
        }
        return sb.toString();
    }

    public static  String joinToString(final Iterable iterable, final String separator,
                                          final String prefix, final String postfix,
                                          final int limit,
                                          final String truncated,
                                          final Function transform) {
        final StringBuilder sb = new StringBuilder();
        int index = 0;
        if (prefix != null) {
            sb.append(prefix);
        }
        for (final T item : iterable) {
            if (index > 0) {
                sb.append(separator);
            }
            index += 1;
            if (limit < 0 || index <= limit) {
                sb.append(transform == null ? item.toString() : transform.apply(item));
            } else {
                break;
            }
        }
        joinToStringPostfix(postfix, limit, truncated, index, sb);
        return sb.toString();
    }

    private static void joinToStringPostfix(String postfix, int limit, String truncated, int index, StringBuilder sb) {
        if (limit >= 0 && index > limit) {
            sb.append(truncated == null ? "..." : truncated);
        }
        if (postfix != null) {
            sb.append(postfix);
        }
    }

    public static  String join(final Iterable iterable) {
        return join(iterable, " ");
    }

    public static  String join(final T[] array, final String separator) {
        return join(Arrays.asList(array), separator);
    }

    public static  String join(final T[] array) {
        return join(array, " ");
    }

    public String join(final String separator) {
        return join(iterable, separator);
    }

    public String join() {
        return join(iterable);
    }

    @SuppressWarnings("unchecked")
    public static  List push(final List list, final T... values) {
        final List result = newArrayList(list);
        Collections.addAll(result, values);
        return result;
    }

    @SuppressWarnings("unchecked")
    public List push(final T... values) {
        return push((List) getIterable(), values);
    }

    public static  Map.Entry> pop(final List list) {
        return Map.entry(last(list), initial(list));
    }

    public Map.Entry> pop() {
        return pop((List) getIterable());
    }

    @SuppressWarnings("unchecked")
    public static  List unshift(final List list, final T... values) {
        final List result = newArrayList(list);
        int index = 0;
        for (T value : values) {
            result.add(index, value);
            index += 1;
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    public List unshift(final T... values) {
        return unshift((List) getIterable(), values);
    }

    public static  Map.Entry> shift(final List list) {
        return Map.entry(first(list), rest(list));
    }

    public Map.Entry> shift() {
        return shift((List) getIterable());
    }

    @SuppressWarnings("unchecked")
    public static  T[] concat(final T[] first, final T[]... other) {
        int length = 0;
        for (T[] otherItem : other) {
            length += otherItem.length;
        }
        final T[] result = Arrays.copyOf(first, first.length + length);
        int index = 0;
        for (T[] otherItem : other) {
            System.arraycopy(otherItem, 0, result, first.length + index, otherItem.length);
            index += otherItem.length;
        }
        return result;
    }

    /*
     * Documented, #concat
     */
    @SuppressWarnings("unchecked")
    public static  List concat(final Iterable first, final Iterable... other) {
        List list = newArrayList(first);
        for (Iterable iter : other) {
            list.addAll(newArrayList(iter));
        }
        return list;
    }

    @SuppressWarnings("unchecked")
    public List concatWith(final Iterable... other) {
        return concat(iterable, other);
    }

    /*
     * Documented, #slice
     */
    public static  List slice(final Iterable iterable, final int start) {
        final List result;
        if (start >= 0) {
            result = newArrayList(iterable).subList(start, size(iterable));
        } else {
            result = newArrayList(iterable).subList(size(iterable) + start, size(iterable));
        }
        return result;
    }

    public static  T[] slice(final T[] array, final int start) {
        final T[] result;
        if (start >= 0) {
            result = Arrays.copyOfRange(array, start, array.length);
        } else {
            result = Arrays.copyOfRange(array, array.length + start, array.length);
        }
        return result;
    }

    public List slice(final int start) {
        return slice(iterable, start);
    }

    public static  List slice(final Iterable iterable, final int start, final int end) {
        final List result;
        if (start >= 0) {
            if (end > 0) {
                result = newArrayList(iterable).subList(start, end);
            } else {
                result = newArrayList(iterable).subList(start, size(iterable) + end);
            }
        } else {
            if (end > 0) {
                result = newArrayList(iterable).subList(size(iterable) + start, end);
            } else {
                result =
                        newArrayList(iterable)
                                .subList(size(iterable) + start, size(iterable) + end);
            }
        }
        return result;
    }

    public static  T[] slice(final T[] array, final int start, final int end) {
        final T[] result;
        if (start >= 0) {
            if (end > 0) {
                result = Arrays.copyOfRange(array, start, end);
            } else {
                result = Arrays.copyOfRange(array, start, array.length + end);
            }
        } else {
            if (end > 0) {
                result = Arrays.copyOfRange(array, array.length + start, end);
            } else {
                result = Arrays.copyOfRange(array, array.length + start, array.length + end);
            }
        }
        return result;
    }

    public List slice(final int start, final int end) {
        return slice(iterable, start, end);
    }

    public static  List> splitAt(final Iterable iterable, final int position) {
        List> result = new ArrayList<>();
        int size = size(iterable);
        final int index;
        if (position < 0) {
            index = 0;
        } else {
            index = Math.min(position, size);
        }
        result.add(newArrayList(iterable).subList(0, index));
        result.add(newArrayList(iterable).subList(index, size));
        return result;
    }

    public static  List> splitAt(final T[] array, final int position) {
        return splitAt(Arrays.asList(array), position);
    }

    public List> splitAt(final int position) {
        return splitAt(iterable, position);
    }

    public static  List takeSkipping(final Iterable iterable, final int stepSize) {
        List result = new ArrayList<>();
        if (stepSize <= 0) {
            return result;
        }
        int size = size(iterable);
        if (stepSize > size) {
            result.add(first(iterable));
            return result;
        }
        int i = 0;
        for (T element : iterable) {
            if (i++ % stepSize == 0) {
                result.add(element);
            }
        }
        return result;
    }

    public static  List takeSkipping(final T[] array, final int stepSize) {
        return takeSkipping(Arrays.asList(array), stepSize);
    }

    public List takeSkipping(final int stepSize) {
        return takeSkipping(iterable, stepSize);
    }

    /*
     * Documented, #reverse
     */
    public static  List reverse(final Iterable iterable) {
        final List result = newArrayList(iterable);
        Collections.reverse(result);
        return result;
    }

    @SuppressWarnings("unchecked")
    public static  T[] reverse(final T... array) {
        T temp;
        final T[] newArray = array.clone();
        for (int index = 0; index < array.length / 2; index += 1) {
            temp = newArray[index];
            newArray[index] = newArray[array.length - 1 - index];
            newArray[array.length - 1 - index] = temp;
        }
        return newArray;
    }

    public static List reverse(final int[] array) {
        final List result = newIntegerList(array);
        Collections.reverse(result);
        return result;
    }

    public List reverse() {
        return reverse(iterable);
    }

    public Iterable getIterable() {
        return iterable;
    }

    public Iterable value() {
        return iterable;
    }

    public Optional getString() {
        return string;
    }

    public static  java.util.concurrent.ScheduledFuture setTimeout(
            final Supplier function, final int delayMilliseconds) {
        return delay(function, delayMilliseconds);
    }

    public static void clearTimeout(java.util.concurrent.ScheduledFuture scheduledFuture) {
        if (scheduledFuture != null) {
            scheduledFuture.cancel(true);
        }
    }

    public static  java.util.concurrent.ScheduledFuture setInterval(
            final Supplier function, final int delayMilliseconds) {
        final java.util.concurrent.ScheduledExecutorService scheduler =
                java.util.concurrent.Executors.newSingleThreadScheduledExecutor();
        return scheduler.scheduleAtFixedRate(
                function::get,
                delayMilliseconds,
                delayMilliseconds,
                java.util.concurrent.TimeUnit.MILLISECONDS);
    }

    public static void clearInterval(java.util.concurrent.ScheduledFuture scheduledFuture) {
        clearTimeout(scheduledFuture);
    }

    public static  List copyOf(final Iterable iterable) {
        return newArrayList(iterable);
    }

    public List copyOf() {
        return newArrayList(value());
    }

    public static  List copyOfRange(
            final Iterable iterable, final int start, final int end) {
        return slice(iterable, start, end);
    }

    public List copyOfRange(final int start, final int end) {
        return slice(value(), start, end);
    }

    public static  T elementAt(final List list, final int index) {
        return list.get(index);
    }

    public T elementAt(final int index) {
        return elementAt((List) value(), index);
    }

    public static  T get(final List list, final int index) {
        return elementAt(list, index);
    }

    public T get(final int index) {
        return elementAt((List) value(), index);
    }

    public static  Map.Entry> set(
            final List list, final int index, final T value) {
        final List newList = newArrayList(list);
        return Map.entry(newList.set(index, value), newList);
    }

    public Map.Entry> set(final int index, final T value) {
        return set((List) value(), index, value);
    }

    public static  T elementAtOrElse(final List list, final int index, T defaultValue) {
        try {
            return list.get(index);
        } catch (IndexOutOfBoundsException ex) {
            return defaultValue;
        }
    }

    public T elementAtOrElse(final int index, T defaultValue) {
        return elementAtOrElse((List) value(), index, defaultValue);
    }

    public static  T elementAtOrNull(final List list, final int index) {
        try {
            return list.get(index);
        } catch (IndexOutOfBoundsException ex) {
            return null;
        }
    }

    public T elementAtOrNull(final int index) {
        return elementAtOrNull((List) value(), index);
    }

    public static  int lastIndex(final Iterable iterable) {
        return size(iterable) - 1;
    }

    public static  int lastIndex(final T[] array) {
        return array.length - 1;
    }

    public static int lastIndex(final int[] array) {
        return array.length - 1;
    }

    public static  T checkNotNull(T reference) {
        if (reference == null) {
            throw new NullPointerException();
        }
        return reference;
    }

    public static  List checkNotNullElements(List references) {
        if (references == null) {
            throw new NullPointerException();
        }
        for (T reference : references) {
            checkNotNull(reference);
        }
        return references;
    }

    public static  T checkNotNull(T reference, Object errorMessage) {
        if (reference == null) {
            throw new NullPointerException(String.valueOf(errorMessage));
        }
        return reference;
    }

    public static boolean nonNull(Object obj) {
        return obj != null;
    }

    public static  T defaultTo(T value, T defaultValue) {
        if (value == null) {
            return defaultValue;
        }
        return value;
    }

    protected static  List newArrayList(final Iterable iterable) {
        final List result;
        if (iterable instanceof Collection) {
            result = new ArrayList<>((Collection) iterable);
        } else {
            result = new ArrayList<>();
            for (final T item : iterable) {
                result.add(item);
            }
        }
        return result;
    }

    protected static  List newArrayList(final T object) {
        final List result = new ArrayList<>();
        result.add(object);
        return result;
    }

    protected static  List newArrayList(final Iterable iterable, final int size) {
        final List result = new ArrayList<>();
        for (int index = 0; iterable.iterator().hasNext() && index < size; index += 1) {
            result.add(iterable.iterator().next());
        }
        return result;
    }

    protected static List newIntegerList(int... array) {
        final List result = new ArrayList<>(array.length);
        for (final int item : array) {
            result.add(item);
        }
        return result;
    }

    protected static  List newArrayListWithExpectedSize(int size) {
        return new ArrayList<>((int) (CAPACITY_SIZE_5 + size + (size / 10)));
    }

    protected static  Set newLinkedHashSet(Iterable iterable) {
        final Set result = new LinkedHashSet<>();
        for (final T item : iterable) {
            result.add(item);
        }
        return result;
    }

    protected static  Set newLinkedHashSetWithExpectedSize(int size) {
        return new LinkedHashSet<>((int) Math.max(size * CAPACITY_COEFF_2, CAPACITY_SIZE_16));
    }

    @SuppressWarnings("unchecked")
    public static  Predicate and(
            final Predicate pred1,
            final Predicate pred2,
            final Predicate... rest) {
        checkNotNull(pred1);
        checkNotNull(pred2);
        checkNotNullElements(Arrays.asList(rest));
        return value -> {
            boolean result = pred1.test(value) && pred2.test(value);
            if (!result) {
                return false;
            }
            for (Predicate predicate : rest) {
                if (!predicate.test(value)) {
                    return false;
                }
            }
            return true;
        };
    }

    @SuppressWarnings("unchecked")
    public static  Predicate or(
            final Predicate pred1,
            final Predicate pred2,
            final Predicate... rest) {
        checkNotNull(pred1);
        checkNotNull(pred2);
        checkNotNullElements(Arrays.asList(rest));
        return value -> {
            boolean result = pred1.test(value) || pred2.test(value);
            if (result) {
                return true;
            }
            for (Predicate predicate : rest) {
                if (predicate.test(value)) {
                    return true;
                }
            }
            return false;
        };
    }

    public static void main(String... args) {
        final String message =
                "Underscore-java is a java port of Underscore.js.\n\n"
                        + "In addition to porting Underscore's functionality,"
                        + " Underscore-java includes matching unit tests.\n\n"
                        + "For docs, license, tests, and downloads, see: https://javadev.github.io/underscore-java";
        System.out.println(message);
    }

    public interface Function3 {
        T apply(F1 arg1, F2 arg2, F3 arg3);
    }

    public abstract static class MemoizeFunction implements Function {
        private final Map cache = new LinkedHashMap<>();

        public abstract T calc(final F n);

        public T apply(final F key) {
            cache.putIfAbsent(key, calc(key));
            return cache.get(key);
        }
    }

    public interface PredicateIndexed {
        boolean test(int index, T arg);
    }

    public interface Template extends Function {
        List check(T arg);
    }
}