Emulate Java8 Collectors Change-Id: I948aad38f4250d27e80c172fd56bb44d50cf10af
diff --git a/user/super/com/google/gwt/emul/java/util/stream/Collector.java b/user/super/com/google/gwt/emul/java/util/stream/Collector.java new file mode 100644 index 0000000..ea96bb5 --- /dev/null +++ b/user/super/com/google/gwt/emul/java/util/stream/Collector.java
@@ -0,0 +1,142 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package java.util.stream; + +import static javaemul.internal.InternalPreconditions.checkNotNull; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * See <a + * href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html">the + * official Java API doc</a> for details. + * @param <T> the type of data to be collected + * @param <A> the type of accumulator used to track results + * @param <R> the final output data type + */ +public interface Collector<T,A,R> { + + /** + * See <a + * href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.Characteristics.html">the + * official Java API doc</a> for details. + */ + enum Characteristics { CONCURRENT, IDENTITY_FINISH, UNORDERED } + + static <T,A,R> Collector<T,A,R> of(Supplier<A> supplier, BiConsumer<A,T> accumulator, BinaryOperator<A> combiner, Function<A,R> finisher, Collector.Characteristics... characteristics) { + checkNotNull(supplier); + checkNotNull(accumulator); + checkNotNull(combiner); + checkNotNull(finisher); + checkNotNull(characteristics); + return new CollectorImpl<>( + supplier, + accumulator, + combiner, + finisher, + characteristics + ); + } + + static <T,R> Collector<T,R,R> of(Supplier<R> supplier, BiConsumer<R,T> accumulator, BinaryOperator<R> combiner, Collector.Characteristics... characteristics) { + checkNotNull(supplier); + checkNotNull(accumulator); + checkNotNull(combiner); + checkNotNull(characteristics); + return new CollectorImpl<>( + supplier, + accumulator, + combiner, + Function.identity(), + Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, characteristics)) + ); + } + + Supplier<A> supplier(); + + BiConsumer<A,T> accumulator(); + + Set<Collector.Characteristics> characteristics(); + + BinaryOperator<A> combiner(); + + Function<A,R> finisher(); + + /** + * Simple internal implementation of a collector, holding each of the functions in a field. + */ + static final class CollectorImpl<T, A, R> implements Collector<T, A, R> { + private final Supplier<A> supplier; + private final BiConsumer<A, T> accumulator; + private final Set<Collector.Characteristics> characteristics; + private final BinaryOperator<A> combiner; + private final Function<A, R> finisher; + + public CollectorImpl(Supplier<A> supplier, BiConsumer<A, T> accumulator, BinaryOperator<A> combiner, Function<A, R> finisher, Characteristics... characteristics) { + this.supplier = supplier; + this.accumulator = accumulator; + if (characteristics.length == 0) { + this.characteristics = Collections.emptySet(); + } else if (characteristics.length == 1) { + this.characteristics = Collections.singleton(characteristics[0]); + } else { + this.characteristics = Collections.unmodifiableSet(EnumSet.of(characteristics[0], characteristics)); + } + this.combiner = combiner; + this.finisher = finisher; + } + + public CollectorImpl(Supplier<A> supplier, BiConsumer<A, T> accumulator, BinaryOperator<A> combiner, Function<A, R> finisher, Set<Characteristics> characteristics) { + this.supplier = supplier; + this.accumulator = accumulator; + this.combiner = combiner; + this.finisher = finisher; + this.characteristics = characteristics; + } + + @Override + public Supplier<A> supplier() { + return supplier; + } + + @Override + public BiConsumer<A, T> accumulator() { + return accumulator; + } + + @Override + public BinaryOperator<A> combiner() { + return combiner; + } + + @Override + public Function<A, R> finisher() { + return finisher; + } + + @Override + public Set<Characteristics> characteristics() { + return characteristics; + } + } +} \ No newline at end of file
diff --git a/user/super/com/google/gwt/emul/java/util/stream/Collectors.java b/user/super/com/google/gwt/emul/java/util/stream/Collectors.java new file mode 100644 index 0000000..620e0ed --- /dev/null +++ b/user/super/com/google/gwt/emul/java/util/stream/Collectors.java
@@ -0,0 +1,339 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package java.util.stream; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.DoubleSummaryStatistics; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IntSummaryStatistics; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.LongSummaryStatistics; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; + +/** + * See <a + * href="https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html">the + * official Java API doc</a> for details. + */ +public final class Collectors { + public static <T> Collector<T,?,Double> averagingDouble(ToDoubleFunction<? super T> mapper) { + // TODO simplify to only collect average if possible + return collectingAndThen(summarizingDouble(mapper), DoubleSummaryStatistics::getAverage); + } + + public static <T> Collector<T,?,Double> averagingInt(ToIntFunction<? super T> mapper) { + // TODO simplify to only collect average if possible + return collectingAndThen(summarizingInt(mapper), IntSummaryStatistics::getAverage); + } + + public static <T> Collector<T,?,Double> averagingLong(ToLongFunction<? super T> mapper) { + // TODO simplify to only collect average if possible + return collectingAndThen(summarizingLong(mapper), LongSummaryStatistics::getAverage); + } + + public static <T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream, Function<R,RR> finisher) { + return new Collector.CollectorImpl<>( + downstream.supplier(), + downstream.accumulator(), + downstream.combiner(), + downstream.finisher().andThen(finisher), + removeIdentFinisher(downstream.characteristics()) + ); + } + + public static <T> Collector<T,?,Long> counting() { + return reducing(0L, item -> 1L, Long::sum); + } + + public static <T,K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier) { + // TODO inline this and avoid the finisher extra work of copying from a map to another map + // kept separate for now to unify implementations and reduce testing required + return groupingBy(classifier, toList()); + } + + public static <T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream) { + return groupingBy(classifier, HashMap::new, downstream); + } + + public static <T,K,D,A,M extends Map<K,D>> Collector<T,?,M> groupingBy(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream) { + return Collector.<T, Map<K, List<T>>, M>of( + LinkedHashMap::new, + (m, o) -> { + K k = classifier.apply(o); + List<T> l = m.get(k); + if (l == null) { + l = new ArrayList<>(); + m.put(k, l); + } + l.add(o); + + }, + (m1, m2) -> mergeAll(m1, m2, Collectors::addAll), + m -> { + M result = mapFactory.get(); + for (Map.Entry<K, List<T>> entry : m.entrySet()) { + result.put(entry.getKey(), streamAndCollect(downstream, entry.getValue())); + } + return result; + }); + } + +// not supported +// public static <T,K> Collector<T,?,ConcurrentMap<K,List<T>>> groupingByConcurrent(Function<? super T,? extends K> classifier) +// public static <T,K,A,D> Collector<T,?,ConcurrentMap<K,D>> groupingByConcurrent(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream) +// public static <T,K,A,D,M extends ConcurrentMap<K,D>> Collector<T,?,M> groupingByConcurrent(Function<? super T,? extends K> classifier, Supplier<M> mapFactory, Collector<? super T,A,D> downstream) + + public static Collector<CharSequence,?,String> joining() { + // specific implementation rather than calling joining("") since we don't need to worry about + // appending delimiters between empty strings + return Collector.of( + StringBuilder::new, + StringBuilder::append, + StringBuilder::append, + StringBuilder::toString + ); + } + + public static Collector<CharSequence,?,String> joining(CharSequence delimiter) { + return joining(delimiter, "", ""); + } + + public static Collector<CharSequence,?,String> joining(final CharSequence delimiter, CharSequence prefix, CharSequence suffix) { + return Collector.of( + () -> new StringJoiner(delimiter, prefix, suffix), + StringJoiner::add, + StringJoiner::merge, + StringJoiner::toString + ); + } + + public static <T,U,A,R> Collector<T,?,R> mapping(final Function<? super T,? extends U> mapper, final Collector<? super U,A,R> downstream) { + return new Collector.CollectorImpl<>( + downstream.supplier(), + (BiConsumer<A, T>) (A a, T t) -> { + downstream.accumulator().accept(a, mapper.apply(t)); + }, + downstream.combiner(), + downstream.finisher(), + downstream.characteristics() + ); + } + + public static <T> Collector<T,?,Optional<T>> maxBy(Comparator<? super T> comparator) { + return minBy(comparator.reversed()); + } + + public static <T> Collector<T,?,Optional<T>> minBy(final Comparator<? super T> comparator) { + return reducing((a, b) -> comparator.compare(a, b) < 0 ? a : b); + } + + public static <T> Collector<T,?,Map<Boolean,List<T>>> partitioningBy(Predicate<? super T> predicate) { + // calling groupBy directly rather than partioningBy so that it can be optimized later + return groupingBy(predicate::test); + } + + public static <T,D,A> Collector<T,?,Map<Boolean,D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T,A,D> downstream) { + return groupingBy(predicate::test, downstream); + } + + public static <T> Collector<T,?,Optional<T>> reducing(BinaryOperator<T> op) { + return reducing(Optional.empty(), Optional::of, (a, b) -> { + if (!a.isPresent()) { + return b; + } + if (!b.isPresent()) { + return a; + } + return Optional.of(op.apply(a.get(), b.get())); + }); + } + + public static <T> Collector<T,?,T> reducing(T identity, BinaryOperator<T> op) { + return reducing(identity, Function.identity(), op); + } + + @SuppressWarnings("unchecked") + public static <T,U> Collector<T,?,U> reducing(final U identity, final Function<? super T,? extends U> mapper, BinaryOperator<U> op) { + return Collector.of( + () -> new Object[]{identity}, + (u, t) -> u[0] = op.apply((U) u[0], mapper.apply(t)), + (Object[] u1, Object[] u2) -> { + u1[0] = op.apply((U) u1[0], (U) u2[0]); + return u1; + }, + (Object[] a) -> (U) a[0] + ); + } + + public static <T> Collector<T,?,DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper) { + return Collector.of( + DoubleSummaryStatistics::new, + (stats, item) -> stats.accept(mapper.applyAsDouble(item)), + (t, u) -> { + t.combine(u); + return t; + }, + Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH + ); + } + + public static <T> Collector<T,?,IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper) { + return Collector.of( + IntSummaryStatistics::new, + (stats, item) -> stats.accept(mapper.applyAsInt(item)), + (t, u) -> { + t.combine(u); + return t; + }, + Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH + ); + } + + public static <T> Collector<T,?,LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper) { + return Collector.of( + LongSummaryStatistics::new, + (stats, item) -> stats.accept(mapper.applyAsLong(item)), + (t, u) -> { + t.combine(u); + return t; + }, + Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH + ); + } + + public static <T> Collector<T,?,Double> summingDouble(final ToDoubleFunction<? super T> mapper) { + // TODO simplify to only collect sum if possible + return collectingAndThen(summarizingDouble(mapper), DoubleSummaryStatistics::getSum); + } + + public static <T> Collector<T,?,Integer> summingInt(ToIntFunction<? super T> mapper) { + // TODO simplify to only collect sum if possible + return collectingAndThen(summarizingInt(mapper), intSummaryStatistics -> (int) intSummaryStatistics.getSum()); + } + + public static <T> Collector<T,?,Long> summingLong(ToLongFunction<? super T> mapper) { + // TODO simplify to only collect sum if possible + return collectingAndThen(summarizingLong(mapper), LongSummaryStatistics::getSum); + } + + public static <T,C extends Collection<T>> Collector<T,?,C> toCollection(final Supplier<C> collectionFactory) { + return Collector.of( + collectionFactory, + Collection::add, + // TODO switch to a lambda reference once #9333 is fixed + (c1, c2) -> addAll(c1, c2), + Collector.Characteristics.IDENTITY_FINISH + ); + } + +// not supported +// public static <T,K,U> Collector<T,?,ConcurrentMap<K,U>> toConcurrentMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper) +// public static <T,K,U> Collector<T,?,ConcurrentMap<K,U>> toConcurrentMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction) +// public static <T,K,U,M extends ConcurrentMap<K,U>> Collector<T,?,M> toConcurrentMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier) + + public static <T> Collector<T,?,List<T>> toList() { + return toCollection(ArrayList::new); + } + + public static <T,K,U> Collector<T,?,Map<K,U>> toMap(final Function<? super T,? extends K> keyMapper, final Function<? super T,? extends U> valueMapper) { + return toMap(keyMapper, valueMapper, (m1, m2) -> { throw new IllegalStateException("Can't assign multiple values to the same key"); }); + } + + public static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction) { + return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); + } + + public static <T,K,U,M extends Map<K,U>> Collector<T,?,M> toMap(final Function<? super T,? extends K> keyMapper, final Function<? super T,? extends U> valueMapper, final BinaryOperator<U> mergeFunction, final Supplier<M> mapSupplier) { + return Collector.of( + mapSupplier, + (map, item) -> { + K key = keyMapper.apply(item); + U newValue = valueMapper.apply(item); + if (map.containsKey(key)) { + map.put(key, mergeFunction.apply(map.get(key), newValue)); + } else { + map.put(key, newValue); + } + }, + (m1, m2) -> mergeAll(m1, m2, mergeFunction), + Collector.Characteristics.IDENTITY_FINISH + ); + } + + public static <T> Collector<T,?,Set<T>> toSet() { + return Collector.<T, HashSet<T>, Set<T>>of( + HashSet::new, + HashSet::add, + // TODO switch to a lambda reference once #9333 is fixed + (c1, c2) -> addAll(c1, c2), + // this is Function.identity, but Java doesn't like it here to change types. + s -> s, + Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH + ); + } + + private static Set<Collector.Characteristics> removeIdentFinisher(Set<Collector.Characteristics> characteristics) { + if (!characteristics.contains(Collector.Characteristics.IDENTITY_FINISH)) { + return characteristics; + } + + if (characteristics.size() == 1) { + return Collections.emptySet(); + } + + EnumSet<Collector.Characteristics> result = EnumSet.copyOf(characteristics); + result.remove(Collector.Characteristics.IDENTITY_FINISH); + return Collections.unmodifiableSet(result); + } + + private static <T, D, A> D streamAndCollect(Collector<? super T, A, D> downstream, List<T> list) { + A a = downstream.supplier().get(); + for (T t : list) { + downstream.accumulator().accept(a, t); + } + return downstream.finisher().apply(a); + } + + private static <K, V, M extends Map<K, V>> M mergeAll(M m1, M m2, BinaryOperator<V> mergeFunction) { + for (Map.Entry<K, V> entry : m2.entrySet()) { + m1.merge(entry.getKey(), entry.getValue(), mergeFunction); + } + return m1; + } + + private static <T, C extends Collection<T>> C addAll(C collection, Collection<T> items) { + collection.addAll(items); + return collection; + } +}
diff --git a/user/test/com/google/gwt/emultest/EmulJava8Suite.java b/user/test/com/google/gwt/emultest/EmulJava8Suite.java index 62c6526..fbe0c91 100644 --- a/user/test/com/google/gwt/emultest/EmulJava8Suite.java +++ b/user/test/com/google/gwt/emultest/EmulJava8Suite.java
@@ -38,6 +38,7 @@ import com.google.gwt.emultest.java8.util.StringJoinerTest; import com.google.gwt.emultest.java8.util.TreeMapTest; import com.google.gwt.emultest.java8.util.VectorTest; +import com.google.gwt.emultest.java8.util.stream.CollectorsTest; import com.google.gwt.junit.tools.GWTTestSuite; import junit.framework.Test; @@ -76,6 +77,10 @@ suite.addTestSuite(DoubleSummaryStatisticsTest.class); suite.addTestSuite(IntSummaryStatisticsTest.class); suite.addTestSuite(LongSummaryStatisticsTest.class); + + //-- java.util.stream + suite.addTestSuite(CollectorsTest.class); + return suite; } }
diff --git a/user/test/com/google/gwt/emultest/java8/util/stream/CollectorsTest.java b/user/test/com/google/gwt/emultest/java8/util/stream/CollectorsTest.java new file mode 100644 index 0000000..7a14614 --- /dev/null +++ b/user/test/com/google/gwt/emultest/java8/util/stream/CollectorsTest.java
@@ -0,0 +1,546 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.gwt.emultest.java8.util.stream; + +import static java.util.stream.Collectors.averagingDouble; +import static java.util.stream.Collectors.averagingInt; +import static java.util.stream.Collectors.averagingLong; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.counting; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.maxBy; +import static java.util.stream.Collectors.minBy; +import static java.util.stream.Collectors.partitioningBy; +import static java.util.stream.Collectors.summarizingDouble; +import static java.util.stream.Collectors.summarizingInt; +import static java.util.stream.Collectors.summarizingLong; +import static java.util.stream.Collectors.summingDouble; +import static java.util.stream.Collectors.summingInt; +import static java.util.stream.Collectors.summingLong; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; + +import com.google.gwt.emultest.java.util.EmulTestBase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.DoubleSummaryStatistics; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IntSummaryStatistics; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.LongSummaryStatistics; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; + +/** + * Tests {@link java.util.stream.Collectors}. + * <p /> + * Methods that are presently only tested indirectly: + * <ul> + * <li>reducing: counting, minBy/maxBy use this</li> + * <li>toCollection tested by toList (toSet now uses its own impl)</li> + * </ul> + */ +public class CollectorsTest extends EmulTestBase { + + public void testAveragingDouble() { + Collector<Double, ?, Double> c = averagingDouble(Double::doubleValue); + applyItems((4.0 + 8.0) / 2.0, c, 4.0, 8.0); + + assertZeroItemsCollectedAs(0D, c); + assertSingleItemCollectedAs(5D, c, 5D); + } + + public void testAveragingInt() { + Collector<Integer, ?, Double> c = averagingInt(Integer::intValue); + applyItems((4.0 + 8.0) / 2.0, c, 4, 8); + + assertZeroItemsCollectedAs(0D, c); + assertSingleItemCollectedAs(5D, c, 5); + } + + public void testAveragingLong() { + Collector<Long, ?, Double> c = averagingLong(Long::longValue); + applyItems((4.0 + 8.0) / 2.0, c, 4L, 8L); + + assertZeroItemsCollectedAs(0D, c); + assertSingleItemCollectedAs(5D, c, 5L); + } + + public void testCollectingAndThen() { + Collector<Object, ?, List<Object>> listIdentityCollector = collectingAndThen(toList(), Function.identity()); + // same test as toList(): + // same items (allow dups) + applyItems( + Arrays.asList("a", "a"), + listIdentityCollector, + "a", "a" + ); + + // ordered + applyItems( + Arrays.asList("a", "b"), + listIdentityCollector, + "a", "b" + ); + assertZeroItemsCollectedAs(Collections.emptyList(), listIdentityCollector); + assertSingleItemCollectedAs(Collections.singletonList("a"), listIdentityCollector, "a"); + + Collector<Object, ?, Integer> uglyCount = collectingAndThen(toList(), List::size); + // (nearly) same test as counting(): + applyItems(2, uglyCount, "1", new Object()); + + assertZeroItemsCollectedAs(0, uglyCount); + assertSingleItemCollectedAs(1, uglyCount, new Object()); + } + + public void testCounting() { + Collector<Object, ?, Long> c = counting(); + applyItems(2L, c, "1", new Object()); + + assertZeroItemsCollectedAs(0L, c); + assertSingleItemCollectedAs(1L, c, new Object()); + } + + public void testGroupingBy() { + Collector<String, ?, Map<String, List<String>>> c1 = groupingBy(Function.identity()); + + Map<String, List<String>> mapOfLists = new HashMap<>(); + mapOfLists.put("a", Arrays.asList("a", "a")); + applyItems(mapOfLists, c1, "a", "a"); + mapOfLists.clear(); + mapOfLists.put("a", Collections.singletonList("a")); + mapOfLists.put("b", Collections.singletonList("b")); + applyItems(mapOfLists, c1, "a", "b"); + + assertZeroItemsCollectedAs(Collections.emptyMap(), c1); + assertSingleItemCollectedAs(Collections.singletonMap("a", Collections.singletonList("a")), c1, "a"); + + Collector<String, ?, LinkedHashMap<String, Set<String>>> c2 = groupingBy(Function.identity(), LinkedHashMap::new, toSet()); + + LinkedHashMap<String, Set<String>> linkedMapOfSets = new LinkedHashMap<>(); + linkedMapOfSets.put("a", Collections.singleton("a")); + applyItems(linkedMapOfSets, c2, "a", "a"); + linkedMapOfSets.clear(); + linkedMapOfSets.put("a", Collections.singleton("a")); + linkedMapOfSets.put("b", Collections.singleton("b")); + applyItems(linkedMapOfSets, c2, "a", "b"); + + // check to make sure we actually get the linked results, and that they are ordered how we want them + LinkedHashMap<String, Set<String>> out = applyItemsWithoutSplitting(c2, "a", "b"); + assertEquals(Arrays.asList("a", "b"), new ArrayList<>(out.keySet())); + out = applyItemsWithoutSplitting(c2, "b", "a"); + assertEquals(Arrays.asList("b", "a"), new ArrayList<>(out.keySet())); + + assertZeroItemsCollectedAs(new LinkedHashMap<>(), c2); + linkedMapOfSets.clear(); + linkedMapOfSets.put("a", Collections.singleton("a")); + assertSingleItemCollectedAs(linkedMapOfSets, c2, "a"); + } + + public void testJoining() { + Collector<CharSequence, ?, String> c = joining(); + applyItems("ab", c, "a", "b"); + applyItems("a,", c, "a", ","); + applyItems("", c, "", ""); + assertZeroItemsCollectedAs("", c); + assertSingleItemCollectedAs("a", c, "a"); + assertSingleItemCollectedAs("", c, ""); + + c = joining(","); + applyItems("a,b", c, "a", "b"); + applyItems("a,,", c, "a", ","); + applyItems(",", c, "", ""); + assertZeroItemsCollectedAs("", c); + assertSingleItemCollectedAs("a", c, "a"); + assertSingleItemCollectedAs("", c, ""); + + c = joining("-", "{", "}"); + applyItems("{a-b}", c, "a", "b"); + assertZeroItemsCollectedAs("{}", c); + assertSingleItemCollectedAs("{a}", c, "a"); + assertSingleItemCollectedAs("{}", c, ""); + } + + public void testMapping() { + Collector<String, ?, List<String>> identityMapping = mapping(Function.identity(), toList()); + // same test as toList(): + // same items (allow dups) + applyItems( + Arrays.asList("a", "a"), + identityMapping, + "a", "a" + ); + + // ordered + applyItems( + Arrays.asList("a", "b"), + identityMapping, + "a", "b" + ); + assertZeroItemsCollectedAs(Collections.emptyList(), identityMapping); + assertSingleItemCollectedAs(Collections.singletonList("a"), identityMapping, "a"); + + Collector<Integer, ?, List<String>> numberMapping = mapping(s -> "#" + s, toList()); + // poke the same tests as list, make sure the mapper is run + applyItems( + Arrays.asList("#1", "#2"), + numberMapping, + 1, 2 + ); + + // ordered + applyItems( + Arrays.asList("#1", "#2"), + numberMapping, + 1, 2 + ); + assertZeroItemsCollectedAs(Collections.emptyList(), numberMapping); + assertSingleItemCollectedAs(Collections.singletonList("#10"), numberMapping, 10); + } + + public void testMaxBy() { + Collector<String, ?, Optional<String>> c = maxBy(Comparator.naturalOrder()); + applyItems(Optional.of("z"), c, "a", "z"); + applyItems(Optional.of("z"), c, "z", "a"); + + assertZeroItemsCollectedAs(Optional.empty(), c); + assertSingleItemCollectedAs(Optional.of("foo"), c, "foo"); + } + + public void testMinBy() { + Collector<String, ?, Optional<String>> c = minBy(Comparator.naturalOrder()); + applyItems(Optional.of("a"), c, "a", "z"); + applyItems(Optional.of("a"), c, "z", "a"); + + assertZeroItemsCollectedAs(Optional.empty(), c); + assertSingleItemCollectedAs(Optional.of("foo"), c, "foo"); + } + + public void testPartitioningBy() { + Collector<Boolean, ?, Map<Boolean, List<Boolean>>> c1 = partitioningBy(Boolean::valueOf); + + Map<Boolean, List<Boolean>> mapOfLists = new HashMap<>(); + mapOfLists.put(true, Collections.singletonList(true)); + mapOfLists.put(false, Collections.singletonList(false)); + applyItems(mapOfLists, c1, true, false); + mapOfLists.clear(); + mapOfLists.put(true, Arrays.asList(true, true)); + applyItems(mapOfLists, c1, true, true); + mapOfLists.clear(); + mapOfLists.put(false, Arrays.asList(false, false)); + applyItems(mapOfLists, c1, false, false); + + assertZeroItemsCollectedAs(Collections.emptyMap(), c1); + assertSingleItemCollectedAs(Collections.singletonMap(true, Collections.singletonList(true)), c1, true); + assertSingleItemCollectedAs(Collections.singletonMap(false, Collections.singletonList(false)), c1, false); + + Collector<Boolean, ?, Map<Boolean, Set<Boolean>>> c2 = partitioningBy(Boolean::valueOf, toSet()); + + Map<Boolean, Set<Boolean>> mapOfSets = new HashMap<>(); + mapOfSets.put(true, Collections.singleton(true)); + mapOfSets.put(false, Collections.singleton(false)); + applyItems(mapOfSets, c2, true, false); + mapOfSets.clear(); + mapOfSets.put(true, Collections.singleton(true)); + applyItems(mapOfSets, c2, true, true); + mapOfSets.clear(); + mapOfSets.put(false, Collections.singleton(false)); + applyItems(mapOfSets, c2, false, false); + } + + public void testSummarizingDouble() { + Collector<Double, ?, DoubleSummaryStatistics> c = summarizingDouble(Double::doubleValue); + DoubleSummaryStatistics stats = new DoubleSummaryStatistics(); + stats.accept(5.1); + stats.accept(7); + BiPredicate<DoubleSummaryStatistics, DoubleSummaryStatistics> equals = (s1, s2) -> + s1.getSum() == s2.getSum() + && s1.getAverage() == s2.getAverage() + && s1.getCount() == s2.getCount() + && s1.getMin() == s2.getMin() + && s1.getMax() == s2.getMax(); + applyItems(stats, c, 5.1, 7.0, equals); + applyItems(stats, c, 7.0, 5.1, equals);//probably unnecessary to run these backward + + assertZeroItemsCollectedAs(new DoubleSummaryStatistics(), c, equals); + stats = new DoubleSummaryStatistics(); + stats.accept(7.3); + assertSingleItemCollectedAs(stats, c, 7.3, equals); + } + + public void testSummarizingInt() { + Collector<Integer, ?, IntSummaryStatistics> c = summarizingInt(Integer::intValue); + IntSummaryStatistics stats = new IntSummaryStatistics(); + stats.accept(2); + stats.accept(10); + BiPredicate<IntSummaryStatistics, IntSummaryStatistics> equals = (s1, s2) -> + s1.getSum() == s2.getSum() + && s1.getAverage() == s2.getAverage() + && s1.getCount() == s2.getCount() + && s1.getMin() == s2.getMin() + && s1.getMax() == s2.getMax(); + applyItems(stats, c, 2, 10, equals); + applyItems(stats, c, 10, 2, equals); + + assertZeroItemsCollectedAs(new IntSummaryStatistics(), c, equals); + stats = new IntSummaryStatistics(); + stats.accept(7); + assertSingleItemCollectedAs(stats, c, 7, equals); + } + + public void testSummarizingLong() { + Collector<Long, ?, LongSummaryStatistics> c = summarizingLong(Long::longValue); + LongSummaryStatistics stats = new LongSummaryStatistics(); + stats.accept(2); + stats.accept(10); + BiPredicate<LongSummaryStatistics, LongSummaryStatistics> equals = (s1, s2) -> + s1.getSum() == s2.getSum() + && s1.getAverage() == s2.getAverage() + && s1.getCount() == s2.getCount() + && s1.getMin() == s2.getMin() + && s1.getMax() == s2.getMax(); + applyItems(stats, c, 2L, 10L, equals); + applyItems(stats, c, 10L, 2L, equals); + + assertZeroItemsCollectedAs(new LongSummaryStatistics(), c, equals); + stats = new LongSummaryStatistics(); + stats.accept(7L); + assertSingleItemCollectedAs(stats, c, 7L, equals); + } + + public void testSummingDouble() { + Collector<Double, ?, Double> c = summingDouble(Double::doubleValue); + applyItems(4.1 + 8.2, c, 4.1, 8.2); + + assertZeroItemsCollectedAs(0d, c); + assertSingleItemCollectedAs(7.3, c, 7.3); + } + + public void testSummingInt() { + Collector<Integer, ?, Integer> c = summingInt(Integer::intValue); + applyItems(4 + 8, c, 4, 8); + + assertZeroItemsCollectedAs(0, c); + assertSingleItemCollectedAs(7, c, 7); + } + + public void testSummingLong() { + Collector<Long, ?, Long> c = summingLong(Long::longValue); + applyItems(4L + 8L, c, 4L, 8L); + + assertZeroItemsCollectedAs(0L, c); + assertSingleItemCollectedAs(5L, c, 5L); + } + + public void testList() { + Collector<String, ?, List<String>> c = toList(); + + // same items (allow dups) + applyItems( + Arrays.asList("a", "a"), + c, + "a", "a" + ); + + // ordered + applyItems( + Arrays.asList("a", "b"), + c, + "a", "b" + ); + assertZeroItemsCollectedAs(Collections.emptyList(), c); + assertSingleItemCollectedAs(Collections.singletonList("a"), c, "a"); + } + + public void testMap() { + Collector<String, ?, Map<String, String>> c = toMap(Function.identity(), Function.identity()); + + // two distinct items + Map<String, String> map = new HashMap<>(); + map.put("a", "a"); + map.put("b", "b"); + applyItems(map, c, "a", "b"); + + // inline applyItems and test each to confirm failure for duplicates + try { + applyItemsWithoutSplitting(c, "a", "a"); + fail("expected IllegalStateException"); + } catch (IllegalStateException expected) { + } + try { + applyItemsWithSplitting(c, "a", "a"); + fail("expected IllegalStateException"); + } catch (IllegalStateException expected) { + } + + assertZeroItemsCollectedAs(Collections.emptyMap(), c); + assertSingleItemCollectedAs(Collections.singletonMap("a", "a"), c, "a"); + + List<String> seen = new ArrayList<>(); + c = toMap(Function.identity(), Function.identity(), (s, s2) -> { + seen.add("first: " + s); + seen.add("second: " + s2); + return s + "," + s2; + }); + map = new HashMap<>(); + map.put("a", "a,a"); + applyItems(map, c, "a", "a"); + assertEquals(Arrays.asList("first: a", "second: a", "first: a", "second: a"), seen); + } + + public void testSet() { + Collector<String, ?, Set<String>> c = toSet(); + + // same items (no dups) + applyItems( + Collections.singleton("a"), + c, + "a", "a" + ); + + // different items + applyItems( + new HashSet<>(Arrays.asList("a", "b")), + c, + "a", "b" + ); + + assertZeroItemsCollectedAs(Collections.emptySet(), c); + assertSingleItemCollectedAs(Collections.singleton("a"), c, "a"); + } + + /** + * This method attempts to apply a collector to items as a stream might do, so that we + * can simply verify the output. Taken from the Collector class's javadoc. + */ + private static <T, A, R> void applyItems(R expected, Collector<T, A, R> collector, T t1, T t2, BiPredicate<R, R> equals) { + assertTrue("failed without splitting", equals.test(expected, applyItemsWithoutSplitting(collector, t1, t2))); + assertTrue("failed with splitting", equals.test(expected, applyItemsWithSplitting(collector, t1, t2))); + } + + /** + * This method attempts to apply a collector to items as a stream might do, so that we + * can simply verify the output. Taken from the Collector class's javadoc. + */ + private static <T, A, R> void applyItems(R expected, Collector<T, A, R> collector, T t1, T t2) { + applyItems(expected, collector, t1, t2, Object::equals); + } + + /** + * Helper for applyItems. + */ + private static <T, A, R> R applyItemsWithoutSplitting(Collector<T, A, R> collector, T t1, T t2) { + Supplier<A> supplier = collector.supplier(); + BiConsumer<A, T> accumulator = collector.accumulator(); + // unused in this impl + BinaryOperator<A> combiner = collector.combiner(); + Function<A, R> finisher = collector.finisher(); + + A a1 = supplier.get(); + + accumulator.accept(a1, t1); + accumulator.accept(a1, t2); + + // result without splitting + R r1 = finisher.apply(a1); + return r1; + } + + /** + * Helper for applyItems. + */ + private static <T, A, R> R applyItemsWithSplitting(Collector<T, A, R> collector, T t1, T t2) { + Supplier<A> supplier = collector.supplier(); + BiConsumer<A, T> accumulator = collector.accumulator(); + // actually used in this impl + BinaryOperator<A> combiner = collector.combiner(); + Function<A, R> finisher = collector.finisher(); + + A a2 = supplier.get(); + accumulator.accept(a2, t1); + A a3 = supplier.get(); + accumulator.accept(a3, t2); + + // result with splitting + R r2 = finisher.apply(combiner.apply(a2, a3)); + return r2; + } + + private static <T, A, R> void assertZeroItemsCollectedAs(R expected, Collector<T, A, R> collector) { + assertZeroItemsCollectedAs(expected, collector, Object::equals); + } + + private static <T, A, R> void assertZeroItemsCollectedAs(R expected, Collector<T, A, R> collector, BiPredicate<R, R> equals) { + Supplier<A> supplier = collector.supplier(); + // unused in this impl + BiConsumer<A, T> accumulator = collector.accumulator(); + // shouldn't really be used, just handy to poke the internals quick + BinaryOperator<A> combiner = collector.combiner(); + Function<A, R> finisher = collector.finisher(); + + R actual = finisher.apply(supplier.get()); + assertTrue(equals, expected, actual); + // doesn't actually ever happen, just internal checks + actual = finisher.apply(combiner.apply(supplier.get(), supplier.get())); + assertTrue(equals, expected, actual); + } + + private static <T, A, R> void assertSingleItemCollectedAs(R expected, Collector<T, A, R> collector, T item) { + assertSingleItemCollectedAs(expected, collector, item, Object::equals); + } + + private static <T, A, R> void assertSingleItemCollectedAs(R expected, Collector<T, A, R> collector, T item, BiPredicate<R, R> equals) { + Supplier<A> supplier = collector.supplier(); + BiConsumer<A, T> accumulator = collector.accumulator(); + // shouldn't really be used, just handy to poke the internals quick + BinaryOperator<A> combiner = collector.combiner(); + Function<A, R> finisher = collector.finisher(); + + A a1 = supplier.get(); + + accumulator.accept(a1, item); + + R actual = finisher.apply(a1); + // normal test + assertTrue(equals, expected, actual); + // these shouldn't really be used, just handy to poke the internals quick + actual = finisher.apply(combiner.apply(a1, supplier.get())); + assertTrue(equals, expected, actual); + actual = finisher.apply(combiner.apply(supplier.get(), a1)); + assertTrue(equals, expected, actual); + } + + private static <T, U> void assertTrue(BiPredicate<T, U> predicate, T expected, U actual) { + assertTrue("expected= " + expected + ", actual=" + actual, predicate.test(expected, actual)); + } + +}