From aa8396f8a89f6fe2d8a063519059e42293990df4 Mon Sep 17 00:00:00 2001 From: Todd Date: Fri, 1 Nov 2024 17:08:19 -0400 Subject: [PATCH] #48: Account for stream and argument not being the same length + I am not wild about having to use a type witness, but I'm fine with adding it as I think this use case is pretty niche and anybody who needs it could probably tolerate a type witness. --- .../com/ginsberg/gatherers4j/Gatherers4j.java | 6 +- .../ginsberg/gatherers4j/ZipWithGatherer.java | 84 ++++++++++++- .../gatherers4j/ZipWithGathererTest.java | 119 ++++++++++++++++-- 3 files changed, 192 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java b/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java index 72edf09..249e1bc 100644 --- a/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java +++ b/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java @@ -318,7 +318,7 @@ public static IndexingGatherer withIndex() { /// and the argument collection /// /// @param other A non-null iterable to zip with - public static Gatherer> zipWith(final Iterable other) { + public static ZipWithGatherer zipWith(final Iterable other) { return new ZipWithGatherer<>(other); } @@ -326,7 +326,7 @@ public static Gatherer> zipWith /// and the argument iterator /// /// @param other A non-null iterator to zip with - public static Gatherer> zipWith(final Iterator other) { + public static ZipWithGatherer zipWith(final Iterator other) { return new ZipWithGatherer<>(other); } @@ -334,7 +334,7 @@ public static Gatherer> zipWith /// and the argument stream /// /// @param other A non-null stream to zip with - public static Gatherer> zipWith(final Stream other) { + public static ZipWithGatherer zipWith(final Stream other) { return new ZipWithGatherer<>(other); } diff --git a/src/main/java/com/ginsberg/gatherers4j/ZipWithGatherer.java b/src/main/java/com/ginsberg/gatherers4j/ZipWithGatherer.java index d0383b0..2b0ab49 100644 --- a/src/main/java/com/ginsberg/gatherers4j/ZipWithGatherer.java +++ b/src/main/java/com/ginsberg/gatherers4j/ZipWithGatherer.java @@ -18,6 +18,8 @@ import java.util.Iterator; import java.util.Spliterator; +import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.stream.Gatherer; import java.util.stream.Stream; @@ -25,6 +27,8 @@ public class ZipWithGatherer implements Gatherer> { private final Spliterator otherSpliterator; + private Function sourceWhenArgumentLonger; + private Function argumentWhenSourceLonger; ZipWithGatherer(final Iterable other) { mustNotBeNull(other, "Other iterable must not be null"); @@ -34,7 +38,7 @@ public class ZipWithGatherer implements Gatherer other) { mustNotBeNull(other, "Other iterator must not be null"); final Iterable iterable = () -> other; - otherSpliterator = iterable.spliterator(); + otherSpliterator = (iterable).spliterator(); } ZipWithGatherer(final Stream other) { @@ -42,11 +46,81 @@ public class ZipWithGatherer implements GathererzipWith(right).argumentWhenSourceLonger(String::length))` + /// + /// @param mappingFunction A non-null function which takes a possibly null `` + /// and emits a possibly null `` + public ZipWithGatherer argumentWhenSourceLonger(final Function mappingFunction) { + mustNotBeNull(mappingFunction, "Mapping function must not be null, use nullArgumentWhenSourceLonger() to insert nulls"); + argumentWhenSourceLonger = mappingFunction; + return this; + } + + /// When the source stream runs out of elements before the argument `Iterable`, `Iterator` or `Stream` does, + /// use the result of the `function` provided for the remaining `FIRST` elements of each `Pair` + /// until the argument is exhausted. + /// + /// Note: You may need a type witness when using this: + /// + /// `source.gather(Gatherers4j.zipWith(right).sourceWhenArgumentLonger(String::valueOf))` + /// + /// @param mappingFunction A non-null function which takes a possibly null `` + /// and emits a possibly null `` + public ZipWithGatherer sourceWhenArgumentLonger(final Function mappingFunction) { + mustNotBeNull(mappingFunction, "Mapping function must not be null, use nullSourceWhenArgumentLonger() to insert nulls"); + sourceWhenArgumentLonger = mappingFunction; + return this; + } + + /// When the argument `Iterable`, `Iterator` or `Stream` runs out of elements before the source stream does, + /// use `null` for the remaining `SECOND` elements of each `Pair` until the source is exhausted. + /// + /// Note: You may need a type witness when using this: + /// + /// `source.gather(Gatherers4j.zipWith(right).nullArgumentWhenSourceLonger())` + /// + public ZipWithGatherer nullArgumentWhenSourceLonger() { + argumentWhenSourceLonger = _ -> null; + return this; + } + + /// When the source stream runs out of elements before the argument `Iterable`, `Iterator` or `Stream` does, + /// use `null` for the remaining `FIRST` elements of each `Pair` until the argument is exhausted. + /// + /// Note: You may need a type witness when using this: + /// + /// `source.gather(Gatherers4j.zipWith(right).nullSourceWhenArgumentLonger())` + public ZipWithGatherer nullSourceWhenArgumentLonger() { + sourceWhenArgumentLonger = _ -> null; + return this; + } + @Override public Integrator> integrator() { - return (_, element, downstream) -> otherSpliterator - .tryAdvance( - it -> downstream.push(new Pair<>(element, it)) - ) && !downstream.isRejecting(); + return (_, element, downstream) -> { + boolean advanced = otherSpliterator.tryAdvance(it -> downstream.push(new Pair<>(element, it))); + if (!advanced && argumentWhenSourceLonger != null) { + downstream.push(new Pair<>(element, argumentWhenSourceLonger.apply(element))); + } + return !downstream.isRejecting(); + }; + } + + @Override + public BiConsumer>> finisher() { + return (_, downstream) -> { + boolean downstreamIsRejecting = downstream.isRejecting(); + while (sourceWhenArgumentLonger != null && !downstreamIsRejecting) { + downstreamIsRejecting = !otherSpliterator.tryAdvance(arg -> + downstream.push(new Pair<>(sourceWhenArgumentLonger.apply(arg), arg)) + ); + } + }; } } diff --git a/src/test/java/com/ginsberg/gatherers4j/ZipWithGathererTest.java b/src/test/java/com/ginsberg/gatherers4j/ZipWithGathererTest.java index edb3622..dfa2be3 100644 --- a/src/test/java/com/ginsberg/gatherers4j/ZipWithGathererTest.java +++ b/src/test/java/com/ginsberg/gatherers4j/ZipWithGathererTest.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.Test; -import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.stream.Stream; @@ -50,22 +49,65 @@ void argumentStreamMustNotBeNull() { } @Test - void interleavingGathererThisEmpty() { + void argumentWhenSourceLongerFunctionMustNotBeNull() { + assertThatThrownBy(() -> Stream.of("A") + .gather(Gatherers4j.zipWith(List.of("A")).argumentWhenSourceLonger(null)).toList() + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void sourceWhenArgumentLongerFunctionMustNotBeNull() { + assertThatThrownBy(() -> Stream.of("A") + .gather(Gatherers4j.zipWith(List.of("A")).sourceWhenArgumentLonger(null)).toList() + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void zipWhenArgumentIsLongerFromFunction() { // Arrange - final Stream left = Stream.empty(); - final Stream right = Stream.of(1, 2, 3); + final Stream left = Stream.of("A"); + final Stream right = Stream.of(1, 2, 3, 4); // Act final List> output = left - .gather(Gatherers4j.zipWith(right)) + .gather(Gatherers4j.zipWith(right).sourceWhenArgumentLonger(String::valueOf)) .toList(); // Assert - assertThat(output).isEmpty(); + assertThat(output) + .hasSize(4) + .containsExactly( + new Pair<>("A", 1), + new Pair<>("2", 2), + new Pair<>("3", 3), + new Pair<>("4", 4) + ); } @Test - void zipGathererOtherEmpty() { + void zipWhenArgumentIsLongerNull() { + // Arrange + final Stream left = Stream.of("A"); + final Stream right = Stream.of(1, 2, 3, 4); + + // Act + final List> output = left + .gather(Gatherers4j.zipWith(right).nullSourceWhenArgumentLonger()) + .toList(); + + // Assert + assertThat(output) + .hasSize(4) + .containsExactly( + new Pair<>("A", 1), + new Pair<>(null, 2), + new Pair<>(null, 3), + new Pair<>(null, 4) + ); + } + + @Test + void zipWhenOtherIsEmpty() { // Arrange final Stream left = Stream.of("A", "B", "C"); final Stream right = Stream.empty(); @@ -80,10 +122,69 @@ void zipGathererOtherEmpty() { } @Test - void zipWithCollectionGatherer() { + void zipWhenSourceIsLongerFromFunction() { + // Arrange + final Stream left = Stream.of("A", "Bb", "Ccc", "Dddd"); + final Stream right = Stream.of(1); + + // Act + final List> output = left + .gather(Gatherers4j.zipWith(right).argumentWhenSourceLonger(String::length)) + .toList(); + + // Assert + assertThat(output) + .hasSize(4) + .containsExactly( + new Pair<>("A", 1), + new Pair<>("Bb", 2), + new Pair<>("Ccc", 3), + new Pair<>("Dddd", 4) + ); + } + + @Test + void zipWhenSourceIsLongerNull() { + // Arrange + final Stream left = Stream.of("A", "B", "C", "D"); + final Stream right = Stream.of(1); + + // Act + final List> output = left + .gather(Gatherers4j.zipWith(right).nullArgumentWhenSourceLonger()) + .toList(); + + // Assert + assertThat(output) + .hasSize(4) + .containsExactly( + new Pair<>("A", 1), + new Pair<>("B", null), + new Pair<>("C", null), + new Pair<>("D", null) + ); + } + + @Test + void zipWhenThisIsEmpty() { + // Arrange + final Stream left = Stream.empty(); + final Stream right = Stream.of(1, 2, 3); + + // Act + final List> output = left + .gather(Gatherers4j.zipWith(right)) + .toList(); + + // Assert + assertThat(output).isEmpty(); + } + + @Test + void zipWithIterableGatherer() { // Arrange final Stream left = Stream.of("A", "B", "C"); - final Collection right = List.of(1, 2, 3); + final Iterable right = List.of(1, 2, 3); // Act final List> output = left