From b4cffeb417ca04550c197918b616bb8a08641c27 Mon Sep 17 00:00:00 2001 From: Todd Date: Mon, 6 Jan 2025 10:16:45 -0500 Subject: [PATCH] Broader support for stream sizes + Rename `exactSize(n)` to `sizeExactly(n)` + Add `sizeGreaterThan(n)` + Add `sizeGreaterThanOrEqualTo(n)` + Add `sizeLessThan(n)` + Add `sizeLessThanOrEqualTo(n)` --- CHANGELOG.md | 4 +- README.md | 63 +++++- .../com/ginsberg/gatherers4j/Gatherers4j.java | 84 ++++++-- .../ginsberg/gatherers4j/SizeGatherer.java | 111 +++++++++-- .../gatherers4j/SizeGathererTest.java | 186 +++++++++++++++--- 5 files changed, 387 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef403f3..bea8650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,10 @@ + Implement `orderByFrequencyAscending()` and `orderByFrequencyDescending()` + Implement `movingProduct()` and `movingProductBy()` + Implement `movingSum()` and `movingSumBy()` -+ Functions, when used as arguments, should come last for consistency and to play nice with Kotlin (Fixes #64) + Remove `maxBy(fn)` and `minBy(fn)`, can be done with JDK methods trivially ++ Rename `exactSize()` to `sizeExactly()` ++ Implement `sizeLessThan`, `sizeLessThanOrEqualTo`, `sizeGreaterThan`, and `sizeGreaterThanOrEqualTo` ++ API Style - Functions, when used as arguments, should come last for consistency and to play nice with Kotlin (Fixes #64) ### 0.6.0 + Implement `dropLast(n)` diff --git a/README.md b/README.md index 2f6004d..63cbe67 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ implementation("com.ginsberg:gatherers4j:0.7.0") | `dedupeConsecutiveBy(fn)` | Remove consecutive duplicates from a stream as returned by `fn` | | `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 | -| `exactSize(n)` | Ensure the stream is exactly `n` elements long, or throw an `IllegalStateException` | | `filterWithIndex(predicate)` | Filter the stream with the given `predicate`, which takes an `element` and its `index` | | `grouping()` | Group consecute identical elements into lists | | `groupingBy(fn)` | Group consecutive elements that are identical according to `fn` into lists | @@ -51,6 +50,11 @@ implementation("com.ginsberg:gatherers4j:0.7.0") | `reverse()` | Reverse the order of the stream | | `shuffle()` | Shuffle the stream into a random order using the platform default `RandomGenerator` | | `shuffle(rg)` | Shuffle the stream into a random order using the specified `RandomGenerator` | +| `sizeExactly(n)` | Ensure the stream is exactly `n` elements long, or throw an `IllegalStateException` | +| `sizeGreaterThan(n)` | Ensure the stream is greater than `n` elements long, or throw an `IllegalStateException` | +| `sizeGreaterThanOrEqualTo(n)` | Ensure the stream is greater than or equal to `n` elements long, or throw an `IllegalStateException` | +| `sizeLessThan(n)` | Ensure the stream is less than `n` elements long, or throw an `IllegalStateException` | +| `sizeLessThanOrEqualTo(n)` | Ensure the stream is less than or equal to `n` elements long, or throw an `IllegalStateException` | | `throttle(amount, duration)` | Limit stream elements to `amount` elements over `duration`, pausing until a new `duration` period starts | | `withIndex()` | Maps all elements of the stream as-is along with their 0-based index | | `zipWith(iterable)` | Creates a stream of `Pair` objects whose values come from the input stream and argument iterable | @@ -165,11 +169,64 @@ Stream.of("A", "B", "C", "D", "E") ```java // Good -Stream.of("A", "B", "C").gather(Gatherers4j.exactSize(3)).toList(); +Stream.of("A", "B", "C").gather(Gatherers4j.sizeExactly(3)).toList(); // ["A", "B", "C"] // Bad -Stream.of("A").gather(Gatherers4j.exactSize(3)).toList(); +Stream.of("A").gather(Gatherers4j.sizeExactly(3)).toList(); +// IllegalStateException +``` + +#### Ensure the stream is greater than `n` elements long + +```java +// Good + +Stream.of("A", "B", "C").gather(Gatherers4j.sizeGreaterThan(2)).toList(); +// ["A", "B", "C"] + +// Bad +Stream.of("A", "B").gather(Gatherers4j.sizeGreaterThan(2)).toList(); +// IllegalStateException +``` + +#### Ensure the stream is greater than or equal to `n` elements long + +```java +// Good + +Stream.of("A", "B").gather(Gatherers4j.sizeGreaterThanOrEqualTo(2)).toList(); +// ["A", "B"] + +// Bad +Stream.of("A").gather(Gatherers4j.sizeGreaterThanOrEqualTo(2)).toList(); +// IllegalStateException +``` + +#### Ensure the stream is less than `n` elements long + +```java +// Good + +Stream.of("A").gather(Gatherers4j.sizeLessThan(2)).toList(); +// ["A"] + +// Bad +Stream.of("A", "B").gather(Gatherers4j.sizeLessThan(2)).toList(); +// IllegalStateException +``` + + +#### Ensure the stream is less than or equal to `n` elements long + +```java +// Good + +Stream.of("A", "B").gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList(); +// ["A", "B"] + +// Bad +Stream.of("A", "B", "C").gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList(); // IllegalStateException ``` diff --git a/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java b/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java index b9f3d05..093016a 100644 --- a/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java +++ b/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java @@ -86,22 +86,11 @@ public abstract class Gatherers4j { return new DropLastGatherer<>(count); } - /// Ensure the input stream is exactly `size` elements long, and emit all elements if so. - /// If not, throw an `IllegalStateException`. - /// - /// @param size Exact number of elements the stream must have - /// @param Type of elements in both the input and output streams - /// @return A non-null `SizeGatherer` - /// @throws IllegalStateException when the input stream is not exactly `size` elements long - public static SizeGatherer exactSize(final long size) { - return new SizeGatherer<>(size); - } - /// Filter a stream according to the given `predicate`, which takes both the item being examined, /// and its index. /// /// @param predicate A non-null `BiPredicate` where the `Long` is the zero-based index of the element - /// being filtered, and the `INPUT` is the element itself. + /// being filtered, and the `INPUT` is the element itself. /// @param Type of elements in the input stream /// @return A non-null `FilteringWithIndexGatherer` public static FilteringWithIndexGatherer filterWithIndex( @@ -181,7 +170,7 @@ public static LastGatherer last(final int count) { /// /// @param windowSize The trailing number of elements to multiply, must be greater than 1. /// @param mappingFunction A function to map `` objects to `BigDecimal`, the results of which will be used - /// in the moving product calculation + /// in the moving product calculation /// @param Type of elements in the input stream, to be remapped to `BigDecimal` by the `mappingFunction` /// @return A non-null `BigDecimalMovingProductGatherer` public static BigDecimalMovingProductGatherer movingProductBy( @@ -205,7 +194,7 @@ public static LastGatherer last(final int count) { /// /// @param windowSize The trailing number of elements to multiply, must be greater than 1. /// @param mappingFunction A function to map `` objects to `BigDecimal`, the results of which will be used - /// in the moving sum calculation + /// in the moving sum calculation /// @param Type of elements in the input stream, to be remapped to `BigDecimal` by the `mappingFunction` /// @return A non-null `BigDecimalMovingSumGatherer` public static BigDecimalMovingSumGatherer movingSumBy( @@ -287,7 +276,7 @@ public static LastGatherer last(final int count) { /// objects mapped from a `Stream` via a `mappingFunction`. /// /// @param mappingFunction A function to map `` objects to `BigDecimal`, the results of which will be used - /// in the standard deviation calculation + /// in the standard deviation calculation /// @param Type of elements in the input stream, to be remapped to `BigDecimal` by the `mappingFunction` /// @return A non-null `BigDecimalStandardDeviationGatherer` public static BigDecimalStandardDeviationGatherer runningPopulationStandardDeviationBy( @@ -310,7 +299,7 @@ public static LastGatherer last(final int count) { /// from a `Stream` via a `mappingFunction`. /// /// @param mappingFunction A function to map `` objects to `BigDecimal`, the results of which will be used - /// in the product calculation + /// in the product calculation /// @param Type of elements in the input stream, to be remapped to `BigDecimal` by the `mappingFunction` /// @return A non-null `BigDecimalProductGatherer` public static BigDecimalProductGatherer runningProductBy( @@ -333,7 +322,7 @@ public static LastGatherer last(final int count) { /// from a `Stream` via a `mappingFunction`. /// /// @param mappingFunction A function to map `` objects to `BigDecimal`, the results of which will be used - /// in the standard deviation calculation + /// in the standard deviation calculation /// @param Type of elements in the input stream, to be remapped to `BigDecimal` by the `mappingFunction` /// @return A non-null `BigDecimalStandardDeviationGatherer` public static BigDecimalStandardDeviationGatherer runningSampleStandardDeviationBy( @@ -356,7 +345,7 @@ public static LastGatherer last(final int count) { /// from a `Stream` via a `mappingFunction`. /// /// @param mappingFunction A function to map `` objects to `BigDecimal`, the results of which will be used - /// in the running sum calculation + /// in the running sum calculation /// @param Type of elements in the input stream, to be remapped to `BigDecimal` by the `mappingFunction` /// @return A non-null `BigDecimalSumGatherer` public static BigDecimalSumGatherer runningSumBy( @@ -399,7 +388,7 @@ public static LastGatherer last(final int count) { /// the given function. This is useful when paired with the `withOriginal` function. /// /// @param mappingFunction A function to map `` objects to `BigDecimal`, the results of which will be used - /// in the running average calculation + /// in the running average calculation /// @param Type of elements in the input stream, to be remapped to `BigDecimal` by the `mappingFunction` /// @return A non-null `BigDecimalSimpleAverageGatherer` public static BigDecimalSimpleAverageGatherer simpleRunningAverageBy( @@ -422,7 +411,7 @@ public static LastGatherer last(final int count) { /// /// @param windowSize The number of elements to average, must be greater than 1. /// @param mappingFunction A function to map `` objects to `BigDecimal`, the results of which will be used - /// in the moving average calculation + /// in the moving average calculation /// @param Type of elements in the input stream, to be remapped to `BigDecimal` by the `mappingFunction` /// @return A non-null `BigDecimalSimpleMovingAverageGatherer` public static BigDecimalSimpleMovingAverageGatherer simpleMovingAverageBy( @@ -432,6 +421,61 @@ public static LastGatherer last(final int count) { return new BigDecimalSimpleMovingAverageGatherer<>(windowSize, mappingFunction); } + /// Ensure the input stream is exactly `size` elements long, and emit all elements if so. + /// If not, throw an `IllegalStateException`. + /// + /// @param size Exact number of elements the stream must have + /// @param Type of elements in both the input and output streams + /// @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); + } + + /// Ensure the input stream is greater than `size` elements long, and emit all elements if so. + /// If not, throw an `IllegalStateException`. + /// + /// @param size The size the stream must be longer than + /// @param Type of elements in both the input and output streams + /// @return A non-null `SizeGatherer` + /// @throws IllegalStateException when the input stream is not exactly `size` elements long + public static SizeGatherer sizeGreaterThan(final long size) { + return new SizeGatherer<>(SizeGatherer.Operation.GreaterThan, size); + } + + /// Ensure the input stream is greater than or equal to `size` elements long, and emit all elements if so. + /// If not, throw an `IllegalStateException`. + /// + /// @param size The minimum size of the stream + /// @param Type of elements in both the input and output streams + /// @return A non-null `SizeGatherer` + /// @throws IllegalStateException when the input stream is not exactly `size` elements long + public static SizeGatherer sizeGreaterThanOrEqualTo(final long size) { + return new SizeGatherer<>(SizeGatherer.Operation.GreaterThanOrEqualTo, size); + } + + /// Ensure the input stream is less than `size` elements long, and emit all elements if so. + /// If not, throw an `IllegalStateException`. + /// + /// @param size The size the stream must be shorter than + /// @param Type of elements in both the input and output streams + /// @return A non-null `SizeGatherer` + /// @throws IllegalStateException when the input stream is not exactly `size` elements long + public static SizeGatherer sizeLessThan(final long size) { + return new SizeGatherer<>(SizeGatherer.Operation.LessThan, size); + } + + /// Ensure the input stream is less than or equal to `size` elements long, and emit all elements if so. + /// If not, throw an `IllegalStateException`. + /// + /// @param size The maximum size the stream + /// @param Type of elements in both the input and output streams + /// @return A non-null `SizeGatherer` + /// @throws IllegalStateException when the input stream is not exactly `size` elements long + public static SizeGatherer sizeLessThanOrEqualTo(final long size) { + return new SizeGatherer<>(SizeGatherer.Operation.LessThanOrEqualTo, size); + } + /// Limit the number of elements in the stream to some number per period. When the limit is reached, /// consumption is paused until a new period starts and the count resets. /// diff --git a/src/main/java/com/ginsberg/gatherers4j/SizeGatherer.java b/src/main/java/com/ginsberg/gatherers4j/SizeGatherer.java index b99b1c6..53c9fb4 100644 --- a/src/main/java/com/ginsberg/gatherers4j/SizeGatherer.java +++ b/src/main/java/com/ginsberg/gatherers4j/SizeGatherer.java @@ -28,14 +28,24 @@ public class SizeGatherer implements Gatherer, INPUT> { private final long targetSize; + private final Operation operation; - SizeGatherer(long targetSize) { + SizeGatherer(final Operation operation, final long targetSize) { if (targetSize < 0) { throw new IllegalArgumentException("Target size cannot be negative"); } + this.operation = operation; this.targetSize = targetSize; } + @Override + public BiConsumer, Downstream> finisher() { + return (state, downstream) -> { + operation.checkFinalLength(state.elements.size(), targetSize); + state.elements.forEach(downstream::push); + }; + } + @Override public Supplier> initializer() { return State::new; @@ -44,27 +54,100 @@ public Supplier> initializer() { @Override public Integrator, INPUT, INPUT> integrator() { return (state, element, downstream) -> { + operation.tryAccept(state.elements.size() + 1, targetSize); state.elements.add(element); - if (state.elements.size() > targetSize) { - fail(); - } return !downstream.isRejecting(); }; } - @Override - public BiConsumer, Downstream> finisher() { - return (state, downstream) -> { - if (state.elements.size() == targetSize) { - state.elements.forEach(downstream::push); - } else { - fail(); + enum Operation { + Equal { + @Override + void tryAccept(long length, long target) { + if(length > target) { + fail(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); + } + }, + GreaterThan { + @Override + void checkFinalLength(long length, long target) { + if (length <= target) { + fail(target); + } + } + + void fail(long target) { + throw new IllegalStateException("Stream length must be greater than " + target); + } + + }, + GreaterThanOrEqualTo { + @Override + void checkFinalLength(long length, long target) { + if (length < target) { + fail(target); + } + } + + void fail(long target) { + throw new IllegalStateException("Stream length must be greater than or equal to " + target); + } + }, + LessThan { + @Override + void tryAccept(long length, long target) { + if(length >= target) { + fail(target); + } + } + + @Override + void checkFinalLength(long length, long target) { + if (length >= target) { + fail(target); + } + } + + void fail(long target) { + throw new IllegalStateException("Stream length must be less than " + target); + } + }, + LessThanOrEqualTo { + @Override + void tryAccept(long length, long target) { + if(length > target) { + fail(target); + } + } + + @Override + void checkFinalLength(long length, long target) { + if (length > target) { + fail(target); + } + } + + void fail(long target) { + throw new IllegalStateException("Stream length must be less than or equal to " + target); } }; - } - private void fail() { - throw new IllegalStateException("Size must be exactly " + targetSize); + abstract void checkFinalLength(long length, long target); + void tryAccept(long length, long target){ + // Empty implementation + } } public static class State { diff --git a/src/test/java/com/ginsberg/gatherers4j/SizeGathererTest.java b/src/test/java/com/ginsberg/gatherers4j/SizeGathererTest.java index 58ab708..35b4f8e 100644 --- a/src/test/java/com/ginsberg/gatherers4j/SizeGathererTest.java +++ b/src/test/java/com/ginsberg/gatherers4j/SizeGathererTest.java @@ -16,6 +16,7 @@ package com.ginsberg.gatherers4j; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.List; @@ -26,37 +27,176 @@ class SizeGathererTest { - @Test - void sizeMustNotBeNegative() { - assertThatThrownBy(() -> - Stream.empty().gather(Gatherers4j.exactSize(-1)).toList() - ).isInstanceOf(IllegalArgumentException.class); + + @Nested + class Common { + @Test + void targetSizeMustNotBeNegative() { + assertThatThrownBy(() -> + Stream.empty().gather(Gatherers4j.sizeExactly(-1)).toList() + ).isInstanceOf(IllegalArgumentException.class); + } } - @Test - void doesNotEmitUnderTarget() { - assertThatThrownBy(() -> - Stream.of("A").gather(Gatherers4j.exactSize(2)).toList() - ).isInstanceOf(IllegalStateException.class); + @Nested + class Equal { + + @Test + void doesNotEmitUnderTarget() { + assertThatThrownBy(() -> + Stream.of("A").gather(Gatherers4j.sizeExactly(2)).toList() + ).isInstanceOf(IllegalStateException.class); + } + + @Test + void doesNotEmitOverTarget() { + assertThatThrownBy(() -> + Stream.of("A", "B", "C").gather(Gatherers4j.sizeExactly(2)).toList() + ).isInstanceOf(IllegalStateException.class); + } + + @Test + void emitsAtTarget() { + // Arrange + final Stream input = Stream.of("A", "B", "C"); + + // Act + final List output = input.gather(Gatherers4j.sizeExactly(3)).toList(); + + // Assert + assertThat(output).containsExactly("A", "B", "C"); + } } - @Test - void doesNotEmitOverTarget() { - assertThatThrownBy(() -> - Stream.of("A", "B", "C").gather(Gatherers4j.exactSize(2)).toList() - ).isInstanceOf(IllegalStateException.class); + @Nested + class GreaterThan { + + @Test + void doesNotEmitUnderTarget() { + assertThatThrownBy(() -> + Stream.of("A").gather(Gatherers4j.sizeGreaterThan(2)).toList() + ).isInstanceOf(IllegalStateException.class); + } + + @Test + void doesNotEmitAtTarget() { + assertThatThrownBy(() -> + Stream.of("A", "B").gather(Gatherers4j.sizeGreaterThan(2)).toList() + ).isInstanceOf(IllegalStateException.class); + } + + @Test + void emitsOverTarget() { + // Arrange + final Stream input = Stream.of("A", "B", "C"); + + // Act + final List output = input.gather(Gatherers4j.sizeGreaterThan(2)).toList(); + + // Assert + assertThat(output).containsExactly("A", "B", "C"); + } } - @Test - void emitsAtTarget() { - // Arrange - final Stream input = Stream.of("A", "B", "C"); + @Nested + class GreaterThanOrEqualTo { + + @Test + void doesNotEmitUnderTarget() { + assertThatThrownBy(() -> + Stream.of("A").gather(Gatherers4j.sizeGreaterThanOrEqualTo(2)).toList() + ).isInstanceOf(IllegalStateException.class); + } + + @Test + void emitsAtTarget() { + // Arrange + final Stream input = Stream.of("A", "B"); + + // Act + final List output = input.gather(Gatherers4j.sizeGreaterThanOrEqualTo(2)).toList(); + + // Assert + assertThat(output).containsExactly("A", "B"); + } + + @Test + void emitsOverTarget() { + // Arrange + final Stream input = Stream.of("A", "B", "C"); + + // Act + final List output = input.gather(Gatherers4j.sizeGreaterThanOrEqualTo(2)).toList(); - // Act - final List output = input.gather(Gatherers4j.exactSize(3)).toList(); + // Assert + assertThat(output).containsExactly("A", "B", "C"); + } - // Assert - assertThat(output).containsExactly("A", "B", "C"); } + @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(() -> + Stream.of("A", "B").gather(Gatherers4j.sizeLessThan(2)).toList() + ).isInstanceOf(IllegalStateException.class); + } + + @Test + void doesNotEmitOverTarget() { + assertThatThrownBy(() -> + Stream.of("A", "B", "C").gather(Gatherers4j.sizeLessThan(2)).toList() + ).isInstanceOf(IllegalStateException.class); + } + } + + + @Nested + class LessThanOrEqualTo { + + @Test + void emitsUnderTarget() { + // Arrange + final Stream input = Stream.of("A"); + + // Act + final List output = input.gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList(); + + // Assert + assertThat(output).containsExactly("A"); + } + + @Test + void emitsAtTarget() { + // Arrange + final Stream input = Stream.of("A", "B"); + + // Act + final List output = input.gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList(); + + // Assert + assertThat(output).containsExactly("A", "B"); + } + + @Test + void doesNotEmitOverTarget() { + assertThatThrownBy(() -> + Stream.of("A", "B", "C").gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList() + ).isInstanceOf(IllegalStateException.class); + } + } } \ No newline at end of file