diff --git a/README.md b/README.md index 3008f11..dc196f8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A library of useful [Stream Gatherers](https://openjdk.org/jeps/473) (custom int # Installing -To use this library, add it as a dependency to your build. +To use this library, add it as a dependency to your build. This library has no additional dependencies. **Maven** @@ -30,16 +30,17 @@ implementation("com.ginsberg:gatherers4j:0.0.1") # Gatherers In This Library ### Streams -| Function | Purpose | -|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| -| `dedupeConsecutive()` | Remove consecutive duplicates from a stream | -| `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` | -| `interleave(stream)` | Creates a stream of alternating objects from the input stream and the argument stream | -| `last(n)` | Constrain the stream to the last `n` values | -| `withIndex()` | Maps all elements of the stream as-is, along with their 0-based index. | -| `zipWith(stream)` | Creates a stream of `Pair` objects whose values come from the input stream and argument stream | -| `zipWithNext()` | Creates a stream of `List` objects via a sliding window of width 2 and stepping 1 | | +| Function | Purpose | +|---------------------------|------------------------------------------------------------------------------------------------------------------------------------| +| `concat(stream)` | Creates a stream which is the concatenation of the source stream and the given stream, which must be of the same type | +| `dedupeConsecutive()` | Remove consecutive duplicates from a stream | +| `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` | +| `interleave(stream)` | Creates a stream of alternating objects from the input stream and the argument stream | +| `last(n)` | Constrain the stream to the last `n` values | +| `withIndex()` | Maps all elements of the stream as-is, along with their 0-based index. | +| `zipWith(stream)` | Creates a stream of `Pair` objects whose values come from the input stream and argument stream | +| `zipWithNext()` | Creates a stream of `List` objects via a sliding window of width 2 and stepping 1 | | ### Mathematics/Statistics | Function | Purpose | @@ -61,7 +62,7 @@ implementation("com.ginsberg:gatherers4j:0.0.1") Stream .of("1.0", "2.0", "10.0") .map(BigDecimal::new) - .gather(Gatherers4j.averageBigDecimals()) + .gather(Gatherers4j.simpleRunningAverage()) .toList(); // [1, 1.5, 4.3333333333333333] @@ -73,12 +74,37 @@ Stream Stream .of("1.0", "2.0", "10.0", "20.0", "30.0") .map(BigDecimal::new) - .gather(Gatherers4j.averageBigDecimals().simpleMovingAverage(2)) + .gather(Gatherers4j.simpleMovingAverage(2)) .toList(); // [1.5, 6, 15, 25] ``` +#### Concatenate two streams + +```java +Stream + .of("A", "B", "C") + .gather(Gatherers4j.concat(Stream.of("D", "E", "F"))) + .toList(); + +// ["A", "B", "C", "D", "E", "F"] +``` + +#### Concatenate multiple streams + +```java +Stream + .of("A", "B", "C") + .gather(Gatherers4j + .concat(Stream.of("D", "E", "F")) + .concat(Stream.of("G", "H", "I")) // concat can be called again for more streams + ) + .toList(); + +// ["A", "B", "C", "D", "E", "F", "G", "H", "I"] +``` + #### Remove consecutive duplicate elements ```java diff --git a/src/main/java/com/ginsberg/gatherers4j/ConcatenationGatherer.java b/src/main/java/com/ginsberg/gatherers4j/ConcatenationGatherer.java new file mode 100644 index 0000000..d11e6f3 --- /dev/null +++ b/src/main/java/com/ginsberg/gatherers4j/ConcatenationGatherer.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024 Todd Ginsberg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ginsberg.gatherers4j; + +import java.util.ArrayList; +import java.util.List; +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 ConcatenationGatherer implements Gatherer, INPUT> { + + private final List> concatThese = new ArrayList<>(); + + ConcatenationGatherer(final Stream concatThis) { + concat(concatThis); + } + + public final ConcatenationGatherer concat(final Stream concatThis) { + mustNotBeNull(concatThis, "Concatenated stream must not be null"); + this.concatThese.add(concatThis); + return this; + } + + @Override + public Supplier> initializer() { + return () -> new State<>(concatThese); + } + + @Override + public Integrator, INPUT, INPUT> integrator() { + return (_, element, downstream) -> downstream.push(element); + } + + @Override + public BiConsumer, Downstream> finisher() { + return (state, downstream) -> { + for (Stream concatThis : state.concatThese) { + concatThis.forEach(downstream::push); + } + }; + } + + public static class State { + final List> concatThese; + + public State(List> concatThese) { + this.concatThese = concatThese; + } + } +} diff --git a/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java b/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java index f4720d3..c3cec7f 100644 --- a/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java +++ b/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java @@ -26,6 +26,15 @@ public class Gatherers4j { + /** + * Concatenate the given Stream<INPUT> to the end of the current stream, in order. + * + * @param concatThis A non-null Stream<INPUT> instance to concatenate. + */ + public static ConcatenationGatherer concat(final Stream concatThis) { + return new ConcatenationGatherer<>(concatThis); + } + /** *

Given a stream of objects, filter the objects such that any consecutively appearing * after the first one are dropped. @@ -162,7 +171,7 @@ public static BigDecimalSimpleMovingAverageGatherer simpleMovingAver * via a mappingFunction and looking back `windowSize` number of elements. * * @param mappingFunction The non-null function to map from <INPUT> to BigDecimal. - * @param windowSize The number of elements to average, must be greater than 1. + * @param windowSize The number of elements to average, must be greater than 1. */ public static BigDecimalSimpleMovingAverageGatherer simpleMovingAverageBy( final Function mappingFunction, diff --git a/src/test/java/com/ginsberg/gatherers4j/ConcatenationGathererTest.java b/src/test/java/com/ginsberg/gatherers4j/ConcatenationGathererTest.java new file mode 100644 index 0000000..b261a37 --- /dev/null +++ b/src/test/java/com/ginsberg/gatherers4j/ConcatenationGathererTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2024 Todd Ginsberg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ginsberg.gatherers4j; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ConcatenationGathererTest { + + @Test + void additionalStreamCannotBeNull() { + assertThatThrownBy(() -> + Stream.of("A").gather(Gatherers4j.concat(Stream.of("1")).concat(null)) + ).isExactlyInstanceOf(IllegalArgumentException.class); + } + + @Test + void allowsEmptyConcat() { + // Arrange + final Stream input1 = Stream.of("A", "B", "C"); + final Stream input2 = Stream.empty(); + + // Act + final List output = input1.gather(Gatherers4j.concat(input2)).toList(); + + // Assert + assertThat(output).containsExactly("A", "B", "C"); + } + + @Test + void allowsEmptySource() { + // Arrange + final Stream input1 = Stream.empty(); + final Stream input2 = Stream.of("D", "E", "F"); + + // Act + final List output = input1.gather(Gatherers4j.concat(input2)).toList(); + + // Assert + assertThat(output).containsExactly("D", "E", "F"); + } + + @Test + void initialStreamCannotBeNull() { + assertThatThrownBy(() -> + Stream.of("A").gather(Gatherers4j.concat(null)) + ).isExactlyInstanceOf(IllegalArgumentException.class); + } + + @Test + void multipleConcatenations() { + // Arrange + final Stream input1 = Stream.of("A", "B", "C"); + final Stream input2 = Stream.of("D", "E", "F"); + final Stream input3 = Stream.of("G", "H", "I"); + + // Act + final List output = input1.gather(Gatherers4j.concat(input2).concat(input3)).toList(); + + // Assert + assertThat(output).containsExactly("A", "B", "C", "D", "E", "F", "G", "H", "I"); + } + + @Test + void simpleConcatenation() { + // Arrange + final Stream input1 = Stream.of("A", "B", "C"); + final Stream input2 = Stream.of("D", "E", "F"); + + // Act + final List output = input1.gather(Gatherers4j.concat(input2)).toList(); + + // Assert + assertThat(output).containsExactly("A", "B", "C", "D", "E", "F"); + } + +} \ No newline at end of file