├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── question.md │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── pitest.yml │ ├── build.yml │ └── release.yml ├── CONTRIBUTING.md ├── src ├── main │ └── java │ │ ├── module-info.java │ │ └── com │ │ └── pivovarit │ │ └── gatherers │ │ ├── package-info.java │ │ ├── ZipWithIndexGatherer.java │ │ ├── FilterByIndexGatherer.java │ │ ├── ZipWithIndexMappingGatherer.java │ │ ├── SamplingGatherer.java │ │ ├── DistinctByGatherer.java │ │ ├── ZipIteratorGatherer.java │ │ ├── DistinctUntilChangedGatherer.java │ │ ├── DistinctByKeepLastGatherer.java │ │ ├── GroupingByGatherer.java │ │ ├── WindowSlidingGatherer.java │ │ ├── LastGatherer.java │ │ └── MoreGatherers.java └── test │ ├── java │ └── com │ │ └── pivovarit │ │ └── gatherers │ │ ├── blackbox │ │ ├── DistinctUntilChangedTest.java │ │ ├── ZipWithIndexTest.java │ │ ├── DistinctByTest.java │ │ ├── ByIndexTest.java │ │ ├── ZipWithIndexMapperTest.java │ │ ├── SamplingTest.java │ │ ├── DistinctUntilChangedMapperTest.java │ │ ├── LastTest.java │ │ ├── DistinctByKeepLastTest.java │ │ ├── ZipStreamTest.java │ │ ├── ZipIteratorTest.java │ │ ├── ZipWithIterableTest.java │ │ ├── ZipStreamMapperTest.java │ │ ├── ZipWithIterableMapperTest.java │ │ ├── ZipIteratorMapperTest.java │ │ ├── WindowSlidingTest.java │ │ └── GroupingByGathererTest.java │ │ ├── Benchmarks.java │ │ ├── FilterByIndexBenchmark.java │ │ └── ArchitectureTest.java │ └── resources │ └── benchmarks │ └── FilterByIndexBenchmark.json ├── .gitignore ├── CODE_OF_CONDUCT.md ├── README.md ├── LICENSE.md └── pom.xml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @pivovarit 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [pivovarit] 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Just a bit of common sense - it's better to start with an issue and discuss in order to avoid doing good work that doesn't go along the roadmap. -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /// Missing Stream API functionality you always longed for - provided via `Gatherers` 2 | module com.pivovarit.gatherers { 3 | exports com.pivovarit.gatherers; 4 | 5 | requires static org.jspecify; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/package-info.java: -------------------------------------------------------------------------------- 1 | /// Contains various [java.util.stream.Gatherer] implementations expanding [java.util.stream.Stream] API functionality 2 | @NullMarked 3 | package com.pivovarit.gatherers; 4 | 5 | import org.jspecify.annotations.NullMarked; 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Just ask a question if you need clarification or have doubts 4 | title: '' 5 | labels: 'question' 6 | assignees: 'pivovarit' 7 | 8 | --- 9 | 10 | **Additional context** 11 | Add any other context or screenshots here. 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "02:00" 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | time: "02:00" 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | ### Java template 25 | # Compiled class file 26 | 27 | # Log file 28 | 29 | # BlueJ files 30 | 31 | # Mobile Tools for Java (J2ME) 32 | 33 | # Package Files # 34 | 35 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 36 | 37 | .idea 38 | *.iml 39 | target 40 | 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: 'pivovarit' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/ZipWithIndexGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.atomic.AtomicLong; 5 | import java.util.function.Supplier; 6 | import java.util.stream.Gatherer; 7 | 8 | record ZipWithIndexGatherer() implements Gatherer> { 9 | 10 | @Override 11 | public Supplier initializer() { 12 | return AtomicLong::new; 13 | } 14 | 15 | @Override 16 | public Integrator> integrator() { 17 | return Integrator.ofGreedy((state, element, downstream) -> downstream.push(Map.entry(element, state.getAndIncrement()))); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: 'pivovarit' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/DistinctUntilChangedTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.Stream; 6 | 7 | import static com.pivovarit.gatherers.MoreGatherers.distinctUntilChanged; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | class DistinctUntilChangedTest { 11 | 12 | @Test 13 | void shouldDistinctUntilChangedEmptyStream() { 14 | assertThat(Stream.empty().gather(distinctUntilChanged())).isEmpty(); 15 | } 16 | 17 | @Test 18 | void shouldDistinctUntilChanged() { 19 | assertThat(Stream.of(1, 1, 2, 2, 3, 3, 4, 5, 5, 5, 1, 1) 20 | .gather(distinctUntilChanged())) 21 | .containsExactly(1, 2, 3, 4, 5, 1); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/ZipWithIndexTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.Stream; 6 | 7 | import static com.pivovarit.gatherers.MoreGatherers.zipWithIndex; 8 | import static java.util.Map.entry; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class ZipWithIndexTest { 12 | 13 | @Test 14 | void shouldZipEmptyStream() { 15 | assertThat(Stream.empty().gather(zipWithIndex())).isEmpty(); 16 | } 17 | 18 | @Test 19 | void shouldZipWithIndex() { 20 | assertThat(Stream.of("a", "b", "c") 21 | .gather(zipWithIndex())) 22 | .containsExactly( 23 | entry("a", 0L), 24 | entry("b", 1L), 25 | entry("c", 2L) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/FilterByIndexGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.atomic.AtomicLong; 5 | import java.util.function.BiPredicate; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Gatherer; 8 | 9 | record FilterByIndexGatherer(BiPredicate predicate) implements Gatherer { 10 | 11 | FilterByIndexGatherer { 12 | Objects.requireNonNull(predicate); 13 | } 14 | 15 | @Override 16 | public Supplier initializer() { 17 | return AtomicLong::new; 18 | } 19 | 20 | @Override 21 | public Integrator integrator() { 22 | return Integrator.ofGreedy((seq, t, downstream) -> predicate.test(seq.getAndIncrement(), t) && downstream.push(t)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/ZipWithIndexMappingGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.atomic.AtomicLong; 5 | import java.util.function.BiFunction; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Gatherer; 8 | 9 | record ZipWithIndexMappingGatherer(BiFunction mapper) 10 | implements Gatherer { 11 | 12 | ZipWithIndexMappingGatherer { 13 | Objects.requireNonNull(mapper, "mapper can't be null"); 14 | } 15 | 16 | @Override 17 | public Supplier initializer() { 18 | return AtomicLong::new; 19 | } 20 | 21 | @Override 22 | public Integrator integrator() { 23 | return Integrator.ofGreedy((state, element, downstream) -> downstream.push(mapper.apply(state.getAndIncrement(), element))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/SamplingGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.concurrent.atomic.AtomicLong; 4 | import java.util.function.Supplier; 5 | import java.util.stream.Gatherer; 6 | 7 | record SamplingGatherer(int n) implements Gatherer { 8 | 9 | SamplingGatherer { 10 | if (n <= 0) { 11 | throw new IllegalArgumentException("sampling frequency can't be lower than 1"); 12 | } 13 | } 14 | 15 | @Override 16 | public Supplier initializer() { 17 | return AtomicLong::new; 18 | } 19 | 20 | @Override 21 | public Integrator integrator() { 22 | return Integrator.ofGreedy((state, element, downstream) -> { 23 | if (state.getAndIncrement() % n == 0) { 24 | return downstream.push(element); 25 | } 26 | return true; 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/DistinctByGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.HashSet; 4 | import java.util.Objects; 5 | import java.util.function.Function; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Gatherer; 8 | 9 | record DistinctByGatherer(Function keyExtractor) implements Gatherer, T> { 10 | 11 | DistinctByGatherer { 12 | Objects.requireNonNull(keyExtractor, "keyExtractor can't be null"); 13 | } 14 | 15 | @Override 16 | public Supplier> initializer() { 17 | return HashSet::new; 18 | } 19 | 20 | @Override 21 | public Integrator, T, T> integrator() { 22 | return (state, element, downstream) -> { 23 | if (state.add(keyExtractor.apply(element))) { 24 | return downstream.push(element); 25 | } 26 | return true; 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/DistinctByTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.Stream; 6 | 7 | import static com.pivovarit.gatherers.MoreGatherers.distinctBy; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 10 | 11 | class DistinctByTest { 12 | 13 | @Test 14 | void shouldDistinctByEmptyStream() { 15 | assertThat(Stream.empty().gather(distinctBy(i -> i))).isEmpty(); 16 | } 17 | 18 | @Test 19 | void shouldDistinctBy() { 20 | assertThat(Stream.of("a", "bb", "cc", "ddd") 21 | .gather(distinctBy(String::length))) 22 | .containsExactly("a", "bb", "ddd"); 23 | } 24 | 25 | @Test 26 | void shouldRejectNullExtractor() { 27 | assertThatThrownBy(() -> distinctBy(null)).isInstanceOf(NullPointerException.class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/Benchmarks.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import org.openjdk.jmh.results.format.ResultFormatType; 4 | import org.openjdk.jmh.runner.Runner; 5 | import org.openjdk.jmh.runner.RunnerException; 6 | import org.openjdk.jmh.runner.options.OptionsBuilder; 7 | 8 | import java.nio.file.Path; 9 | 10 | final class Benchmarks { 11 | 12 | private Benchmarks() { 13 | } 14 | 15 | private static final Path BENCHMARKS_PATH = Path.of("src/test/resources/benchmarks/"); 16 | 17 | static void run(Class clazz) throws RunnerException { 18 | new Runner(new OptionsBuilder() 19 | .include(clazz.getSimpleName()) 20 | .warmupIterations(3) 21 | .measurementIterations(5) 22 | .resultFormat(ResultFormatType.JSON) 23 | .result(Benchmarks.BENCHMARKS_PATH.resolve("%s.json".formatted(clazz.getSimpleName())).toString()) 24 | .forks(1) 25 | .build()).run(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/ByIndexTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.Stream; 6 | 7 | import static com.pivovarit.gatherers.MoreGatherers.filteringByIndex; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 10 | 11 | class ByIndexTest { 12 | 13 | @Test 14 | void shouldRejectNullPredicate() { 15 | assertThatThrownBy(() -> filteringByIndex(null)).isInstanceOf(NullPointerException.class); 16 | } 17 | 18 | @Test 19 | void shouldFilterByIndexEmptyStream() { 20 | assertThat(Stream.empty().gather(filteringByIndex((_, _) -> true))).isEmpty(); 21 | } 22 | 23 | @Test 24 | void shouldFilterByIndex() { 25 | assertThat(Stream.of("a", "bb", "cc", "ddd") 26 | .gather(filteringByIndex((i, _) -> i % 2 == 0))) 27 | .containsExactly("a", "cc"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/ZipWithIndexMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.Stream; 6 | 7 | import static com.pivovarit.gatherers.MoreGatherers.zipWithIndex; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 10 | 11 | class ZipWithIndexMapperTest { 12 | 13 | @Test 14 | void shouldRejectNullMapper() { 15 | assertThatThrownBy(() -> zipWithIndex(null)).isInstanceOf(NullPointerException.class); 16 | } 17 | 18 | @Test 19 | void shouldZipEmptyStream() { 20 | assertThat(Stream.empty().gather(zipWithIndex((idx, i) -> "" + idx + i))).isEmpty(); 21 | } 22 | 23 | @Test 24 | void shouldZipWithIndex() { 25 | assertThat(Stream.of("a", "b", "c") 26 | .gather(zipWithIndex((idx, i) -> idx + i))) 27 | .containsExactly("0a", "1b", "2c"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/ZipIteratorGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.Iterator; 4 | import java.util.Objects; 5 | import java.util.function.BiFunction; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Gatherer; 8 | 9 | record ZipIteratorGatherer(Iterator iterator, BiFunction mapper) 10 | implements Gatherer, R> { 11 | 12 | ZipIteratorGatherer { 13 | Objects.requireNonNull(mapper, "mapper can't be null"); 14 | Objects.requireNonNull(iterator, "iterator can't be null"); 15 | } 16 | 17 | @Override 18 | public Supplier> initializer() { 19 | return () -> iterator; 20 | } 21 | 22 | @Override 23 | public Integrator, T1, R> integrator() { 24 | return (state, element, downstream) -> state.hasNext() 25 | ? downstream.push(mapper.apply(element, state.next())) 26 | : state.hasNext(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/pitest.yml: -------------------------------------------------------------------------------- 1 | name: pitest 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | - '**.yml' 10 | - '**.yaml' 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | java: [ '24-ea' ] 18 | architecture: [ 'x64' ] 19 | name: Mutation Testing with JDK ${{ matrix.java }} on ${{ matrix.architecture }} 20 | steps: 21 | - uses: actions/checkout@v6 22 | - name: Setup JDK 23 | uses: actions/setup-java@v5 24 | with: 25 | distribution: 'temurin' 26 | java-version: ${{ matrix.java }} 27 | architecture: ${{ matrix.architecture }} 28 | cache: 'maven' 29 | 30 | - name: Run Mutation Testing 31 | run: mvn package -Ppitest org.pitest:pitest-maven:mutationCoverage 32 | 33 | - name: Deploy 34 | uses: peaceiris/actions-gh-pages@v4.0.0 35 | with: 36 | github_token: ${{ secrets.GITHUB_TOKEN }} 37 | publish_dir: ./target/pit-reports 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/FilterByIndexBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import org.openjdk.jmh.annotations.Benchmark; 4 | import org.openjdk.jmh.runner.RunnerException; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.stream.Stream; 9 | 10 | public class FilterByIndexBenchmark { 11 | 12 | private static List source = Stream.iterate(0, i -> i + 1).limit(10_000_000).toList(); 13 | 14 | @Benchmark 15 | public List filterByIndex() { 16 | return source.stream() 17 | .gather(MoreGatherers.filteringByIndex((i, _) -> i % 2 == 0)) 18 | .toList(); 19 | } 20 | 21 | @Benchmark 22 | public List zipWithIndexThenFilter() { 23 | return source.stream() 24 | .gather(MoreGatherers.zipWithIndex()) 25 | .filter(t -> t.getValue() % 2 == 0) 26 | .map(Map.Entry::getKey) 27 | .toList(); 28 | } 29 | 30 | public static void main(String[] ignored) throws RunnerException { 31 | Benchmarks.run(FilterByIndexBenchmark.class); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/SamplingTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.Stream; 6 | 7 | import static com.pivovarit.gatherers.MoreGatherers.sampling; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 10 | 11 | class SamplingTest { 12 | 13 | @Test 14 | void shouldRejectInvalidSampleSize() { 15 | assertThatThrownBy(() -> sampling(0)) 16 | .isInstanceOf(IllegalArgumentException.class) 17 | .hasMessage("sampling frequency can't be lower than 1"); 18 | } 19 | 20 | @Test 21 | void shouldSampleEmpty() throws Exception { 22 | assertThat(Stream.empty().gather(sampling(42))).isEmpty(); 23 | } 24 | 25 | @Test 26 | void shouldSampleEvery() { 27 | assertThat(Stream.of(1,2,3).gather(sampling(1))).containsExactly(1,2,3); 28 | } 29 | 30 | @Test 31 | void shouldSampleEveryOther() { 32 | assertThat(Stream.of(1,2,3).gather(sampling(2))).containsExactly(1,3); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/DistinctUntilChangedMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.Stream; 6 | 7 | import static com.pivovarit.gatherers.MoreGatherers.distinctUntilChanged; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 10 | 11 | class DistinctUntilChangedMapperTest { 12 | 13 | @Test 14 | void shouldDistinctUntilChangedEmptyStream() { 15 | assertThat(Stream.empty().gather(distinctUntilChanged(i -> i))).isEmpty(); 16 | } 17 | 18 | @Test 19 | void shouldDistinctUntilChangedMapper() { 20 | assertThat(Stream.of("a", "b", "bb", "cc", "ddd", "eee", "ffff", "ggggg", "hhhhh", "iiiii", "j", "k") 21 | .gather(distinctUntilChanged(s -> s.length()))) 22 | .containsExactly("a", "bb", "ddd", "ffff", "ggggg", "j"); 23 | } 24 | 25 | @Test 26 | void shouldRejectNullMapper() { 27 | assertThatThrownBy(() -> distinctUntilChanged(null)).isInstanceOf(NullPointerException.class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/DistinctUntilChangedGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Function; 5 | import java.util.function.Supplier; 6 | import java.util.stream.Gatherer; 7 | 8 | record DistinctUntilChangedGatherer( 9 | Function keyExtractor) implements Gatherer, T> { 10 | 11 | DistinctUntilChangedGatherer { 12 | Objects.requireNonNull(keyExtractor, "keyExtractor can't be null"); 13 | } 14 | 15 | @Override 16 | public Supplier> initializer() { 17 | return State::new; 18 | } 19 | 20 | @Override 21 | public Integrator, T, T> integrator() { 22 | return (state, element, downstream) -> { 23 | U key = keyExtractor.apply(element); 24 | if (!state.hasValue || !Objects.equals(state.value, key)) { 25 | state.value = key; 26 | state.hasValue = true; 27 | return downstream.push(element); 28 | } 29 | return true; 30 | }; 31 | } 32 | 33 | static class State { 34 | U value; 35 | boolean hasValue; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/LastTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.Stream; 6 | 7 | import static com.pivovarit.gatherers.MoreGatherers.last; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 10 | 11 | class LastTest { 12 | 13 | @Test 14 | void shouldRejectInvalidSize() { 15 | assertThatThrownBy(() -> last(0)) 16 | .isInstanceOf(IllegalArgumentException.class) 17 | .hasMessage("number of elements can't be lower than one"); 18 | } 19 | 20 | @Test 21 | void shouldLastEmpty() { 22 | assertThat(Stream.of().gather(last(42))).isEmpty(); 23 | } 24 | 25 | @Test 26 | void shouldTakeLastElement() { 27 | assertThat(Stream.of(1, 2, 3).gather(last(1))).containsExactly(3); 28 | } 29 | 30 | @Test 31 | void shouldTakeLastNElements() { 32 | assertThat(Stream.of(1, 2, 3).gather(last(2))).containsExactly(2, 3); 33 | } 34 | 35 | @Test 36 | void shouldTakeLastAllElements() { 37 | assertThat(Stream.of(1, 2, 3).gather(last(42))).containsExactly(1, 2, 3); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/DistinctByKeepLastTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.Stream; 6 | 7 | import static com.pivovarit.gatherers.MoreGatherers.distinctByKeepLast; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 10 | 11 | class DistinctByKeepLastTest { 12 | 13 | @Test 14 | void shouldDistinctByEmptyStream() { 15 | assertThat(Stream.empty().gather(distinctByKeepLast(i -> i))).isEmpty(); 16 | } 17 | 18 | @Test 19 | void shouldDistinctBy() { 20 | assertThat(Stream.of("a", "bb", "cc", "ddd") 21 | .gather(distinctByKeepLast(String::length))) 22 | .containsExactly("a", "cc", "ddd"); 23 | } 24 | 25 | @Test 26 | void shouldDistinctByAndKeepLastOrder() { 27 | assertThat(Stream.of("a", "bb", "ddd", "cc") 28 | .gather(distinctByKeepLast(String::length))) 29 | .containsExactly("a", "ddd", "cc"); 30 | } 31 | 32 | @Test 33 | void shouldRejectNullExtractor() { 34 | assertThatThrownBy(() -> distinctByKeepLast(null)).isInstanceOf(NullPointerException.class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - '**.md' 8 | - '**.yml' 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | java: [ '24', '25' ] 19 | architecture: [ 'x64' ] 20 | name: Build with JDK ${{ matrix.java }} on ${{ matrix.architecture }} 21 | steps: 22 | - uses: actions/checkout@v6 23 | - name: Setup JDK 24 | uses: actions/setup-java@v5 25 | with: 26 | distribution: 'temurin' 27 | java-version: ${{ matrix.java }} 28 | architecture: ${{ matrix.architecture }} 29 | cache: 'maven' 30 | 31 | - name: Build with Maven 32 | run: mvn verify 33 | 34 | verify-javadoc: 35 | runs-on: ubuntu-latest 36 | name: Validate JavaDocs 37 | steps: 38 | - uses: actions/checkout@v6 39 | - name: Setup JDK 40 | uses: actions/setup-java@v5 41 | with: 42 | distribution: 'temurin' 43 | java-version: '24' 44 | architecture: 'x64' 45 | cache: 'maven' 46 | 47 | - name: Validate JavaDocs 48 | run: mvn -Pdoclint package -DskipTests=true 49 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/DistinctByKeepLastGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Objects; 5 | import java.util.function.BiConsumer; 6 | import java.util.function.Function; 7 | import java.util.function.Supplier; 8 | import java.util.stream.Gatherer; 9 | 10 | record DistinctByKeepLastGatherer( 11 | Function keyExtractor) implements Gatherer, T> { 12 | 13 | DistinctByKeepLastGatherer { 14 | Objects.requireNonNull(keyExtractor, "keyExtractor can't be null"); 15 | } 16 | 17 | @Override 18 | public Supplier> initializer() { 19 | return LinkedHashMap::new; 20 | } 21 | 22 | @Override 23 | public Integrator, T, T> integrator() { 24 | return Integrator.ofGreedy((state, element, _) -> { 25 | state.putLast(keyExtractor.apply(element), element); 26 | return true; 27 | }); 28 | } 29 | 30 | @Override 31 | public BiConsumer, Downstream> finisher() { 32 | return (state, downstream) -> { 33 | for (T element : state.sequencedValues()) { 34 | downstream.push(element); 35 | } 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | release-tag: 6 | description: 'Version to release' 7 | required: true 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | java: [ '24' ] 15 | architecture: [ 'x64' ] 16 | 17 | name: Release ${{ github.event.inputs.release-tag }} 18 | steps: 19 | - uses: actions/checkout@v6 20 | with: 21 | ref: ${{ github.event.inputs.release-tag }} 22 | 23 | - name: Setup JDK 24 | uses: actions/setup-java@v5 25 | with: 26 | distribution: 'temurin' 27 | java-version: ${{ matrix.java }} 28 | architecture: ${{ matrix.architecture }} 29 | cache: 'maven' 30 | server-id: central 31 | server-username: MAVEN_USERNAME 32 | server-password: MAVEN_PASSWORD 33 | gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 34 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 35 | 36 | 37 | - name: Release with Maven 38 | run: mvn -B --no-transfer-progress -Psonatype-oss-release -DskipTests=true deploy 39 | env: 40 | MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} 41 | MAVEN_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} 42 | MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} 43 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/ZipStreamTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Iterator; 6 | import java.util.stream.Stream; 7 | 8 | import static com.pivovarit.gatherers.MoreGatherers.*; 9 | import static java.util.Map.entry; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | 13 | class ZipStreamTest { 14 | 15 | @Test 16 | void shouldZipEmpty() { 17 | assertThat(Stream.empty().gather(zip(Stream.of(1, 2, 3)))).isEmpty(); 18 | } 19 | 20 | @Test 21 | void shouldZipWithEmpty() { 22 | assertThat(Stream.of(1, 2, 3).gather(zip(Stream.of()))).isEmpty(); 23 | } 24 | 25 | @Test 26 | void shouldZip() { 27 | assertThat(Stream.of(1, 2, 3).gather(zip(Stream.of("a", "b", "c", "d")))) 28 | .containsExactly( 29 | entry(1, "a"), 30 | entry(2, "b"), 31 | entry(3, "c") 32 | ); 33 | } 34 | 35 | @Test 36 | void shouldZipWithShorter() { 37 | assertThat(Stream.of(1, 2, 3).gather(zip(Stream.of("a", "b")))) 38 | .containsExactly( 39 | entry(1, "a"), 40 | entry(2, "b") 41 | ); 42 | } 43 | 44 | @Test 45 | void shouldRejectNullStream() { 46 | assertThatThrownBy(() -> zip((Iterator) null)).isInstanceOf(NullPointerException.class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/ZipIteratorTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Iterator; 6 | import java.util.stream.Stream; 7 | 8 | import static com.pivovarit.gatherers.MoreGatherers.zip; 9 | import static java.util.Map.entry; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | 13 | class ZipIteratorTest { 14 | 15 | @Test 16 | void shouldZipEmpty() { 17 | assertThat(Stream.empty().gather(zip(Stream.of(1, 2, 3).iterator()))).isEmpty(); 18 | } 19 | 20 | @Test 21 | void shouldZipWithEmpty() { 22 | assertThat(Stream.of(1, 2, 3).gather(zip(Stream.of().iterator()))).isEmpty(); 23 | } 24 | 25 | @Test 26 | void shouldZip() { 27 | assertThat(Stream.of(1, 2, 3).gather(zip(Stream.of("a", "b", "c", "d").iterator()))) 28 | .containsExactly( 29 | entry(1, "a"), 30 | entry(2, "b"), 31 | entry(3, "c") 32 | ); 33 | } 34 | 35 | @Test 36 | void shouldZipWithShorter() { 37 | assertThat(Stream.of(1, 2, 3).gather(zip(Stream.of("a", "b").iterator()))) 38 | .containsExactly( 39 | entry(1, "a"), 40 | entry(2, "b") 41 | ); 42 | } 43 | 44 | @Test 45 | void shouldRejectNullIterator() { 46 | assertThatThrownBy(() -> zip((Iterator) null)).isInstanceOf(NullPointerException.class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/ZipWithIterableTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | 8 | import static com.pivovarit.gatherers.MoreGatherers.zipWithIterable; 9 | import static java.util.Map.entry; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | 13 | class ZipWithIterableTest { 14 | 15 | @Test 16 | void shouldZipEmpty() { 17 | assertThat(Stream.empty().gather(zipWithIterable(List.of(1, 2, 3)))).isEmpty(); 18 | } 19 | 20 | @Test 21 | void shouldZipWithEmpty() { 22 | assertThat(Stream.of(1, 2, 3).gather(zipWithIterable(List.of()))).isEmpty(); 23 | } 24 | 25 | @Test 26 | void shouldZip() { 27 | assertThat(Stream.of(1, 2, 3).gather(zipWithIterable(List.of("a", "b", "c", "d")))) 28 | .containsExactly( 29 | entry(1, "a"), 30 | entry(2, "b"), 31 | entry(3, "c") 32 | ); 33 | } 34 | 35 | @Test 36 | void shouldZipWithShorter() { 37 | assertThat(Stream.of(1, 2, 3).gather(zipWithIterable(List.of("a", "b")))) 38 | .containsExactly( 39 | entry(1, "a"), 40 | entry(2, "b") 41 | ); 42 | } 43 | 44 | @Test 45 | void shouldRejectNullCollection() { 46 | assertThatThrownBy(() -> zipWithIterable(null)).isInstanceOf(NullPointerException.class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/ZipStreamMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | 8 | import static com.pivovarit.gatherers.MoreGatherers.zip; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 11 | 12 | class ZipStreamMapperTest { 13 | 14 | @Test 15 | void shouldZipEmpty() { 16 | assertThat(Stream.empty().gather(zip(Stream.of(1, 2, 3), Integer::sum))).isEmpty(); 17 | } 18 | 19 | @Test 20 | void shouldZipWithEmpty() { 21 | assertThat(Stream.of(1, 2, 3).gather(zip(Stream.of(), Integer::sum))).isEmpty(); 22 | } 23 | 24 | @Test 25 | void shouldZip() { 26 | assertThat(Stream.of(1, 2, 3).gather(zip(Stream.of("a", "b", "c", "d"), (i, s) -> i + s))) 27 | .containsExactly("1a", "2b", "3c"); 28 | } 29 | 30 | @Test 31 | void shouldZipWithShorter() { 32 | assertThat(Stream.of(1, 2, 3).gather(zip(Stream.of("a", "b"), (i, s) -> i + s))) 33 | .containsExactly("1a", "2b"); 34 | } 35 | 36 | @Test 37 | void shouldRejectNullStream() { 38 | assertThatThrownBy(() -> zip((Stream) null, (i, _) -> i)).isInstanceOf(NullPointerException.class); 39 | } 40 | 41 | @Test 42 | void shouldRejectNullMapper() { 43 | assertThatThrownBy(() -> zip(List.of(1).stream(), null)).isInstanceOf(NullPointerException.class); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/ZipWithIterableMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | 8 | import static com.pivovarit.gatherers.MoreGatherers.zipWithIterable; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 11 | 12 | class ZipWithIterableMapperTest { 13 | 14 | @Test 15 | void shouldZipEmpty() { 16 | assertThat(Stream.empty().gather(zipWithIterable(List.of(1, 2, 3), Integer::sum))).isEmpty(); 17 | } 18 | 19 | @Test 20 | void shouldZipWithEmpty() { 21 | assertThat(Stream.of(1, 2, 3).gather(zipWithIterable(List.of(), Integer::sum))).isEmpty(); 22 | } 23 | 24 | @Test 25 | void shouldZip() { 26 | assertThat(Stream.of(1, 2, 3).gather(zipWithIterable(List.of("a", "b", "c", "d"), (i, s) -> i + s))) 27 | .containsExactly("1a", "2b", "3c"); 28 | } 29 | 30 | @Test 31 | void shouldZipWithShorter() { 32 | assertThat(Stream.of(1, 2, 3).gather(zipWithIterable(List.of("a", "b"), (i, s) -> i + s))) 33 | .containsExactly("1a", "2b"); 34 | } 35 | 36 | @Test 37 | void shouldRejectNullCollection() { 38 | assertThatThrownBy(() -> zipWithIterable(null, (i, _) -> i)).isInstanceOf(NullPointerException.class); 39 | } 40 | 41 | @Test 42 | void shouldRejectNullMapper() { 43 | assertThatThrownBy(() -> zipWithIterable(List.of(1), null)).isInstanceOf(NullPointerException.class); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/GroupingByGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | import java.util.function.BiConsumer; 7 | import java.util.function.Function; 8 | import java.util.function.Supplier; 9 | import java.util.stream.Collector; 10 | import java.util.stream.Gatherer; 11 | import java.util.stream.Stream; 12 | 13 | record GroupingByGatherer(Function classifier, 14 | Collector collector) 15 | implements Gatherer>, Map.Entry> { 16 | 17 | GroupingByGatherer { 18 | Objects.requireNonNull(classifier, "classifier can't be null"); 19 | Objects.requireNonNull(collector, "collector can't be null"); 20 | } 21 | 22 | @Override 23 | public Supplier>> initializer() { 24 | return HashMap::new; 25 | } 26 | 27 | @Override 28 | public Integrator>, T, Map.Entry> integrator() { 29 | return Integrator.ofGreedy((state, element, _) -> { 30 | state.computeIfAbsent(classifier.apply(element), _ -> Stream.builder()).accept(element); 31 | return true; 32 | }); 33 | } 34 | 35 | @Override 36 | public BiConsumer>, Downstream>> finisher() { 37 | return (map, downstream) -> map.forEach((key, builder) -> downstream.push(Map.entry(key, builder.build() 38 | .collect(collector)))); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/ZipIteratorMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Iterator; 6 | import java.util.List; 7 | import java.util.stream.Stream; 8 | 9 | import static com.pivovarit.gatherers.MoreGatherers.zip; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | 13 | class ZipIteratorMapperTest { 14 | 15 | @Test 16 | void shouldZipEmpty() { 17 | assertThat(Stream.empty().gather(zip(List.of(1, 2, 3).iterator(), Integer::sum))).isEmpty(); 18 | } 19 | 20 | @Test 21 | void shouldZipWithEmpty() { 22 | assertThat(Stream.of(1, 2, 3).gather(zip(List.of().iterator(), Integer::sum))).isEmpty(); 23 | } 24 | 25 | @Test 26 | void shouldZip() { 27 | assertThat(Stream.of(1, 2, 3).gather(zip(List.of("a", "b", "c", "d").iterator(), (i, s) -> i + s))) 28 | .containsExactly("1a", "2b", "3c"); 29 | } 30 | 31 | @Test 32 | void shouldZipWithShorter() { 33 | assertThat(Stream.of(1, 2, 3).gather(zip(List.of("a", "b").iterator(), (i, s) -> i + s))) 34 | .containsExactly("1a", "2b"); 35 | } 36 | 37 | @Test 38 | void shouldRejectNullIterator() { 39 | assertThatThrownBy(() -> zip((Iterator) null, (i, _) -> i)).isInstanceOf(NullPointerException.class); 40 | } 41 | 42 | @Test 43 | void shouldRejectNullMapper() { 44 | assertThatThrownBy(() -> zip(List.of(1).iterator(), null)).isInstanceOf(NullPointerException.class); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/ArchitectureTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import com.tngtech.archunit.core.importer.ImportOption; 4 | import com.tngtech.archunit.junit.AnalyzeClasses; 5 | import com.tngtech.archunit.junit.ArchTest; 6 | import com.tngtech.archunit.lang.ArchRule; 7 | 8 | import static com.tngtech.archunit.core.domain.JavaModifier.FINAL; 9 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 10 | 11 | @AnalyzeClasses(packages = "com.pivovarit", importOptions = ImportOption.DoNotIncludeTests.class) 12 | class ArchitectureTest { 13 | 14 | @ArchTest 15 | static final ArchRule shouldHaveSingleFacade = classes() 16 | .that().arePublic() 17 | .should().haveSimpleName("MoreGatherers") 18 | .andShould().haveOnlyFinalFields() 19 | .andShould().haveOnlyPrivateConstructors() 20 | .andShould().haveModifier(FINAL) 21 | .as("all public factory methods should be accessed through MoreGatherers public API") 22 | .because("users of MoreGatherers should have a single entry point"); 23 | 24 | @ArchTest 25 | static final ArchRule shouldHaveZeroDependencies = classes() 26 | .that().resideInAPackage("com.pivovarit.gatherers") 27 | .should() 28 | .onlyDependOnClassesThat() 29 | .resideInAnyPackage("com.pivovarit.gatherers", "org.jspecify.annotations", "java..") 30 | .as("the library should depend only on core Java classes") 31 | .because("users appreciate not experiencing a dependency hell"); 32 | 33 | @ArchTest 34 | static final ArchRule shouldHaveSinglePackage = classes() 35 | .should().resideInAPackage("com.pivovarit.gatherers"); 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/WindowSlidingTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import com.pivovarit.gatherers.MoreGatherers; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.stream.Stream; 7 | 8 | import static java.util.List.of; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 11 | 12 | class WindowSlidingTest { 13 | 14 | @Test 15 | void shouldRejectInvalidWindowSize() { 16 | assertThatThrownBy(() -> MoreGatherers.windowSliding(0, 1)) 17 | .isInstanceOf(IllegalArgumentException.class) 18 | .hasMessage("'windowSize' must be greater than zero"); 19 | } 20 | 21 | @Test 22 | void shouldRejectInvalidStep() { 23 | assertThatThrownBy(() -> MoreGatherers.windowSliding(1, -1)) 24 | .isInstanceOf(IllegalArgumentException.class) 25 | .hasMessage("'step' must be greater than or equal to zero"); 26 | } 27 | 28 | @Test 29 | void shouldRejectInvalidWindowSizeAndStep() { 30 | assertThatThrownBy(() -> MoreGatherers.windowSliding(3, 4)) 31 | .isInstanceOf(IllegalArgumentException.class) 32 | .hasMessage("'step' must be less than or equal to 'windowSize'"); 33 | } 34 | 35 | @Test 36 | void shouldWindowSlidingEmpty() { 37 | assertThat(Stream.empty().gather(MoreGatherers.windowSliding(2, 1))).isEmpty(); 38 | } 39 | 40 | @Test 41 | void shouldWindowSlidingWithWindowSizeGreaterThanStreamSize() { 42 | assertThat(Stream.of(1, 2, 3).gather(MoreGatherers.windowSliding(4, 1))) 43 | .containsExactly(of(1, 2, 3)); 44 | } 45 | 46 | @Test 47 | void shouldWindowSlidingWithStep1() { 48 | assertThat(Stream.of(1, 2, 3, 4, 5).gather(MoreGatherers.windowSliding(2, 1))) 49 | .containsExactly(of(1, 2), of(2, 3), of(3, 4), of(4, 5)); 50 | } 51 | 52 | @Test 53 | void shouldWindowSlidingWithStep2() { 54 | assertThat(Stream.of(1, 2, 3, 4, 5).gather(MoreGatherers.windowSliding(2, 2))) 55 | .containsExactly(of(1, 2), of(3, 4), of(5)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/com/pivovarit/gatherers/blackbox/GroupingByGathererTest.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers.blackbox; 2 | 3 | import com.pivovarit.gatherers.MoreGatherers; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 14 | 15 | class GroupingByGathererTest { 16 | 17 | @Test 18 | void shouldRejectNullClassifier() { 19 | assertThatThrownBy(() -> MoreGatherers.groupingBy(null)) 20 | .isInstanceOf(NullPointerException.class) 21 | .hasMessageContaining("classifier"); 22 | } 23 | 24 | @Test 25 | void shouldRejectNullCollector() { 26 | assertThatThrownBy(() -> MoreGatherers.groupingBy(i -> i, null)) 27 | .isInstanceOf(NullPointerException.class) 28 | .hasMessageContaining("collector"); 29 | } 30 | 31 | @Test 32 | void shouldGroupEmpty() { 33 | assertThat(List.of().stream().gather(MoreGatherers.groupingBy(i -> i))).isEmpty(); 34 | } 35 | 36 | @Test 37 | void shouldGroupEmptyWithCustomCollector() { 38 | assertThat(List.of().stream().gather(MoreGatherers.groupingBy(i -> i, Collectors.toSet()))).isEmpty(); 39 | } 40 | 41 | @Test 42 | void shouldGroupToList() { 43 | List>> results = Stream.of("a", "bb", "cc", "ddd", "ee", "fff") 44 | .gather(MoreGatherers.groupingBy(String::length)) 45 | .toList(); 46 | 47 | assertThat(results) 48 | .hasSize(3) 49 | .containsExactlyInAnyOrder( 50 | Map.entry(1, List.of("a")), 51 | Map.entry(2, List.of("bb", "cc", "ee")), 52 | Map.entry(3, List.of("ddd", "fff")) 53 | ); 54 | } 55 | 56 | @Test 57 | void shouldGroupUsingCustomCollector() { 58 | List>> results = Stream.of("a", "bb", "cc", "ddd", "ee", "fff") 59 | .gather(MoreGatherers.groupingBy(String::length, Collectors.toSet())) 60 | .toList(); 61 | 62 | assertThat(results) 63 | .hasSize(3) 64 | .containsExactlyInAnyOrder( 65 | Map.entry(1, Set.of("a")), 66 | Map.entry(2, Set.of("bb", "cc", "ee")), 67 | Map.entry(3, Set.of("ddd", "fff")) 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/WindowSlidingGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.function.BiConsumer; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Gatherer; 8 | 9 | record WindowSlidingGatherer(int windowSize, int step) 10 | implements Gatherer> { 11 | WindowSlidingGatherer { 12 | if (windowSize < 1) { 13 | throw new IllegalArgumentException("'windowSize' must be greater than zero"); 14 | } 15 | 16 | if (step < 0) { 17 | throw new IllegalArgumentException("'step' must be greater than or equal to zero"); 18 | } 19 | 20 | if (step > windowSize) { 21 | throw new IllegalArgumentException("'step' must be less than or equal to 'windowSize'"); 22 | } 23 | } 24 | 25 | @Override 26 | public Supplier initializer() { 27 | return WindowSlidingGatherer.SlidingWindow::new; 28 | } 29 | 30 | @Override 31 | public Integrator> integrator() { 32 | return Integrator.ofGreedy((state, e, downstream) -> state.integrate(e, downstream)); 33 | } 34 | 35 | @Override 36 | public BiConsumer>> finisher() { 37 | return SlidingWindow::finish; 38 | } 39 | 40 | class SlidingWindow { 41 | private Object[] window = new Object[windowSize]; 42 | private int at = 0; 43 | private boolean emitted = false; 44 | 45 | boolean integrate(T element, Downstream> downstream) { 46 | window[at++] = element; 47 | emitted = false; 48 | if (at < windowSize) { 49 | return true; 50 | } else { 51 | final var oldWindow = window; 52 | final var newWindow = new Object[windowSize]; 53 | System.arraycopy(oldWindow, step, newWindow, 0, windowSize - step); 54 | window = newWindow; 55 | at -= step; 56 | emitted = true; 57 | return downstream.push((List) Arrays.asList(oldWindow)); 58 | } 59 | } 60 | 61 | void finish(Downstream> downstream) { 62 | if (!emitted && at > 0 && !downstream.isRejecting()) { 63 | var lastWindow = new Object[at]; 64 | System.arraycopy(window, 0, lastWindow, 0, at); 65 | window = null; 66 | at = 0; 67 | emitted = true; 68 | downstream.push((List) Arrays.asList(lastWindow)); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at gpiwowarek@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/LastGatherer.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | import java.util.function.BiConsumer; 5 | import java.util.function.Consumer; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Gatherer; 8 | 9 | final class LastGatherer { 10 | private LastGatherer() { 11 | } 12 | 13 | static Gatherer size(int n) { 14 | return switch (n) { 15 | case 1 -> new SingleElementLastGatherer<>(); 16 | default -> new CircularBufferLastGatherer<>(n); 17 | }; 18 | } 19 | 20 | private record CircularBufferLastGatherer( 21 | long n) implements Gatherer, T> { 22 | 23 | CircularBufferLastGatherer { 24 | if (n <= 0) { 25 | throw new IllegalArgumentException("number of elements can't be lower than one"); 26 | } 27 | } 28 | 29 | @Override 30 | public Supplier> initializer() { 31 | return () -> new AppendOnlyCircularBuffer<>((int) n); 32 | } 33 | 34 | @Override 35 | public Integrator, T, T> integrator() { 36 | return Integrator.ofGreedy((state, element, _) -> { 37 | state.add(element); 38 | return true; 39 | }); 40 | } 41 | 42 | @Override 43 | public BiConsumer, Downstream> finisher() { 44 | return (state, downstream) -> { 45 | if (!downstream.isRejecting()) { 46 | state.forEach(downstream::push); 47 | } 48 | }; 49 | } 50 | 51 | static class AppendOnlyCircularBuffer { 52 | private final T[] buffer; 53 | private final int maxSize; 54 | private final AtomicInteger endIdx = new AtomicInteger(0); 55 | private final AtomicInteger size = new AtomicInteger(0); 56 | 57 | public AppendOnlyCircularBuffer(int size) { 58 | this.maxSize = size; 59 | this.buffer = (T[]) new Object[size]; 60 | } 61 | 62 | public void add(T element) { 63 | buffer[endIdx.getAndIncrement() % maxSize] = element; 64 | if (size.get() < maxSize) { 65 | size.incrementAndGet(); 66 | } 67 | } 68 | 69 | public void forEach(Consumer consumer) { 70 | int startIdx = (endIdx.get() - size.get() + maxSize) % maxSize; 71 | for (int i = 0; i < size.get(); i++) { 72 | consumer.accept(buffer[(startIdx + i) % maxSize]); 73 | } 74 | } 75 | } 76 | } 77 | 78 | private record SingleElementLastGatherer() implements Gatherer, T> { 79 | 80 | @Override 81 | public Supplier> initializer() { 82 | return ValueHolder::new; 83 | } 84 | 85 | @Override 86 | public Integrator, T, T> integrator() { 87 | return Integrator.ofGreedy((state, element, _) -> { 88 | state.value = element; 89 | state.isSet = true; 90 | return true; 91 | }); 92 | } 93 | 94 | @Override 95 | public BiConsumer, Downstream> finisher() { 96 | return (state, downstream) -> { 97 | if (state.isSet && !downstream.isRejecting()) { 98 | downstream.push(state.value); 99 | } 100 | }; 101 | } 102 | 103 | static class ValueHolder { 104 | private T value; 105 | private boolean isSet; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # more-gatherers 2 | 3 | Missing Stream API functionality you always longed for - provided via `Gatherers` 4 | 5 | [![build](https://github.com/pivovarit/more-gatherers/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/pivovarit/more-gatherers/actions/workflows/build.yml) 6 | [![pitest](https://github.com/pivovarit/more-gatherers/actions/workflows/pitest.yml/badge.svg?branch=main)](https://pivovarit.github.io/more-gatherers) 7 | [![Maven Central Version](https://img.shields.io/maven-central/v/com.pivovarit/more-gatherers)](https://central.sonatype.com/artifact/com.pivovarit/more-gatherers/versions) 8 | [![libs.tech recommends](https://libs.tech/project/869106466/badge.svg)](https://libs.tech/project/869106466/more-gatherers) 9 | 10 | [![Stargazers over time](https://starchart.cc/pivovarit/more-gatherers.svg?variant=adaptive)](https://starchart.cc/pivovarit/more-gatherers) 11 | 12 | ### Overview 13 | 14 | Java's Stream API is a powerful tool for processing collections of data. However, it lacks some functionality that could make it even more powerful. This library aims to fill that gap by providing a set of `Gatherers` that can be used to collect data from a stream more flexibly. 15 | 16 | Whenever possible, the library follows Project Reactor's naming conventions. 17 | 18 | Provided `Gatherers`: 19 | - `MoreGatherers.last(int)` 20 | - takes last `n` elements from the stream 21 | - `MoreGatherers.sampling(int)` 22 | - takes every `n`-th element from the stream 23 | - `MoreGatherers.zip(Iterator)` 24 | - zips `Stream` elements with elements from the provided `Iterator` 25 | - `MoreGatherers.zip(Iterator, BiFunction)` 26 | - zips `Stream` elements with elements from the provided `Iterator` using a custom zipper function 27 | - `MoreGatherers.zip(Stream)` 28 | - zips `Stream` elements with elements from the provided `Stream` 29 | - `MoreGatherers.zip(Stream, BiFunction)` 30 | - zips `Stream` elements with elements from the provided `Stream` using a custom zipper function 31 | - `MoreGatherers.zipWithIterable(Iterable)` 32 | - zips `Stream` elements with elements from the provided `Iterable` 33 | - `MoreGatherers.zipWithIterable(Iterable, BiFunction)` 34 | - zips elements with elements from the provided `Iterable` using a custom zipper function 35 | - `MoreGatherers.zipWithIndex()` 36 | - zips `Stream` elements with their index 37 | - `MoreGatherers.zipWithIndex(BiFunction)` 38 | - zips `Stream` elements with their index using a custom zipper function 39 | - `MoreGatherers.distinctBy(Function)` 40 | - takes distinct elements based on a key extractor function 41 | - `MoreGatherers.distinctByKeepLast(Function)` 42 | - takes distinct elements based on a key extractor function, keeping the last occurrence 43 | - `MoreGatherers.distinctUntilChanged()` 44 | - takes elements until a change is detected 45 | - `MoreGatherers.distinctUntilChanged(Function)` 46 | - takes elements until a change is detected based on a key extractor function 47 | - `MoreGatherers.windowSliding(int, int)` 48 | - creates a sliding window of a fixed size with a fixed step, extends `Gatherers.windowSliding(int)` by adding a step parameter 49 | - `MoreGatherers.filteringByIndex(BiPredicate)` 50 | - filters elements based on their index and value 51 | - `MoreGatherers.groupingBy(Function, Collector)` 52 | - groups elements by a key extractor function and applies a custom collector 53 | 54 | ### Philosophy 55 | 56 | The primary goal of this library is to complement the existing Stream API by providing functionality that's currently missing without duplicating features already available. While it is technically possible to create numerous custom Gatherers, this library focuses on offering only those that cannot be easily achieved using standard Stream API operations. 57 | 58 | The library is designed to be as lightweight as possible, with no external dependencies. It's implemented using core Java libraries and follows the same conventions as the standard Stream API, drawing inspiration from Project Reactor's method names. 59 | -------------------------------------------------------------------------------- /src/test/resources/benchmarks/FilterByIndexBenchmark.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "jmhVersion" : "1.37", 4 | "benchmark" : "com.pivovarit.gatherers.FilterByIndexBenchmark.filterByIndex", 5 | "mode" : "thrpt", 6 | "threads" : 1, 7 | "forks" : 1, 8 | "jvm" : "/Users/pivovarit/Library/Java/JavaVirtualMachines/openjdk-23/Contents/Home/bin/java", 9 | "jvmArgs" : [ 10 | "--enable-preview", 11 | "-javaagent:/Users/pivovarit/Applications/IntelliJ IDEA Ultimate.app/Contents/lib/idea_rt.jar=57563:/Users/pivovarit/Applications/IntelliJ IDEA Ultimate.app/Contents/bin", 12 | "-Dfile.encoding=UTF-8", 13 | "-Dsun.stdout.encoding=UTF-8", 14 | "-Dsun.stderr.encoding=UTF-8" 15 | ], 16 | "jdkVersion" : "23", 17 | "vmName" : "OpenJDK 64-Bit Server VM", 18 | "vmVersion" : "23+37-2369", 19 | "warmupIterations" : 3, 20 | "warmupTime" : "10 s", 21 | "warmupBatchSize" : 1, 22 | "measurementIterations" : 5, 23 | "measurementTime" : "10 s", 24 | "measurementBatchSize" : 1, 25 | "primaryMetric" : { 26 | "score" : 24.892622992010878, 27 | "scoreError" : 2.171902468249229, 28 | "scoreConfidence" : [ 29 | 22.72072052376165, 30 | 27.064525460260107 31 | ], 32 | "scorePercentiles" : { 33 | "0.0" : 24.551089114037865, 34 | "50.0" : 24.67685263461223, 35 | "90.0" : 25.89673224823203, 36 | "95.0" : 25.89673224823203, 37 | "99.0" : 25.89673224823203, 38 | "99.9" : 25.89673224823203, 39 | "99.99" : 25.89673224823203, 40 | "99.999" : 25.89673224823203, 41 | "99.9999" : 25.89673224823203, 42 | "100.0" : 25.89673224823203 43 | }, 44 | "scoreUnit" : "ops/s", 45 | "rawData" : [ 46 | [ 47 | 25.89673224823203, 48 | 24.551089114037865, 49 | 24.64380175941238, 50 | 24.67685263461223, 51 | 24.694639203759873 52 | ] 53 | ] 54 | }, 55 | "secondaryMetrics" : { 56 | } 57 | }, 58 | { 59 | "jmhVersion" : "1.37", 60 | "benchmark" : "com.pivovarit.gatherers.FilterByIndexBenchmark.zipWithIndexThenFilter", 61 | "mode" : "thrpt", 62 | "threads" : 1, 63 | "forks" : 1, 64 | "jvm" : "/Users/pivovarit/Library/Java/JavaVirtualMachines/openjdk-23/Contents/Home/bin/java", 65 | "jvmArgs" : [ 66 | "--enable-preview", 67 | "-javaagent:/Users/pivovarit/Applications/IntelliJ IDEA Ultimate.app/Contents/lib/idea_rt.jar=57563:/Users/pivovarit/Applications/IntelliJ IDEA Ultimate.app/Contents/bin", 68 | "-Dfile.encoding=UTF-8", 69 | "-Dsun.stdout.encoding=UTF-8", 70 | "-Dsun.stderr.encoding=UTF-8" 71 | ], 72 | "jdkVersion" : "23", 73 | "vmName" : "OpenJDK 64-Bit Server VM", 74 | "vmVersion" : "23+37-2369", 75 | "warmupIterations" : 3, 76 | "warmupTime" : "10 s", 77 | "warmupBatchSize" : 1, 78 | "measurementIterations" : 5, 79 | "measurementTime" : "10 s", 80 | "measurementBatchSize" : 1, 81 | "primaryMetric" : { 82 | "score" : 17.311580632178348, 83 | "scoreError" : 0.3356105307010133, 84 | "scoreConfidence" : [ 85 | 16.975970101477333, 86 | 17.647191162879363 87 | ], 88 | "scorePercentiles" : { 89 | "0.0" : 17.158261528505264, 90 | "50.0" : 17.33638831156214, 91 | "90.0" : 17.372805550060715, 92 | "95.0" : 17.372805550060715, 93 | "99.0" : 17.372805550060715, 94 | "99.9" : 17.372805550060715, 95 | "99.99" : 17.372805550060715, 96 | "99.999" : 17.372805550060715, 97 | "99.9999" : 17.372805550060715, 98 | "100.0" : 17.372805550060715 99 | }, 100 | "scoreUnit" : "ops/s", 101 | "rawData" : [ 102 | [ 103 | 17.158261528505264, 104 | 17.33638831156214, 105 | 17.33402194561894, 106 | 17.356425825144683, 107 | 17.372805550060715 108 | ] 109 | ] 110 | }, 111 | "secondaryMetrics" : { 112 | } 113 | } 114 | ] 115 | 116 | 117 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2024] [Grzegorz Piwowarek] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com/pivovarit/gatherers/MoreGatherers.java: -------------------------------------------------------------------------------- 1 | package com.pivovarit.gatherers; 2 | 3 | import java.util.Iterator; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Objects; 7 | import java.util.function.BiFunction; 8 | import java.util.function.BiPredicate; 9 | import java.util.function.Function; 10 | import java.util.stream.Collector; 11 | import java.util.stream.Collectors; 12 | import java.util.stream.Gatherer; 13 | import java.util.stream.Stream; 14 | 15 | /// Contains various [java.util.stream.Gatherer] implementations expanding [java.util.stream.Stream] API functionality 16 | public final class MoreGatherers { 17 | 18 | private MoreGatherers() { 19 | } 20 | 21 | /** 22 | * Creates a {@link Gatherer} that gathers the last {@code n} elements. 23 | * 24 | * @param the type of the input elements 25 | * @param n the number of last elements to gather 26 | * 27 | * @return a {@link Gatherer} that collects the last {@code n} elements 28 | */ 29 | public static Gatherer last(int n) { 30 | return LastGatherer.size(n); 31 | } 32 | 33 | /** 34 | * Creates a {@link Gatherer} that gathers every {@code n}-th element. 35 | * 36 | * @param the type of the input elements 37 | * @param n the interval at which elements are gathered 38 | * 39 | * @return a {@link Gatherer} that samples elements every {@code n}-th element 40 | */ 41 | public static Gatherer sampling(int n) { 42 | return new SamplingGatherer<>(n); 43 | } 44 | 45 | /** 46 | * Creates a {@link Gatherer} that filters elements to ensure that only distinct elements 47 | * remain, based on a key extracted by the given {@code keyExtractor}, keeping the last 48 | * occurrence of each distinct key. 49 | * 50 | * @param the type of the input elements 51 | * @param the type of the key extracted from the input elements 52 | * @param keyExtractor the function used to extract the key for distinguishing elements 53 | * 54 | * @return a {@link Gatherer} that collects distinct elements by key, keeping the last occurrence 55 | */ 56 | public static Gatherer distinctByKeepLast(Function keyExtractor) { 57 | return new DistinctByKeepLastGatherer<>(keyExtractor); 58 | } 59 | 60 | /** 61 | * Creates a {@link Gatherer} that filters elements to ensure that only distinct elements 62 | * remain, based on a key extracted by the given {@code keyExtractor}, keeping the first 63 | * occurrence of each distinct key. 64 | * 65 | * @param the type of the input elements 66 | * @param the type of the key extracted from the input elements 67 | * @param keyExtractor the function used to extract the key for distinguishing elements 68 | * 69 | * @return a {@link Gatherer} that collects distinct elements by key, keeping the first occurrence 70 | */ 71 | public static Gatherer distinctBy(Function keyExtractor) { 72 | return new DistinctByGatherer<>(keyExtractor); 73 | } 74 | 75 | /** 76 | * Creates a {@link Gatherer} that gathers distinct consecutive elements. 77 | * Elements are considered distinct if they are different from the previously gathered element. 78 | * 79 | * @param the type of the input elements 80 | * 81 | * @return a {@link Gatherer} that collects distinct consecutive elements 82 | */ 83 | public static Gatherer distinctUntilChanged() { 84 | return distinctUntilChanged(Function.identity()); 85 | } 86 | 87 | /** 88 | * Creates a {@link Gatherer} that gathers distinct consecutive elements 89 | * based on a key extracted by the given {@code keyExtractor}. 90 | * Elements are considered distinct if the extracted key is different from the previous key. 91 | * 92 | * @param the type of the input elements 93 | * @param the type of the key extracted from the input elements 94 | * @param keyExtractor the function used to extract the key for distinguishing elements 95 | * 96 | * @return a {@link Gatherer} that collects distinct consecutive elements by key 97 | */ 98 | public static Gatherer distinctUntilChanged(Function keyExtractor) { 99 | Objects.requireNonNull(keyExtractor, "keyExtractor can't be null"); 100 | return new DistinctUntilChangedGatherer<>(keyExtractor); 101 | } 102 | 103 | /** 104 | * Creates a {@link Gatherer} that zips elements of type {@code T1} with elements from 105 | * another {@link Stream} of type {@code T2}. The resulting pair is returned as a {@link Map.Entry}. 106 | * 107 | * @param the type of the first stream elements 108 | * @param the type of the second stream elements 109 | * @param other the other stream to zip with 110 | * 111 | * @return a {@link Gatherer} that pairs elements from the two streams 112 | */ 113 | public static Gatherer> zip(Stream other) { 114 | Objects.requireNonNull(other, "other can't be null"); 115 | return zip(other.iterator()); 116 | } 117 | 118 | /** 119 | * Creates a {@link Gatherer} that zips elements of type {@code T1} with elements from 120 | * another {@link Stream} of type {@code T2}, and combines them using the provided {@code mapper}. 121 | * 122 | * @param the type of the first stream elements 123 | * @param the type of the second stream elements 124 | * @param the type of the result produced by the {@code mapper} 125 | * @param other the other stream to zip with 126 | * @param mapper the function that combines elements from both streams 127 | * 128 | * @return a {@link Gatherer} that pairs elements from the two streams using the {@code mapper} 129 | */ 130 | public static Gatherer zip(Stream other, BiFunction mapper) { 131 | Objects.requireNonNull(other, "other can't be null"); 132 | return zip(other.iterator(), mapper); 133 | } 134 | 135 | /** 136 | * Creates a {@link Gatherer} that zips elements of type {@code T1} with elements from 137 | * another {@link Iterable} of type {@code T2}. The resulting pair is returned as a {@link Map.Entry}. 138 | * 139 | * @param the type of the first iterable elements 140 | * @param the type of the second iterable elements 141 | * @param other the other iterable to zip with 142 | * 143 | * @return a {@link Gatherer} that pairs elements from the two iterables 144 | */ 145 | public static Gatherer> zipWithIterable(Iterable other) { 146 | Objects.requireNonNull(other, "other can't be null"); 147 | return zip(other.iterator()); 148 | } 149 | 150 | /** 151 | * Creates a {@link Gatherer} that zips elements of type {@code T1} with elements from 152 | * another {@link Iterable} of type {@code T2}, and combines them using the provided {@code mapper}. 153 | * 154 | * @param the type of the first iterable elements 155 | * @param the type of the second iterable elements 156 | * @param the type of the result produced by the {@code mapper} 157 | * @param other the other iterable to zip with 158 | * @param mapper the function that combines elements from both iterables 159 | * 160 | * @return a {@link Gatherer} that pairs elements from the two iterables using the {@code mapper} 161 | */ 162 | public static Gatherer zipWithIterable(Iterable other, BiFunction mapper) { 163 | Objects.requireNonNull(other, "other can't be null"); 164 | Objects.requireNonNull(mapper, "mapper can't be null"); 165 | return zip(other.iterator(), mapper); 166 | } 167 | 168 | /** 169 | * Creates a {@link Gatherer} that zips elements of type {@code T1} with elements from 170 | * another {@link Iterator} of type {@code T2}. The resulting pair is returned as a {@link Map.Entry}. 171 | * 172 | * @param the type of the first iterator elements 173 | * @param the type of the second iterator elements 174 | * @param iterator the iterator to zip with 175 | * 176 | * @return a {@link Gatherer} that pairs elements from the two iterators 177 | */ 178 | public static Gatherer> zip(Iterator iterator) { 179 | return zip(iterator, Map::entry); 180 | } 181 | 182 | /** 183 | * Creates a {@link Gatherer} that zips elements of type {@code T1} with elements from 184 | * another {@link Iterator} of type {@code T2}, and combines them using the provided {@code mapper}. 185 | * 186 | * @param the type of the first iterator elements 187 | * @param the type of the second iterator elements 188 | * @param the type of the result produced by the {@code mapper} 189 | * @param iterator the iterator to zip with 190 | * @param mapper the function that combines elements from both iterators 191 | * 192 | * @return a {@link Gatherer} that pairs elements from the two iterators using the {@code mapper} 193 | */ 194 | public static Gatherer zip(Iterator iterator, BiFunction mapper) { 195 | return new ZipIteratorGatherer<>(iterator, mapper); 196 | } 197 | 198 | /** 199 | * Creates a {@link Gatherer} that zips elements with their corresponding index, 200 | * using the given {@code mapper} to produce the final result. 201 | * 202 | * @param the type of the input elements 203 | * @param the type of the result produced by the {@code mapper} 204 | * @param mapper the function that combines an index and an element to produce the result 205 | * 206 | * @return a {@link Gatherer} that pairs elements with their corresponding index using the {@code mapper} 207 | */ 208 | public static Gatherer zipWithIndex(BiFunction mapper) { 209 | return new ZipWithIndexMappingGatherer<>(mapper); 210 | } 211 | 212 | /** 213 | * Creates a {@link Gatherer} that zips elements with their corresponding index, 214 | * producing a {@link Map.Entry} where the key is the index and the value is the element. 215 | * 216 | * @param the type of the input elements 217 | * 218 | * @return a {@link Gatherer} that pairs elements with their corresponding index 219 | */ 220 | public static Gatherer> zipWithIndex() { 221 | return new ZipWithIndexGatherer<>(); 222 | } 223 | 224 | /** 225 | * Creates a {@link Gatherer} that collects elements into sliding windows of a specified size. 226 | * Each window captures a subset of elements from the input, and windows slide by a specified step. 227 | *

