From 490983ee3d8ba0e536eb13aef567453096e41a67 Mon Sep 17 00:00:00 2001 From: Grzegorz Piwowarek Date: Mon, 16 Dec 2024 19:12:58 +0100 Subject: [PATCH] Introduce dedicated GroupingByGatherer --- README.md | 2 + .../gatherers/GroupingByGatherer.java | 40 +++++++++++ .../pivovarit/gatherers/MoreGatherers.java | 30 ++++++++ .../blackbox/GroupingByGathererTest.java | 70 +++++++++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 src/main/java/com/pivovarit/gatherers/GroupingByGatherer.java create mode 100644 src/test/java/com/pivovarit/gatherers/blackbox/GroupingByGathererTest.java diff --git a/README.md b/README.md index 2e89f1c..51022a1 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ Provided `Gatherers`: - creates a sliding window of a fixed size with a fixed step, extends `Gatherers.windowSliding(int)` by adding a step parameter - `MoreGatherers.filteringByIndex(BiPredicate)` - filters elements based on their index and value +- `MoreGatherers.groupingBy(Function, Collector)` + - groups elements by a key extractor function and applies a custom collector ### Philosophy diff --git a/src/main/java/com/pivovarit/gatherers/GroupingByGatherer.java b/src/main/java/com/pivovarit/gatherers/GroupingByGatherer.java new file mode 100644 index 0000000..eb224e4 --- /dev/null +++ b/src/main/java/com/pivovarit/gatherers/GroupingByGatherer.java @@ -0,0 +1,40 @@ +package com.pivovarit.gatherers; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Gatherer; +import java.util.stream.Stream; + +record GroupingByGatherer(Function classifier, + Collector collector) + implements Gatherer>, Map.Entry> { + + GroupingByGatherer { + Objects.requireNonNull(classifier, "classifier can't be null"); + Objects.requireNonNull(collector, "collector can't be null"); + } + + @Override + public Supplier>> initializer() { + return HashMap::new; + } + + @Override + public Integrator>, T, Map.Entry> integrator() { + return Integrator.ofGreedy((state, element, _) -> { + state.computeIfAbsent(classifier.apply(element), _ -> Stream.builder()).accept(element); + return true; + }); + } + + @Override + public BiConsumer>, Downstream>> finisher() { + return (map, downstream) -> map.forEach((key, builder) -> downstream.push(Map.entry(key, builder.build() + .collect(collector)))); + } +} diff --git a/src/main/java/com/pivovarit/gatherers/MoreGatherers.java b/src/main/java/com/pivovarit/gatherers/MoreGatherers.java index 2fa69c0..94d8ef0 100644 --- a/src/main/java/com/pivovarit/gatherers/MoreGatherers.java +++ b/src/main/java/com/pivovarit/gatherers/MoreGatherers.java @@ -7,6 +7,8 @@ import java.util.function.BiFunction; import java.util.function.BiPredicate; import java.util.function.Function; +import java.util.stream.Collector; +import java.util.stream.Collectors; import java.util.stream.Gatherer; import java.util.stream.Stream; @@ -263,4 +265,32 @@ private MoreGatherers() { public static Gatherer filteringByIndex(BiPredicate predicate) { return new FilterByIndexGatherer<>(predicate); } + + /** + * Creates a {@link Gatherer} that groups elements based on a key extracted by the given {@code classifier}. + * + * @param classifier the function used to extract the key for grouping elements + * @param collector the {@link Collector} used to accumulate the elements of each group + * @param the type of the input elements + * @param the type of the key extracted from the input elements + * @param the type of the result of the collector + * + * @return a {@link Gatherer} that groups elements based on the extracted key + */ + public static Gatherer> groupingBy(Function classifier, Collector collector) { + return new GroupingByGatherer<>(classifier, collector); + } + + /** + * Creates a {@link Gatherer} that groups elements based on a key extracted by the given {@code classifier}. + * + * @param classifier the function used to extract the key for grouping elements + * @param the type of the input elements + * @param the type of the key extracted from the input elements + * + * @return a {@link Gatherer} that groups elements based on the extracted key + */ + public static Gatherer>> groupingBy(Function classifier) { + return groupingBy(classifier, Collectors.toList()); + } } diff --git a/src/test/java/com/pivovarit/gatherers/blackbox/GroupingByGathererTest.java b/src/test/java/com/pivovarit/gatherers/blackbox/GroupingByGathererTest.java new file mode 100644 index 0000000..0aa5bc5 --- /dev/null +++ b/src/test/java/com/pivovarit/gatherers/blackbox/GroupingByGathererTest.java @@ -0,0 +1,70 @@ +package com.pivovarit.gatherers.blackbox; + +import com.pivovarit.gatherers.MoreGatherers; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GroupingByGathererTest { + + @Test + void shouldRejectNullClassifier() { + assertThatThrownBy(() -> MoreGatherers.groupingBy(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("classifier"); + } + + @Test + void shouldRejectNullCollector() { + assertThatThrownBy(() -> MoreGatherers.groupingBy(i -> i, null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("collector"); + } + + @Test + void shouldGroupEmpty() { + assertThat(List.of().stream().gather(MoreGatherers.groupingBy(i -> i))).isEmpty(); + } + + @Test + void shouldGroupEmptyWithCustomCollector() { + assertThat(List.of().stream().gather(MoreGatherers.groupingBy(i -> i, Collectors.toSet()))).isEmpty(); + } + + @Test + void shouldGroupToList() { + List>> results = Stream.of("a", "bb", "cc", "ddd", "ee", "fff") + .gather(MoreGatherers.groupingBy(String::length)) + .toList(); + + assertThat(results) + .hasSize(3) + .containsExactlyInAnyOrder( + Map.entry(1, List.of("a")), + Map.entry(2, List.of("bb", "cc", "ee")), + Map.entry(3, List.of("ddd", "fff")) + ); + } + + @Test + void shouldGroupUsingCustomCollector() { + List>> results = Stream.of("a", "bb", "cc", "ddd", "ee", "fff") + .gather(MoreGatherers.groupingBy(String::length, Collectors.toSet())) + .toList(); + + assertThat(results) + .hasSize(3) + .containsExactlyInAnyOrder( + Map.entry(1, Set.of("a")), + Map.entry(2, Set.of("bb", "cc", "ee")), + Map.entry(3, Set.of("ddd", "fff")) + ); + } +}