Skip to content

Commit

Permalink
Implement exactSize(n) (#40)
Browse files Browse the repository at this point in the history
+ Going with this implementation for now, not sure if there is much call for a predicate based version of this
  • Loading branch information
tginsberg authored Oct 13, 2024
1 parent 00fae6b commit 2455866
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 3 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
### 0.5.0
+ Implement `reverse()`
+ Implement `maxBy(fn)` and `minBy(fn)`

+ Implement `exactSize(n)`
+
### 0.4.0
+ Implement `suffle()` and `shuffle(RandomGenerator)`
+ Implement `filterWithIndex()`
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ implementation("com.ginsberg:gatherers4j:0.5.0")
| `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` |
| `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` |
| `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 |
Expand Down Expand Up @@ -138,6 +139,19 @@ Stream
// [Person("Todd", "Ginsberg"), Person("Emma", "Ginsberg")]
```

#### Ensure the stream is exactly `n` elements long

```java
// Good

Stream.of("A", "B", "C").gather(Gatherers4j.exactSize(3)).toList();
// ["A", "B", "C"]

// Bad
Stream.of("A").gather(Gatherers4j.exactSize(3)).toList();
// IllegalStateException
```

#### Filter a stream, knowing the index of each element

```java
Expand Down
14 changes: 12 additions & 2 deletions src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ public static <INPUT> ThrottlingGatherer<INPUT> debounce(final int amount, final
return new DistinctGatherer<>(function);
}

/**
* Ensure the input stream is exactly <code>size</code> elements long, and emit all elements
* if so. If not, throw an <code>IllegalStateException</code>.
*
* @param size Exact number of elements the stream must have
*/
public static <INPUT> SizeGatherer<INPUT> exactSize(final long size) {
return new SizeGatherer<>(size);
}

/**
* Filter a stream according to the given <code>predicate</code>, which takes both the item being examined, and its index.
*
Expand Down Expand Up @@ -138,7 +148,7 @@ public static <INPUT, MAPPED extends Comparable<MAPPED>> MinMaxGatherer<INPUT, M

/**
* Reverse the order of the input Stream.
*
* <p>
* Note: This consumes the entire stream and holds it in memory, so it will not work on
* infinite streams and may cause memory pressure on very large streams.
*/
Expand Down Expand Up @@ -243,7 +253,7 @@ public static BigDecimalSimpleAverageGatherer<BigDecimal> simpleRunningAverage()

/**
* Shuffle the input stream into a random order.
*
* <p>
* Note: This consumes the entire stream and holds it in memory, so it will not work on
* infinite streams and may cause memory pressure on very large streams.
*
Expand Down
70 changes: 70 additions & 0 deletions src/main/java/com/ginsberg/gatherers4j/SizeGatherer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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;

public class SizeGatherer<INPUT> implements Gatherer<INPUT, SizeGatherer.State<INPUT>, INPUT> {

private final long targetSize;

SizeGatherer(long targetSize) {
if (targetSize < 0) {
throw new IllegalArgumentException("Target size cannot be negative");
}
this.targetSize = targetSize;
}

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

@Override
public Integrator<State<INPUT>, INPUT, INPUT> integrator() {
return (state, element, downstream) -> {
state.elements.add(element);
if (state.elements.size() > targetSize) {
fail();
}
return !downstream.isRejecting();
};
}

@Override
public BiConsumer<State<INPUT>, Downstream<? super INPUT>> finisher() {
return (state, downstream) -> {
if (state.elements.size() == targetSize) {
state.elements.forEach(downstream::push);
} else {
fail();
}
};
}

private void fail() {
throw new IllegalStateException("Size must be exactly " + targetSize);
}

public static class State<INPUT> {
final List<INPUT> elements = new ArrayList<>();
}
}
62 changes: 62 additions & 0 deletions src/test/java/com/ginsberg/gatherers4j/SizeGathererTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 SizeGathererTest {

@Test
void sizeMustNotBeNegative() {
assertThatThrownBy(() ->
Stream.empty().gather(Gatherers4j.exactSize(-1)).toList()
).isInstanceOf(IllegalArgumentException.class);
}

@Test
void doesNotEmitUnderTarget() {
assertThatThrownBy(() ->
Stream.of("A").gather(Gatherers4j.exactSize(2)).toList()
).isInstanceOf(IllegalStateException.class);
}

@Test
void doesNotEmitOverTarget() {
assertThatThrownBy(() ->
Stream.of("A", "B", "C").gather(Gatherers4j.exactSize(2)).toList()
).isInstanceOf(IllegalStateException.class);
}

@Test
void emitsAtTarget() {
// Arrange
final Stream<String> input = Stream.of("A", "B", "C");

// Act
final List<String> output = input.gather(Gatherers4j.exactSize(3)).toList();

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

}

0 comments on commit 2455866

Please sign in to comment.