diff --git a/CHANGELOG.md b/CHANGELOG.md index 30110c4..122d305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ### 0.8.0 -+ TBD ++ Add support for `orElse()` and `orElseEmpty()` on size-based gatherers to provide a non-exceptional output stream ### 0.7.0 + Use greedy integrators where possible (Fixes #57) diff --git a/README.md b/README.md index d3a6a7e..39fc785 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ implementation("com.ginsberg:gatherers4j:0.8.0") | `distinctBy(fn)` | Emit only distinct elements from the stream, as measured by `fn` | | `dropLast(n)` | Keep all but the last `n` elements of the stream | | `filterWithIndex(predicate)` | Filter the stream with the given `predicate`, which takes an `element` and its `index` | -| `grouping()` | Group consecute identical elements into lists | +| `grouping()` | Group consecutive identical elements into lists | | `groupingBy(fn)` | Group consecutive elements that are identical according to `fn` into lists | | `interleave(iterable)` | Creates a stream of alternating objects from the input stream and the argument iterable | | `interleave(iterator)` | Creates a stream of alternating objects from the input stream and the argument iterator | @@ -177,6 +177,34 @@ Stream.of("A").gather(Gatherers4j.sizeExactly(3)).toList(); // IllegalStateException ``` +#### Replace stream when size check fails + +This applies to `sizeExactly()`, `sizeLessThan()`, `sizeLessThanOrEqualTo()`, `sizeGreaterThan()`, and `sizeGreaterThanOrEqualTo()`. + +Note, this needs a type witness due to how Java generics work. + +```java +Stream.of("A") + .gather(Gatherers4j.sizeExactly(2).orElse(() -> Stream.of("A", "B"))) + .toList(); + +// ["A", "B"] +``` + +#### Return an empty stream when size check fails + +This applies to `sizeExactly()`, `sizeLessThan()`, `sizeLessThanOrEqualTo()`, `sizeGreaterThan()`, and `sizeGreaterThanOrEqualTo()`. + +Note, this needs a type witness due to how Java generics work. + +```java +Stream.of("A") + .gather(Gatherers4j.sizeExactly(2).orElseEmpty()) + .toList(); + +// [] +``` + #### Ensure the stream is greater than `n` elements long ```java diff --git a/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java b/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java index 093016a..9ec0702 100644 --- a/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java +++ b/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java @@ -429,7 +429,7 @@ public static LastGatherer last(final int count) { /// @return A non-null `SizeGatherer` /// @throws IllegalStateException when the input stream is not exactly `size` elements long public static SizeGatherer sizeExactly(final long size) { - return new SizeGatherer<>(SizeGatherer.Operation.Equal, size); + return new SizeGatherer<>(SizeGatherer.Operation.Equals, size); } /// Ensure the input stream is greater than `size` elements long, and emit all elements if so. diff --git a/src/main/java/com/ginsberg/gatherers4j/SizeGatherer.java b/src/main/java/com/ginsberg/gatherers4j/SizeGatherer.java index c7ef720..1013bf3 100644 --- a/src/main/java/com/ginsberg/gatherers4j/SizeGatherer.java +++ b/src/main/java/com/ginsberg/gatherers4j/SizeGatherer.java @@ -23,12 +23,16 @@ import java.util.function.BiConsumer; import java.util.function.Supplier; import java.util.stream.Gatherer; +import java.util.stream.Stream; + +import static com.ginsberg.gatherers4j.GathererUtils.mustNotBeNull; public class SizeGatherer implements Gatherer, INPUT> { private final long targetSize; private final Operation operation; + private Supplier> orElse; SizeGatherer(final Operation operation, final long targetSize) { if (targetSize < 0) { @@ -36,13 +40,45 @@ public class SizeGatherer } this.operation = operation; this.targetSize = targetSize; + this.orElse = () -> { + throw new IllegalStateException("Invalid stream size: wanted " + operation.name() + " " + targetSize); + }; + } + + /// When the current stream does not have the correct length, call the given + /// `Supplier>` to produce an output instead of throwing an exception (the default behavior). + /// + /// Note: You will need a type witness when using this: + /// + /// `source.gather(Gatherers4j.sizeExactly(2).orElse(() -> Stream.of("A", "B")))` + /// + /// @param orElse - A non-null `Supplier`, the results of which will be used instead of the input stream. + public SizeGatherer orElse(final Supplier> orElse) { + mustNotBeNull(orElse, "The orElse function must not be null"); + this.orElse = orElse; + return this; + } + + /// When the current stream does not have the correct length, produce an empty stream instead of throwing + /// an exception (the default behavior). + /// + /// Note: You will need a type witness when using this: + /// + /// `source.gather(Gatherers4j.sizeExactly(2).orElseEmpty())` + /// + public SizeGatherer orElseEmpty() { + this.orElse = Stream::empty; + return this; } @Override public BiConsumer, Downstream> finisher() { return (state, downstream) -> { - operation.checkFinalLength(state.elements.size(), targetSize); - state.elements.forEach(downstream::push); + if (!state.failed && operation.accept(state.elements.size(), targetSize)) { + state.elements.forEach(downstream::push); + } else { + orElse.get().forEach(downstream::push); + } }; } @@ -54,74 +90,63 @@ public Supplier> initializer() { @Override public Integrator, INPUT, INPUT> integrator() { return (state, element, downstream) -> { - operation.tryAccept(state.elements.size() + 1, targetSize); - state.elements.add(element); - return !downstream.isRejecting(); + if (operation.tryAccept(state.elements.size() + 1, targetSize)) { + state.elements.add(element); + } else { + state.failed = true; + } + return !state.failed || !downstream.isRejecting(); }; } enum Operation { - Equal { + Equals { @Override - void tryAccept(long length, long target) { - if(length > target) { - fail(target); - } + boolean tryAccept(long length, long target) { + return length <= target; } @Override - void checkFinalLength(long length, long target) { - if (length != target) { - fail(target); - } - } - - void fail(long target) { - throw new IllegalStateException("Stream length must be equal to " + target); + boolean accept(long length, long target) { + return length == target; } }, GreaterThan { @Override - void checkFinalLength(long length, long target) { - if (length <= target) { - throw new IllegalStateException("Stream length must be greater than " + target); - } + boolean accept(long length, long target) { + return length > target; } }, GreaterThanOrEqualTo { @Override - void checkFinalLength(long length, long target) { - if (length < target) { - throw new IllegalStateException("Stream length must be greater than or equal to " + target); - } + boolean accept(long length, long target) { + return length >= target; } }, LessThan { @Override - void tryAccept(long length, long target) { - if(length >= target) { - throw new IllegalStateException("Stream length must be less than " + target); - } + boolean tryAccept(long length, long target) { + return length < target; } }, LessThanOrEqualTo { @Override - void tryAccept(long length, long target) { - if(length > target) { - throw new IllegalStateException("Stream length must be less than or equal to " + target); - } + boolean tryAccept(long length, long target) { + return length <= target; } }; - void checkFinalLength(long length, long target) { - // Empty implementation + boolean accept(long length, long target) { + return true; } - void tryAccept(long length, long target){ - // Empty implementation + + boolean tryAccept(long length, long target) { + return true; } } public static class State { + boolean failed = false; final List elements = new ArrayList<>(); } } diff --git a/src/test/java/com/ginsberg/gatherers4j/SizeGathererTest.java b/src/test/java/com/ginsberg/gatherers4j/SizeGathererTest.java index 35b4f8e..6c6bcd8 100644 --- a/src/test/java/com/ginsberg/gatherers4j/SizeGathererTest.java +++ b/src/test/java/com/ginsberg/gatherers4j/SizeGathererTest.java @@ -30,6 +30,43 @@ class SizeGathererTest { @Nested class Common { + @Test + void canReplaceStreamEmpty() { + // Arrange + final Stream input = Stream.of("A"); + + // Act + final List output = input + .gather(Gatherers4j.sizeExactly(2).orElseEmpty()) + .toList(); + + // Assert + assertThat(output).isEmpty(); + } + + @Test + void canReplaceStreamNonempty() { + // Arrange + final Stream input = Stream.of("A"); + + // Act + final List output = input + .gather(Gatherers4j.sizeExactly(2).orElse(() -> Stream.of("A", "B"))) + .toList(); + + // Assert + assertThat(output).containsExactly("A", "B"); + } + + + @Test + void orElseMustNotBeNull() { + //noinspection DataFlowIssue + assertThatThrownBy(() -> + Stream.empty().gather(Gatherers4j.sizeExactly(2).orElse(null)).toList() + ).isInstanceOf(IllegalArgumentException.class); + } + @Test void targetSizeMustNotBeNegative() { assertThatThrownBy(() -> @@ -39,19 +76,19 @@ void targetSizeMustNotBeNegative() { } @Nested - class Equal { + class Equals { @Test - void doesNotEmitUnderTarget() { + void doesNotEmitOverTarget() { assertThatThrownBy(() -> - Stream.of("A").gather(Gatherers4j.sizeExactly(2)).toList() + Stream.of("A", "B", "C").gather(Gatherers4j.sizeExactly(2)).toList() ).isInstanceOf(IllegalStateException.class); } @Test - void doesNotEmitOverTarget() { + void doesNotEmitUnderTarget() { assertThatThrownBy(() -> - Stream.of("A", "B", "C").gather(Gatherers4j.sizeExactly(2)).toList() + Stream.of("A").gather(Gatherers4j.sizeExactly(2)).toList() ).isInstanceOf(IllegalStateException.class); } @@ -72,16 +109,16 @@ void emitsAtTarget() { class GreaterThan { @Test - void doesNotEmitUnderTarget() { + void doesNotEmitAtTarget() { assertThatThrownBy(() -> - Stream.of("A").gather(Gatherers4j.sizeGreaterThan(2)).toList() + Stream.of("A", "B").gather(Gatherers4j.sizeGreaterThan(2)).toList() ).isInstanceOf(IllegalStateException.class); } @Test - void doesNotEmitAtTarget() { + void doesNotEmitUnderTarget() { assertThatThrownBy(() -> - Stream.of("A", "B").gather(Gatherers4j.sizeGreaterThan(2)).toList() + Stream.of("A").gather(Gatherers4j.sizeGreaterThan(2)).toList() ).isInstanceOf(IllegalStateException.class); } @@ -137,18 +174,6 @@ void emitsOverTarget() { @Nested class LessThan { - @Test - void emitsUnderTarget() { - // Arrange - final Stream input = Stream.of("A"); - - // Act - final List output = input.gather(Gatherers4j.sizeLessThan(2)).toList(); - - // Assert - assertThat(output).containsExactly("A"); - } - @Test void doesNotEmitAtTarget() { assertThatThrownBy(() -> @@ -162,11 +187,6 @@ void doesNotEmitOverTarget() { Stream.of("A", "B", "C").gather(Gatherers4j.sizeLessThan(2)).toList() ).isInstanceOf(IllegalStateException.class); } - } - - - @Nested - class LessThanOrEqualTo { @Test void emitsUnderTarget() { @@ -174,11 +194,23 @@ void emitsUnderTarget() { final Stream input = Stream.of("A"); // Act - final List output = input.gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList(); + final List output = input.gather(Gatherers4j.sizeLessThan(2)).toList(); // Assert assertThat(output).containsExactly("A"); } + } + + + @Nested + class LessThanOrEqualTo { + + @Test + void doesNotEmitOverTarget() { + assertThatThrownBy(() -> + Stream.of("A", "B", "C").gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList() + ).isInstanceOf(IllegalStateException.class); + } @Test void emitsAtTarget() { @@ -193,10 +225,15 @@ void emitsAtTarget() { } @Test - void doesNotEmitOverTarget() { - assertThatThrownBy(() -> - Stream.of("A", "B", "C").gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList() - ).isInstanceOf(IllegalStateException.class); + void emitsUnderTarget() { + // Arrange + final Stream input = Stream.of("A"); + + // Act + final List output = input.gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList(); + + // Assert + assertThat(output).containsExactly("A"); } } } \ No newline at end of file