diff --git a/src/main/java/one/util/streamex/MoreCollectors.java b/src/main/java/one/util/streamex/MoreCollectors.java index 3d4746e1..7a05311d 100644 --- a/src/main/java/one/util/streamex/MoreCollectors.java +++ b/src/main/java/one/util/streamex/MoreCollectors.java @@ -168,6 +168,184 @@ private MoreCollectors() { }, Function.identity(), set -> set.size() == size, UNORDERED_ID_CHARACTERISTICS); } + /** + * Returns a {@code Collector} that accumulates elements into a {@code Map} + * whose keys and values are taken from {@code Map.Entry}. + * + * @param the {@link Comparable} type of then map keys + * @param the type of the map values + * + * @return {@code Collector} which collects elements into a {@code Map} + * whose keys and values are taken from {@code Map.Entry} + * @throws IllegalStateException if this stream contains duplicate keys + * (according to {@link Object#equals(Object)}). + * + * @see #entriesToMap(BinaryOperator) + * @see #entriesToMap(Function, BinaryOperator) + * @see Collectors#toMap(Function, Function) + * @since 0.7.3 + */ + public static Collector, ?, Map> entriesToMap() { + return Collectors.toMap(Entry::getKey, Entry::getValue); + } + + /** + * Returns a {@code Collector} that accumulates elements into a {@code Map} + * whose keys and values are taken from {@code Map.Entry} and combining them + * using the provided {@code combiner} function to the input elements. + * + *

If the mapped keys contains duplicates (according to {@link Object#equals(Object)}), + * the value mapping function is applied to each equal element, and the + * results are merged using the provided {@code combiner} function. + * + * @param the {@link Comparable} type of then map keys + * @param the type of the map values + * @param combiner a merge function, used to resolve collisions between + * values associated with the same key, as supplied + * to {@link Map#merge(Object, Object, BiFunction)} + * @return {@code Collector} which collects elements into a {@code Map} + * whose keys and values are taken from {@code Map.Entry} and combining them + * using the {@code combiner} function + * + * @see #entriesToMap() + * @see #entriesToMap(Function, BinaryOperator) + * @see Collectors#toMap(Function, Function, BinaryOperator) + * @since 0.7.3 + */ + public static Collector, ?, Map> entriesToMap( + BinaryOperator combiner) { + return Collectors.toMap(Entry::getKey, Entry::getValue, combiner); + } + + /** + * Returns a {@code Collector} that accumulates input elements into a {@code Map} + * whose keys are taken from {@code Map.Entry} and values are the result + * of applying the provided {@code valueMapper} function to {@code Map.Entry} values + * and combining them using the provided {@code combiner} function. + * + * @param the {@link Comparable} type of then map keys + * @param the type of the map values + * @param the output type of the value mapping function + * @param valueMapper a mapping function to produce values + * @param combiner a merge function, used to resolve collisions between + * values associated with the same key, as supplied + * to {@link Map#merge(Object, Object, BiFunction)} + * @return {@code Collector} which collects elements into a {@code Map} + * whose keys are taken from {@code Map.Entry} and values are the result + * of applying the provided {@code valueMapper} function and combining + * them using the provided {@code combiner} function. + * @throws NullPointerException if mapper is null. + * + * @see #entriesToMap() + * @see #entriesToMap(BinaryOperator) + * @see Collectors#toMap(Function, Function, BinaryOperator) + * @since 0.7.3 + */ + public static Collector, ?, Map> entriesToMap( + Function valueMapper, BinaryOperator combiner) { + Objects.requireNonNull(valueMapper); + return Collectors.toMap(Entry::getKey, entry -> valueMapper.apply(entry.getValue()), combiner); + } + + /** + * Returns a {@code Collector} that accumulates elements into + * a result {@code Map} defined by {@code mapSupplier} function + * whose keys and values are taken from {@code Map.Entry}. + * + * @param the {@link Comparable} type of then map keys + * @param the type of the map values + * @param the type of the resulting {@code Map} + * @return {@code Collector} which collects elements into a {@code Map} + * defined by {@code mapSupplier} function + * whose keys and values are taken from {@code Map.Entry} + * @throws IllegalStateException if this stream contains duplicate keys + * (according to {@link Object#equals(Object)}). + * + * @see #entriesToCustomMap(BinaryOperator, Supplier) + * @see #entriesToCustomMap(Function, BinaryOperator, Supplier) + * @see Collectors#toMap(Function, Function, BinaryOperator, Supplier) + * @since 0.7.3 + */ + public static > Collector, ?, M> entriesToCustomMap( + Supplier mapSupplier) { + return Collectors.toMap(Entry::getKey, Entry::getValue, throwingMerger(), mapSupplier); + } + + /** + * Returns a {@code Collector} that accumulates elements into + * a result {@code Map} defined by {@code mapSupplier} function + * whose keys and values are taken from {@code Map.Entry} and combining them + * using the provided {@code combiner} function to the input elements. + * + *

If the mapped keys contains duplicates (according to {@link Object#equals(Object)}), + * the value mapping function is applied to each equal element, and the + * results are merged using the provided {@code combiner} function. + * + * @param the {@link Comparable} type of then map keys + * @param the type of the map values + * @param the type of the resulting {@code Map} + * @param combiner a merge function, used to resolve collisions between + * values associated with the same key, as supplied + * to {@link Map#merge(Object, Object, BiFunction)} + * @param mapSupplier a function which returns a new, empty {@code Map} into + * which the results will be inserted + * @return {@code Collector} which collects elements into a {@code Map} + * whose keys and values are taken from {@code Map.Entry} and combining them + * using the {@code combiner} function + * + * @see #entriesToCustomMap(Supplier) + * @see #entriesToCustomMap(Function, BinaryOperator, Supplier) + * @see Collectors#toMap(Function, Function, BinaryOperator, Supplier) + * @since 0.7.3 + */ + public static > Collector, ?, M> entriesToCustomMap( + BinaryOperator combiner, Supplier mapSupplier) { + return Collectors.toMap(Entry::getKey, Entry::getValue, combiner, mapSupplier); + } + + /** + * Returns a {@code Collector} that accumulates elements into + * a result {@code Map} defined by {@code mapSupplier} function + * whose keys are taken from {@code Map.Entry} and values are the result + * of applying the provided {@code valueMapper} function to {@code Map.Entry} values + * and combining them using the provided {@code combiner} function. + * + *

If the mapped keys contains duplicates (according to {@link Object#equals(Object)}), + * the value mapping function is applied to each equal element, and the + * results are merged using the provided {@code combiner} function. + * + * @param the {@link Comparable} type of then map keys + * @param the type of the map values + * @param the output type of the value mapping function + * @param the type of the resulting {@code Map} + * @param valueMapper a mapping function to produce values + * @param combiner a merge function, used to resolve collisions between + * values associated with the same key, as supplied + * to {@link Map#merge(Object, Object, BiFunction)} + * @param mapSupplier a function which returns a new, empty {@code Map} into + * which the results will be inserted + * @return {@code Collector} which collects elements into a {@code Map} + * whose keys and values are taken from {@code Map.Entry} and combining them + * using the {@code combiner} function + * @throws NullPointerException if mapper is null. + * + * @see #entriesToCustomMap(Supplier) + * @see #entriesToCustomMap(BinaryOperator, Supplier) + * @see Collectors#toMap(Function, Function, BinaryOperator, Supplier) + * @since 0.7.3 + */ + public static > Collector, ?, M> entriesToCustomMap( + Function valueMapper, BinaryOperator combiner, Supplier mapSupplier) { + Objects.requireNonNull(valueMapper); + return Collectors.toMap(Entry::getKey, entry -> valueMapper.apply(entry.getValue()), combiner, mapSupplier); + } + + private static BinaryOperator throwingMerger() { + return (firstKey, secondKey) -> { + throw new IllegalStateException("Duplicate entry keys are not allowed (attempt to merge key '" + firstKey + "'). "); + }; + } + /** * Returns a {@code Collector} which counts a number of distinct values the * mapper function returns for the stream elements. diff --git a/src/test/java/one/util/streamex/MoreCollectorsTest.java b/src/test/java/one/util/streamex/MoreCollectorsTest.java index bc12dc00..9fda63c6 100644 --- a/src/test/java/one/util/streamex/MoreCollectorsTest.java +++ b/src/test/java/one/util/streamex/MoreCollectorsTest.java @@ -25,16 +25,19 @@ import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.NavigableMap; import java.util.Optional; import java.util.OptionalInt; import java.util.OptionalLong; import java.util.TreeMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; @@ -591,6 +594,237 @@ public void testToEnumSet() { checkCollectorEmpty("Empty", EnumSet.noneOf(TimeUnit.class), MoreCollectors.toEnumSet(TimeUnit.class)); } + /** + * See {@link MoreCollectors#entriesToMap()}. + */ + @Test + public void testEntriesToMap() { + assertThrows(IllegalStateException.class, () -> + EntryStream.generate(() -> "a", () -> 1).limit(10).collect(MoreCollectors.entriesToMap())); + + checkCollectorEmpty("entriesToMap", Collections.emptyMap(), MoreCollectors.entriesToMap()); + { + Map expected = Collections.emptyMap().entrySet().stream() + .collect(MoreCollectors.entriesToMap()); + assertTrue(expected.isEmpty()); + } + + { + Map expected = EntryStream.of(1, "*", 2, "**", 3, "***", 4, "****", 5, "*****").toMap(); + Supplier>> stream = expected.entrySet()::stream; + checkCollector("entriesToMap", expected, stream, MoreCollectors.entriesToMap()); + + streamEx(stream, supplier -> { + Map result = supplier.get().collect(MoreCollectors.entriesToMap()); + assertEquals("*", result.get(1)); + assertEquals("**", result.get(2)); + assertEquals("***", result.get(3)); + assertEquals("****", result.get(4)); + assertEquals("*****", result.get(5)); + }); + } + } + + /** + * See {@link MoreCollectors#entriesToMap(BinaryOperator)}. + */ + @Test + public void testEntriesToMapWithCombiner() { + BinaryOperator nullCombiner = null; + assertThrows(NullPointerException.class, () -> EntryStream.of(1, "*").stream().collect(MoreCollectors.entriesToMap(nullCombiner))); + + checkCollectorEmpty("entriesToMap", Collections.emptyMap(), MoreCollectors.entriesToMap(String::concat)); + { + Map expected = Collections.emptyMap().entrySet().stream() + .collect(MoreCollectors.entriesToMap((a, b) -> b)); + assertTrue(expected.isEmpty()); + } + + streamEx(() -> Stream.of( + EntryStream.of(1, "*"), + EntryStream.of(1, "*", 2, "*"), + EntryStream.of(1, "*", 2, "*", 3, "*"), + EntryStream.of(1, "*", 2, "*", 3, "*", 4, "*"), + EntryStream.of(1, "*", 2, "*", 3, "*", 4, "*", 5, "*")) + .flatMap(Function.identity()), supplier -> { + Map result = supplier.get().collect(MoreCollectors.entriesToMap(String::concat)); + assertEquals("*****", result.get(1)); + assertEquals("****", result.get(2)); + assertEquals("***", result.get(3)); + assertEquals("**", result.get(4)); + assertEquals("*", result.get(5)); + }); + + streamEx(() -> Stream.of( + EntryStream.of(1, "one", 2, "two", 3, "three", 4, "four"), + EntryStream.of(1, "ein", 2, "zwei", 3, "drei"), + EntryStream.of(1, "une", 2, "deux")) + .flatMap(Function.identity()), supplier -> { + Map result = supplier.get().collect(MoreCollectors.entriesToMap((left, right) -> left + "," + right)); + assertEquals("one,ein,une", result.get(1)); + assertEquals("two,zwei,deux", result.get(2)); + assertEquals("three,drei", result.get(3)); + assertEquals("four", result.get(4)); + }); + } + + /** + * See {@link MoreCollectors#entriesToMap(Function, BinaryOperator)}. + */ + @Test + public void testEntriesToMapWithValueMapperAndCombiner() { + assertThrows(NullPointerException.class, () -> MoreCollectors.entriesToMap(null, null)); + assertThrows(NullPointerException.class, () -> MoreCollectors.entriesToMap(null, (a, b) -> a)); + BinaryOperator nullCombiner = null; + assertThrows(NullPointerException.class, () -> EntryStream.of(1, "*").stream().collect( + MoreCollectors.entriesToMap(Function.identity(), nullCombiner))); + + streamEx(() -> Stream.of( + EntryStream.of(1, "one", 2, "two", 3, "three", 4, "four"), + EntryStream.of(1, "ein", 2, "zwei", 3, "drei"), + EntryStream.of(1, "une", 2, "deux")) + .flatMap(Function.identity()), supplier -> { + Map result = supplier.get().collect( + MoreCollectors.entriesToMap((value) -> "['" + value + "']", (left, right) -> left + ", " + right)); + assertEquals("['one'], ['ein'], ['une']", result.get(1)); + assertEquals("['two'], ['zwei'], ['deux']", result.get(2)); + assertEquals("['three'], ['drei']", result.get(3)); + assertEquals("['four']", result.get(4)); + }); + } + + /** + * See {@link MoreCollectors#entriesToCustomMap(Supplier)}. + */ + @Test + public void testEntriesToCustomMap() { + assertThrows(IllegalStateException.class, () -> + EntryStream.generate(() -> "a", () -> 1).limit(10).collect(MoreCollectors.entriesToCustomMap(LinkedHashMap::new))); + + checkCollectorEmpty("entriesToMap", Collections.emptyMap(), + MoreCollectors.entriesToCustomMap(HashMap::new)); + checkCollectorEmpty("entriesToMap", Collections.emptyMap(), + MoreCollectors.entriesToCustomMap(IdentityHashMap::new)); + checkCollectorEmpty("entriesToMap", Collections.emptyNavigableMap(), + MoreCollectors.entriesToCustomMap(TreeMap::new)); + checkCollectorEmpty("entriesToMap", Collections.emptySortedMap(), + MoreCollectors.entriesToCustomMap(TreeMap::new)); + checkCollectorEmpty("entriesToMap", Collections.emptySortedMap(), + MoreCollectors.entriesToCustomMap(LinkedHashMap::new)); + + { + Map expected = Collections.emptyMap().entrySet().stream() + .collect(MoreCollectors.entriesToCustomMap(TreeMap::new)); + assertTrue(expected.isEmpty()); + } + + { + Map expected = EntryStream.of(5, "*****", 1, "*", 4, "****", 2, "**", 3, "***").toMap(); + Supplier>> stream = expected.entrySet()::stream; + checkCollector("entriesToMap", expected, stream, MoreCollectors.entriesToCustomMap(TreeMap::new)); + + streamEx(stream, supplier -> { + NavigableMap result = supplier.get().collect(MoreCollectors.entriesToCustomMap(TreeMap::new)); + assertEquals("*", result.get(1)); + assertEquals("**", result.get(2)); + assertEquals("***", result.get(3)); + assertEquals("****", result.get(4)); + assertEquals("*****", result.get(5)); + + assertEquals(1, (int) result.firstKey()); + assertEquals("*", result.firstEntry().getValue()); + assertEquals(5, (int) result.lastKey()); + assertEquals("*****", result.lastEntry().getValue()); + }); + } + + { + LinkedHashMap expected = EntryStream.of(5, "*****", 1, "*", 4, "****", 2, "**", 3, "***") + .collect(MoreCollectors.entriesToCustomMap(LinkedHashMap::new)); + assertEquals("5=*****", expected.entrySet().iterator().next().toString()); + } + } + + /** + * See {@link MoreCollectors#entriesToCustomMap(BinaryOperator, Supplier)}. + */ + @Test + public void testEntriesToCustomMapWithCombiner() { + BinaryOperator nullCombiner = null; + assertThrows(NullPointerException.class, () -> EntryStream.of(1, "*").stream().collect(MoreCollectors.entriesToCustomMap(nullCombiner, TreeMap::new))); + + checkCollectorEmpty("entriesToMap", Collections.emptyMap(), MoreCollectors.entriesToCustomMap(String::concat, TreeMap::new)); + { + Map expected = Collections.emptyMap().entrySet().stream() + .collect(MoreCollectors.entriesToCustomMap((a, b) -> b, TreeMap::new)); + assertTrue(expected.isEmpty()); + } + + streamEx(() -> Stream.of( + EntryStream.of(100, "hundred", 2, "two", 3, "three", 4, "four"), + EntryStream.of(100, "hundert", 2, "zwei", 3, "drei"), + EntryStream.of(100, "cent", 2, "deux")) + .flatMap(Function.identity()), supplier -> { + NavigableMap result = supplier.get().collect( + MoreCollectors.entriesToCustomMap((left, right) -> left + " -> " + right, TreeMap::new)); + assertEquals("hundred -> hundert -> cent", result.get(100)); + assertEquals("two -> zwei -> deux", result.get(2)); + assertEquals("three -> drei", result.get(3)); + assertEquals("four", result.get(4)); + + assertEquals(2, (int) result.firstKey()); + assertEquals("two -> zwei -> deux", result.firstEntry().getValue()); + assertEquals(100, (int) result.lastKey()); + assertEquals("hundred -> hundert -> cent", result.lastEntry().getValue()); + }); + + streamEx(() -> Stream.of( + EntryStream.of(10, "*"), + EntryStream.of(10, "*", 2, "*"), + EntryStream.of(10, "*", 2, "*", 100, "*"), + EntryStream.of(10, "*", 2, "*", 100, "*", 4, "*"), + EntryStream.of(10, "*", 2, "*", 100, "*", 4, "*", 5, "*")) + .flatMap(Function.identity()), supplier -> { + + NavigableMap result = supplier.get().collect( + MoreCollectors.entriesToCustomMap((left, right) -> left + " -> " + right, TreeMap::new)); + assertEquals("2=* -> * -> * -> *", result.entrySet().iterator().next().toString()); + }); + } + + /** + * See {@link MoreCollectors#entriesToCustomMap(Function, BinaryOperator, Supplier)}. + */ + @Test + public void testEntriesToCustomMapWithValueMapperAndCombiner() { + assertThrows(NullPointerException.class, () -> MoreCollectors.entriesToCustomMap(null, null, null)); + assertThrows(NullPointerException.class, () -> MoreCollectors.entriesToCustomMap(null, (a, b) -> a, null)); + assertThrows(NullPointerException.class, () -> MoreCollectors.entriesToCustomMap(null, null, TreeMap::new)); + assertThrows(NullPointerException.class, () -> MoreCollectors.entriesToCustomMap(null, (a, b) -> a, TreeMap::new)); + assertThrows(NullPointerException.class, () -> MoreCollectors.entriesToCustomMap(null, (a, b) -> a, TreeMap::new)); + BinaryOperator nullCombiner = null; + assertThrows(NullPointerException.class, () -> EntryStream.of(1, "*").stream().collect( + MoreCollectors.entriesToCustomMap(Function.identity(), nullCombiner, TreeMap::new))); + + streamEx(() -> Stream.of( + EntryStream.of(100, "hundred", 2, "two", 3, "three", 4, "four"), + EntryStream.of(100, "hundert", 2, "zwei", 3, "drei"), + EntryStream.of(100, "cent", 2, "deux")) + .flatMap(Function.identity()), supplier -> { + NavigableMap result = supplier.get().collect( + MoreCollectors.entriesToCustomMap((value) -> "['" + value + "']", (left, right) -> left + ", " + right, TreeMap::new)); + assertEquals("['hundred'], ['hundert'], ['cent']", result.get(100)); + assertEquals("['two'], ['zwei'], ['deux']", result.get(2)); + assertEquals("['three'], ['drei']", result.get(3)); + assertEquals("['four']", result.get(4)); + + assertEquals(2, (int) result.firstKey()); + assertEquals("['two'], ['zwei'], ['deux']", result.firstEntry().getValue()); + assertEquals(100, (int) result.lastKey()); + assertEquals("['hundred'], ['hundert'], ['cent']", result.lastEntry().getValue()); + }); + } + @Test public void testFlatMapping() { assertThrows(NullPointerException.class, () -> MoreCollectors.flatMapping(null)); diff --git a/wiki/CHANGES.md b/wiki/CHANGES.md index 573a4962..b7196061 100644 --- a/wiki/CHANGES.md +++ b/wiki/CHANGES.md @@ -4,6 +4,7 @@ Check also [MIGRATION.md](MIGRATION.md) for possible compatibility problems. ### 0.7.3 * [#028]: Added: `StreamEx.toCollectionAndThen` +* [#043]: Added: Add `MoreCollectors.entriesToMap` and `MoreCollectors.entriesToCustomMap` methods accepting Entry. * [#219]: Changed: MoreCollectors now reject eagerly null parameters where possible; `MoreCollectors.last` throws NPE if last stream element is null. * [#221]: Fixed: `rangeClosed(x, x, step)` returned empty stream instead of stream of `x` if step absolute value is bigger than one. diff --git a/wiki/CHEATSHEET.md b/wiki/CHEATSHEET.md index cc031895..6a1243ef 100644 --- a/wiki/CHEATSHEET.md +++ b/wiki/CHEATSHEET.md @@ -5,25 +5,25 @@ * [Glossary](#glossary) * [Stream sources](#stream-sources) * [New intermediate operations](#new-intermediate-operations) - * [filtering](#filtering) - * [mapping](#mapping) - * [flat-mapping](#flat-mapping) - * [distinct](#distinct) - * [sorting](#sorting) - * [partial reduction](#partial-reduction) - * [concatenate](#concatenate) - * [peek](#peek) - * [misc](#misc-intermediate-operations) + * [filtering](#filtering) + * [mapping](#mapping) + * [flat-mapping](#flat-mapping) + * [distinct](#distinct) + * [sorting](#sorting) + * [partial reduction](#partial-reduction) + * [concatenate](#concatenate) + * [peek](#peek) + * [misc](#misc-intermediate-operations) * [New terminal operations](#new-terminal-operations) - * [Collector shortcuts](#collector-shortcuts) - * [Search](#search) - * [Folding](#folding) - * [Primitive operations](#primitive-operations) - * [forEach-like operations](#foreach-like-operations) - * [misc](#misc-terminal-operations) + * [Collector shortcuts](#collector-shortcuts) + * [Search](#search) + * [Folding](#folding) + * [Primitive operations](#primitive-operations) + * [forEach-like operations](#foreach-like-operations) + * [misc](#misc-terminal-operations) * [Collectors](#collectors) - * [Basic collectors](#basic-collectors) - * [Adaptor collectors](#adaptor-collectors) + * [Basic collectors](#basic-collectors) + * [Adaptor collectors](#adaptor-collectors) ## Glossary @@ -259,7 +259,8 @@ What I want | How to get it --- | --- Collect to array | `MoreCollectors.toArray()` Collect to boolean array using the `Predicate` applied to each element | `MoreCollectors.toBooleanArray()` -Collect to `EnumSet` | `MoreCollectors.toEnumSet()` +Collect `Map.Entry` entries to `HashMap` | `MoreCollectors.entriesToMap()` +Collect `Map.Entry` entries to custom `Map` implementation | `MoreCollectors.entriesToCustomMap()` Count number of distinct elements using custom key extractor | `MoreCollectors.distinctCount()` Get the `List` of distinct elements using custom key extractor | `MoreCollectors.distinctBy()` Simply counting, but get the result as `Integer` | `MoreCollectors.countingInt()` @@ -287,4 +288,4 @@ Filter the input before passing to the collector | `MoreCollectors.filtering()` Map the input before passing to the collector | `MoreCollectors.mapping()` Flat-map the input before passing to the collector | `MoreCollectors.flatMapping()` Perform a custom final operation after the collection finishes | `MoreCollectors.collectingAndThen()` -Perform a downstream collection if all elements satisfy the predicate | `MoreCollectors.ifAllMatch()` \ No newline at end of file +Perform a downstream collection if all elements satisfy the predicate | `MoreCollectors.ifAllMatch()`