228 | * This {@link Gatherer} extends {@link java.util.stream.Gatherers#windowSliding(int)} by allowing to customize the step 229 | * 230 | *

For example, if the window size is 3 and the step is 1, the gatherer will collect 231 | * windows of size 3, sliding by 1 element at a time. This means each subsequent window overlaps 232 | * with the previous one by two elements.

233 | * 234 | *

Common use cases include moving averages, trend analysis, and any scenario requiring 235 | * overlapping or rolling window operations on a list of elements.

236 | * 237 | * @param the type of elements in the input and output list 238 | * @param windowSize the size of each window (must be a positive integer) 239 | * @param step the number of elements to slide the window by (must be a positive integer) 240 | * 241 | * @return a {@link Gatherer} that collects elements into sliding windows 242 | * 243 | * @throws IllegalArgumentException if {@code windowSize} is less than one or {@code step} is less than zero, or greater than {@code windowSize} 244 | */ 245 | public static Gatherer> windowSliding(int windowSize, int step) { 246 | return new WindowSlidingGatherer<>(windowSize, step); 247 | } 248 | 249 | /** 250 | * Creates a {@link Gatherer} that filters elements based on their index and value using a given {@link BiPredicate}. 251 | * The provided {@code BiPredicate} is applied to each element of the source, along with its corresponding index 252 | * (starting from 0). Only the elements that satisfy the predicate (i.e., for which the predicate returns {@code true}) 253 | * are retained. 254 | *

255 | * The same result can be achieved by using {@code zipWithIndex()}, {@code filter()}, and {@code map()}. 256 | * However, this method is significantly faster because it avoids the intermediate steps and directly filters 257 | * elements based on their index. 258 | * 259 | * @param the type of elements to be filtered 260 | * @param predicate a {@link BiPredicate} that takes the index and element as input, and returns {@code true} to retain 261 | * the element, or {@code false} to exclude it 262 | * 263 | * @return a {@link Gatherer} that applies the given filter based on element index and value 264 | */ 265 | public static Gatherer filteringByIndex(BiPredicate predicate) { 266 | return new FilterByIndexGatherer<>(predicate); 267 | } 268 | 269 | /** 270 | * Creates a {@link Gatherer} that groups elements based on a key extracted by the given {@code classifier}. 271 | * 272 | * @param classifier the function used to extract the key for grouping elements 273 | * @param collector the {@link Collector} used to accumulate the elements of each group 274 | * @param the type of the input elements 275 | * @param the type of the key extracted from the input elements 276 | * @param the type of the result of the collector 277 | * 278 | * @return a {@link Gatherer} that groups elements based on the extracted key 279 | */ 280 | public static Gatherer> groupingBy(Function classifier, Collector collector) { 281 | return new GroupingByGatherer<>(classifier, collector); 282 | } 283 | 284 | /** 285 | * Creates a {@link Gatherer} that groups elements based on a key extracted by the given {@code classifier}. 286 | * 287 | * @param classifier the function used to extract the key for grouping elements 288 | * @param the type of the input elements 289 | * @param the type of the key extracted from the input elements 290 | * 291 | * @return a {@link Gatherer} that groups elements based on the extracted key 292 | */ 293 | public static Gatherer>> groupingBy(Function classifier) { 294 | return groupingBy(classifier, Collectors.toList()); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Apache License, Version 2.0 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | repo 9 | 10 | 11 | 12 | 4.0.0 13 | 14 | com.pivovarit 15 | more-gatherers 16 | 0.1.1-SNAPSHOT 17 | https://github.com/pivovarit/more-gatherers 18 | 19 | jar 20 | 21 | more-gatherers 22 | Missing Stream API functionality you always longed for - provided via Stream API Gatherers 23 | 24 | 25 | https://github.com/pivovarit/more-gatherers 26 | scm:git:git@github.com:pivovarit/more-gatherers.git 27 | scm:git:git@github.com:pivovarit/more-gatherers.git 28 | HEAD 29 | 30 | 31 | 32 | UTF-8 33 | 3.12.0 34 | 6.0.1 35 | 3.27.6 36 | 1.37 37 | default 38 | 39 | 40 | 41 | deploy 42 | ${project.artifactId}-${project.version} 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-compiler-plugin 48 | 3.14.1 49 | 50 | 24 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-jar-plugin 57 | 3.5.0 58 | 59 | 60 | 61 | true 62 | 63 | 64 | ${maven.build.timestamp} 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-resources-plugin 73 | 3.4.0 74 | 75 | UTF-8 76 | 77 | 78 | 79 | 80 | maven-surefire-plugin 81 | 3.5.4 82 | 83 | 84 | 85 | org.apache.felix 86 | maven-bundle-plugin 87 | 6.0.0 88 | true 89 | 90 | 91 | 92 | org.apache.maven.plugins 93 | maven-release-plugin 94 | 3.3.1 95 | 96 | v@{project.version} 97 | forked-path 98 | false 99 | ${arguments} -Psonatype-oss-release 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | GitHub 108 | https://github.com/pivovarit/more-gatherers/issues 109 | 110 | 111 | 112 | 113 | Grzegorz Piwowarek 114 | gpiwowarek@gmail.com 115 | https://4comprehension.com 116 | 117 | 118 | 119 | 120 | 121 | sonatype-oss-release 122 | 123 | 124 | 125 | org.apache.maven.plugins 126 | maven-source-plugin 127 | 3.4.0 128 | 129 | 130 | attach-sources 131 | 132 | jar-no-fork 133 | 134 | 135 | 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-javadoc-plugin 140 | 3.12.0 141 | 142 | 143 | attach-javadocs 144 | 145 | jar 146 | 147 | 148 | 149 | 150 | 24 151 | 152 | 153 | 154 | org.apache.maven.plugins 155 | maven-gpg-plugin 156 | 3.2.8 157 | 158 | 159 | sign-artifacts 160 | verify 161 | 162 | sign 163 | 164 | 165 | 166 | --pinentry-mode 167 | loopback 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | org.apache.maven.plugins 176 | maven-deploy-plugin 177 | 3.1.4 178 | 179 | true 180 | 181 | 182 | 183 | org.sonatype.central 184 | central-publishing-maven-plugin 185 | 0.9.0 186 | true 187 | 188 | central 189 | https://central.sonatype.com/repository/maven-snapshots/ 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | jmh 199 | 200 | 201 | 202 | org.codehaus.mojo 203 | exec-maven-plugin 204 | 3.6.2 205 | 206 | 207 | process-sources 208 | 209 | java 210 | 211 | 212 | true 213 | org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator 214 | 215 | ${project.basedir}/target/classes/ 216 | ${project.basedir}/target/generated-sources/jmh/ 217 | ${project.basedir}/target/classes/ 218 | ${jmh.generator} 219 | 220 | 221 | 222 | 223 | 224 | 225 | org.openjdk.jmh 226 | jmh-generator-bytecode 227 | ${jmh.version} 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | pitest 237 | 238 | 239 | 240 | org.pitest 241 | pitest-maven 242 | 1.22.0 243 | 244 | 245 | *ArchitectureTest 246 | 247 | 248 | 249 | 250 | org.pitest 251 | pitest-junit5-plugin 252 | 1.2.3 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | doclint 262 | 263 | 264 | 265 | org.apache.maven.plugins 266 | maven-javadoc-plugin 267 | ${maven.javadoc.plugin.version} 268 | 269 | 270 | attach-javadocs 271 | 272 | jar 273 | 274 | 275 | 276 | 277 | true 278 | true 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | org.jspecify 289 | jspecify 290 | 1.0.0 291 | 292 | 293 | org.openjdk.jmh 294 | jmh-core 295 | ${jmh.version} 296 | test 297 | 298 | 299 | org.openjdk.jmh 300 | jmh-generator-annprocess 301 | ${jmh.version} 302 | test 303 | 304 | 305 | org.slf4j 306 | slf4j-simple 307 | 2.0.17 308 | test 309 | 310 | 311 | org.junit.jupiter 312 | junit-jupiter-engine 313 | ${junit.version} 314 | test 315 | 316 | 317 | org.assertj 318 | assertj-core 319 | ${assertj.version} 320 | test 321 | 322 | 323 | com.tngtech.archunit 324 | archunit-junit5-api 325 | 1.4.1 326 | test 327 | 328 | 329 | com.tngtech.archunit 330 | archunit-junit5-engine 331 | 1.4.1 332 | test 333 | 334 | 335 | 336 | 337 | 338 | sonatype-nexus-staging 339 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 340 | 341 | 342 | sonatype-nexus-snapshots 343 | https://oss.sonatype.org/content/repositories/snapshots/ 344 | 345 | 346 | 347 | --------------------------------------------------------------------------------