Skip to content

Commit

Permalink
Add options to SizeGatherer to support non-exceptional outcomes
Browse files Browse the repository at this point in the history
+ Add orElse to supply a new stream instead of ending exceptionally
+ Add orElseEmpty to supply an empty stream instead of ending exceptionally
+ Unfortunately these seem to need type witnesses to work
  • Loading branch information
tginsberg committed Jan 13, 2025
1 parent c82d1b8 commit fdcd8ed
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 72 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.<String>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.<String>sizeExactly(2).orElseEmpty())
.toList();

// []
```

#### Ensure the stream is greater than `n` elements long

```java
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ public static <INPUT> LastGatherer<INPUT> last(final int count) {
/// @return A non-null `SizeGatherer`
/// @throws IllegalStateException when the input stream is not exactly `size` elements long
public static <INPUT extends @Nullable Object> SizeGatherer<INPUT> 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.
Expand Down
101 changes: 63 additions & 38 deletions src/main/java/com/ginsberg/gatherers4j/SizeGatherer.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,62 @@
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<INPUT extends @Nullable Object>
implements Gatherer<INPUT, SizeGatherer.State<INPUT>, INPUT> {

private final long targetSize;
private final Operation operation;
private Supplier<Stream<INPUT>> orElse;

SizeGatherer(final Operation operation, final long targetSize) {
if (targetSize < 0) {
throw new IllegalArgumentException("Target size cannot be negative");
}
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<Stream<INPUT>>` 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.<String>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<INPUT> orElse(final Supplier<Stream<INPUT>> 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.<String>sizeExactly(2).orElseEmpty())`
///
public SizeGatherer<INPUT> orElseEmpty() {
this.orElse = Stream::empty;
return this;
}

@Override
public BiConsumer<State<INPUT>, Downstream<? super INPUT>> 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);
}
};
}

Expand All @@ -54,74 +90,63 @@ public Supplier<State<INPUT>> initializer() {
@Override
public Integrator<State<INPUT>, 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<INPUT> {
boolean failed = false;
final List<INPUT> elements = new ArrayList<>();
}
}
99 changes: 68 additions & 31 deletions src/test/java/com/ginsberg/gatherers4j/SizeGathererTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,43 @@ class SizeGathererTest {

@Nested
class Common {
@Test
void canReplaceStreamEmpty() {
// Arrange
final Stream<String> input = Stream.of("A");

// Act
final List<String> output = input
.gather(Gatherers4j.<String>sizeExactly(2).orElseEmpty())
.toList();

// Assert
assertThat(output).isEmpty();
}

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

// Act
final List<String> output = input
.gather(Gatherers4j.<String>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(() ->
Expand All @@ -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);
}

Expand All @@ -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);
}

Expand Down Expand Up @@ -137,18 +174,6 @@ void emitsOverTarget() {
@Nested
class LessThan {

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

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

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

@Test
void doesNotEmitAtTarget() {
assertThatThrownBy(() ->
Expand All @@ -162,23 +187,30 @@ void doesNotEmitOverTarget() {
Stream.of("A", "B", "C").gather(Gatherers4j.sizeLessThan(2)).toList()
).isInstanceOf(IllegalStateException.class);
}
}


@Nested
class LessThanOrEqualTo {

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

// Act
final List<String> output = input.gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList();
final List<String> 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() {
Expand All @@ -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<String> input = Stream.of("A");

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

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

0 comments on commit fdcd8ed

Please sign in to comment.