Skip to content

Commit

Permalink
Implement concatenation (#16)
Browse files Browse the repository at this point in the history
* Implement concatenation

* Fix doc error

* Renamed `thenConcat` to `concat`
  • Loading branch information
tginsberg authored Aug 13, 2024
1 parent 5c111e9 commit d6585eb
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 14 deletions.
52 changes: 39 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand All @@ -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 |
Expand All @@ -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]
Expand All @@ -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
Expand Down
68 changes: 68 additions & 0 deletions src/main/java/com/ginsberg/gatherers4j/ConcatenationGatherer.java
Original file line number Diff line number Diff line change
@@ -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<INPUT> implements Gatherer<INPUT, ConcatenationGatherer.State<INPUT>, INPUT> {

private final List<Stream<INPUT>> concatThese = new ArrayList<>();

ConcatenationGatherer(final Stream<INPUT> concatThis) {
concat(concatThis);
}

public final ConcatenationGatherer<INPUT> concat(final Stream<INPUT> concatThis) {
mustNotBeNull(concatThis, "Concatenated stream must not be null");
this.concatThese.add(concatThis);
return this;
}

@Override
public Supplier<State<INPUT>> initializer() {
return () -> new State<>(concatThese);
}

@Override
public Integrator<State<INPUT>, INPUT, INPUT> integrator() {
return (_, element, downstream) -> downstream.push(element);
}

@Override
public BiConsumer<State<INPUT>, Downstream<? super INPUT>> finisher() {
return (state, downstream) -> {
for (Stream<INPUT> concatThis : state.concatThese) {
concatThis.forEach(downstream::push);
}
};
}

public static class State<INPUT> {
final List<Stream<INPUT>> concatThese;

public State(List<Stream<INPUT>> concatThese) {
this.concatThese = concatThese;
}
}
}
11 changes: 10 additions & 1 deletion src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@

public class Gatherers4j {

/**
* Concatenate the given <code>Stream&lt;INPUT&gt;</code> to the end of the current stream, in order.
*
* @param concatThis A non-null <code>Stream&lt;INPUT&gt;</code> instance to concatenate.
*/
public static <INPUT> ConcatenationGatherer<INPUT> concat(final Stream<INPUT> concatThis) {
return new ConcatenationGatherer<>(concatThis);
}

/**
* <p>Given a stream of objects, filter the objects such that any consecutively appearing
* after the first one are dropped.
Expand Down Expand Up @@ -162,7 +171,7 @@ public static BigDecimalSimpleMovingAverageGatherer<BigDecimal> simpleMovingAver
* via a <code>mappingFunction</code> and looking back `windowSize` number of elements.
*
* @param mappingFunction The non-null function to map from <code>&lt;INPUT&gt;</code> to <code>BigDecimal</code>.
* @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 <INPUT> BigDecimalSimpleMovingAverageGatherer<INPUT> simpleMovingAverageBy(
final Function<INPUT, BigDecimal> mappingFunction,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> input1 = Stream.of("A", "B", "C");
final Stream<String> input2 = Stream.empty();

// Act
final List<String> output = input1.gather(Gatherers4j.concat(input2)).toList();

// Assert
assertThat(output).containsExactly("A", "B", "C");
}

@Test
void allowsEmptySource() {
// Arrange
final Stream<String> input1 = Stream.empty();
final Stream<String> input2 = Stream.of("D", "E", "F");

// Act
final List<String> 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<String> input1 = Stream.of("A", "B", "C");
final Stream<String> input2 = Stream.of("D", "E", "F");
final Stream<String> input3 = Stream.of("G", "H", "I");

// Act
final List<String> 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<String> input1 = Stream.of("A", "B", "C");
final Stream<String> input2 = Stream.of("D", "E", "F");

// Act
final List<String> output = input1.gather(Gatherers4j.concat(input2)).toList();

// Assert
assertThat(output).containsExactly("A", "B", "C", "D", "E", "F");
}

}

0 comments on commit d6585eb

Please sign in to comment.