CollectingIterable.java

package no.motif.iter;

import static java.util.Collections.sort;
import static java.util.Collections.unmodifiableList;
import static no.motif.Base.toString;
import static no.motif.Iterate.by;
import static no.motif.Singular.optional;
import static no.motif.Strings.concat;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import no.motif.Singular;
import no.motif.Strings;
import no.motif.f.Do;
import no.motif.f.Fn;
import no.motif.f.Fn2;
import no.motif.f.Predicate;
import no.motif.single.Optional;
import no.motif.types.Elements;

/**
 * This iterable offers operations which requires collecting (i.e.
 * iterating) the containing elements of the iterable. In
 * particular, this class holds the methods which "bridges" back
 * to the non-lazy Java Collection Framework classes.
 *
 * @param <T> The type of elements in this iterable.
 */
abstract class CollectingIterable<T> implements Elements<T>, Serializable {

    @Override
    public final <O> O reduce(O unit, Fn2<? super O, ? super T, ? extends O> reducer) {
        O reduced = unit;
        for (T element : this) reduced = reducer.$(reduced, element);
        return reduced;
    }


    @Override
    public final List<T> collect() {
        return unmodifiableList(collectIn(new ArrayList<T>()));
    }


    @Override
    public final <C extends Collection<T>> C collectIn(C collection) {
        for (T t : this) {
            collection.add(t);
        }
        return collection;
    }



    @Override
    public final <P extends Comparable<P>> List<T> sortedBy(Fn<? super T, P> property) {
        return sorted(by(property));
    }



    @Override
    public final List<T> sorted(Comparator<? super T> comparator) {
        List<T> elements = collectIn(new ArrayList<T>());
        sort(elements, comparator);
        return unmodifiableList(elements);
    }


    @Override
    public void each(Do<? super T> sideEffect) {
        for (T element : this) sideEffect.with(element);
    }


    @Override
    public <P> Map<P, List<T>> groupBy(Fn<? super T, P> property) {
        Map<P, List<T>> map = new LinkedHashMap<>();
        for (T elem : this) {
            P key = property.$(elem);
            List<T> list;
            if (!map.containsKey(key)) {
                list = new ArrayList<>();
                map.put(key, list);
            } else {
                list = map.get(key);
            }
            list.add(elem);
        }
        return map;
    }


    @Override
    public <P> Map<P, T> mapBy(Fn<? super T, P> uniqueProperty) {
        Map<P, T> map = new LinkedHashMap<>();
        for (T elem : this) {
            P key = uniqueProperty.$(elem);
            if (!map.containsKey(key)) map.put(key, elem);
            else throw new IllegalStateException(
                    "Cannot create the map since both '" + map.get(key) + "' and '" + elem + "' yields " +
                    "the same key: '" + key + "'. This is either due to an inconsistency in " +
                    "your data, or you should use .groupBy(Fn) instead to allow multiple elements " +
                    "being mapped by the same key.");
        }
        return map;
    }


    @Override
    public boolean isEmpty() {
        return !iterator().hasNext();
    }


    @Override
    public boolean exists(Predicate<? super T> predicate) {
        for (T t : this) if (predicate.$(t)) return true;
        return false;
    }


    @Override
    public Optional<T> head() {
        return !isEmpty() ? optional(iterator().next()) : Singular.<T>none();
    }


    @Override
    public Optional<T> last() {
        T last = null;
        for (T elem : this) last = elem;
        return optional(last);
    }


    @Override
    public String join() {
        return reduce("", concat);
    }


    @Override
    public String join(String separator) {
        if (isEmpty()) return "";
        return head().map(toString).append(tail().map(Strings.prepend(separator))).reduce("", concat);
    }


    /**
     * Textual description of the contents of the iterable. Have in mind that for lazy
     * implementations, using {@link #toString()} will iterate over the elements to
     * actually be able to create the description.
     */
    @Override
    public String toString() {
        return collect().toString();
    }

}