Skip to content

Commit

Permalink
#48: Account for stream and argument not being the same length
Browse files Browse the repository at this point in the history
+ I am not wild about having to use a type witness, but I'm fine with adding it as I think this use case is pretty niche and anybody who needs it could probably tolerate a type witness.
  • Loading branch information
tginsberg committed Nov 1, 2024
1 parent da00143 commit aa8396f
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 17 deletions.
6 changes: 3 additions & 3 deletions src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java
Original file line number Diff line number Diff line change
Expand Up @@ -318,23 +318,23 @@ public static <INPUT> IndexingGatherer<INPUT> withIndex() {
/// and the argument collection
///
/// @param other A non-null iterable to zip with
public static <FIRST, SECOND> Gatherer<FIRST, Void, Pair<FIRST, SECOND>> zipWith(final Iterable<SECOND> other) {
public static <FIRST, SECOND> ZipWithGatherer<FIRST, SECOND> zipWith(final Iterable<SECOND> other) {
return new ZipWithGatherer<>(other);
}

/// Creates a stream of `Pair<FIRST,SECOND>` objects whose values come from the stream this is called on
/// and the argument iterator
///
/// @param other A non-null iterator to zip with
public static <FIRST, SECOND> Gatherer<FIRST, Void, Pair<FIRST, SECOND>> zipWith(final Iterator<SECOND> other) {
public static <FIRST, SECOND> ZipWithGatherer<FIRST, SECOND> zipWith(final Iterator<SECOND> other) {
return new ZipWithGatherer<>(other);
}

/// Creates a stream of `Pair<FIRST,SECOND>` objects whose values come from the stream this is called on
/// and the argument stream
///
/// @param other A non-null stream to zip with
public static <FIRST, SECOND> Gatherer<FIRST, Void, Pair<FIRST, SECOND>> zipWith(final Stream<SECOND> other) {
public static <FIRST, SECOND> ZipWithGatherer<FIRST, SECOND> zipWith(final Stream<SECOND> other) {
return new ZipWithGatherer<>(other);
}

Expand Down
84 changes: 79 additions & 5 deletions src/main/java/com/ginsberg/gatherers4j/ZipWithGatherer.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@

import java.util.Iterator;
import java.util.Spliterator;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Gatherer;
import java.util.stream.Stream;

import static com.ginsberg.gatherers4j.GathererUtils.mustNotBeNull;

public class ZipWithGatherer<FIRST, SECOND> implements Gatherer<FIRST, Void, Pair<FIRST, SECOND>> {
private final Spliterator<SECOND> otherSpliterator;
private Function<SECOND, FIRST> sourceWhenArgumentLonger;
private Function<FIRST, SECOND> argumentWhenSourceLonger;

ZipWithGatherer(final Iterable<SECOND> other) {
mustNotBeNull(other, "Other iterable must not be null");
Expand All @@ -34,19 +38,89 @@ public class ZipWithGatherer<FIRST, SECOND> implements Gatherer<FIRST, Void, Pai
ZipWithGatherer(final Iterator<SECOND> other) {
mustNotBeNull(other, "Other iterator must not be null");
final Iterable<SECOND> iterable = () -> other;
otherSpliterator = iterable.spliterator();
otherSpliterator = (iterable).spliterator();
}

ZipWithGatherer(final Stream<SECOND> other) {
mustNotBeNull(other, "Other stream must not be null");
otherSpliterator = other.spliterator();
}

/// When the argument `Iterable`, `Iterator` or `Stream` runs out of elements before the source stream does,
/// use the result of the `function` provided for the remaining `SECOND` elements of each `Pair`
/// until the source is exhausted.
///
/// Note: You may need a type witness when using this:
///
/// `source.gather(Gatherers4j.<String, Integer>zipWith(right).argumentWhenSourceLonger(String::length))`
///
/// @param mappingFunction A non-null function which takes a possibly null `<FIRST>`
/// and emits a possibly null `<SECOND>`
public ZipWithGatherer<FIRST, SECOND> argumentWhenSourceLonger(final Function<FIRST, SECOND> mappingFunction) {
mustNotBeNull(mappingFunction, "Mapping function must not be null, use nullArgumentWhenSourceLonger() to insert nulls");
argumentWhenSourceLonger = mappingFunction;
return this;
}

/// When the source stream runs out of elements before the argument `Iterable`, `Iterator` or `Stream` does,
/// use the result of the `function` provided for the remaining `FIRST` elements of each `Pair`
/// until the argument is exhausted.
///
/// Note: You may need a type witness when using this:
///
/// `source.gather(Gatherers4j.<String, Integer>zipWith(right).sourceWhenArgumentLonger(String::valueOf))`
///
/// @param mappingFunction A non-null function which takes a possibly null `<SECOND>`
/// and emits a possibly null `<FIRST>`
public ZipWithGatherer<FIRST, SECOND> sourceWhenArgumentLonger(final Function<SECOND, FIRST> mappingFunction) {
mustNotBeNull(mappingFunction, "Mapping function must not be null, use nullSourceWhenArgumentLonger() to insert nulls");
sourceWhenArgumentLonger = mappingFunction;
return this;
}

/// When the argument `Iterable`, `Iterator` or `Stream` runs out of elements before the source stream does,
/// use `null` for the remaining `SECOND` elements of each `Pair` until the source is exhausted.
///
/// Note: You may need a type witness when using this:
///
/// `source.gather(Gatherers4j.<String, Integer>zipWith(right).nullArgumentWhenSourceLonger())`
///
public ZipWithGatherer<FIRST, SECOND> nullArgumentWhenSourceLonger() {
argumentWhenSourceLonger = _ -> null;
return this;
}

/// When the source stream runs out of elements before the argument `Iterable`, `Iterator` or `Stream` does,
/// use `null` for the remaining `FIRST` elements of each `Pair` until the argument is exhausted.
///
/// Note: You may need a type witness when using this:
///
/// `source.gather(Gatherers4j.<String, Integer>zipWith(right).nullSourceWhenArgumentLonger())`
public ZipWithGatherer<FIRST, SECOND> nullSourceWhenArgumentLonger() {
sourceWhenArgumentLonger = _ -> null;
return this;
}

@Override
public Integrator<Void, FIRST, Pair<FIRST, SECOND>> integrator() {
return (_, element, downstream) -> otherSpliterator
.tryAdvance(
it -> downstream.push(new Pair<>(element, it))
) && !downstream.isRejecting();
return (_, element, downstream) -> {
boolean advanced = otherSpliterator.tryAdvance(it -> downstream.push(new Pair<>(element, it)));
if (!advanced && argumentWhenSourceLonger != null) {
downstream.push(new Pair<>(element, argumentWhenSourceLonger.apply(element)));
}
return !downstream.isRejecting();
};
}

@Override
public BiConsumer<Void, Downstream<? super Pair<FIRST, SECOND>>> finisher() {
return (_, downstream) -> {
boolean downstreamIsRejecting = downstream.isRejecting();
while (sourceWhenArgumentLonger != null && !downstreamIsRejecting) {
downstreamIsRejecting = !otherSpliterator.tryAdvance(arg ->
downstream.push(new Pair<>(sourceWhenArgumentLonger.apply(arg), arg))
);
}
};
}
}
119 changes: 110 additions & 9 deletions src/test/java/com/ginsberg/gatherers4j/ZipWithGathererTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import org.junit.jupiter.api.Test;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;
Expand Down Expand Up @@ -50,22 +49,65 @@ void argumentStreamMustNotBeNull() {
}

@Test
void interleavingGathererThisEmpty() {
void argumentWhenSourceLongerFunctionMustNotBeNull() {
assertThatThrownBy(() -> Stream.of("A")
.gather(Gatherers4j.zipWith(List.of("A")).argumentWhenSourceLonger(null)).toList()
).isInstanceOf(IllegalArgumentException.class);
}

@Test
void sourceWhenArgumentLongerFunctionMustNotBeNull() {
assertThatThrownBy(() -> Stream.of("A")
.gather(Gatherers4j.zipWith(List.of("A")).sourceWhenArgumentLonger(null)).toList()
).isInstanceOf(IllegalArgumentException.class);
}

@Test
void zipWhenArgumentIsLongerFromFunction() {
// Arrange
final Stream<String> left = Stream.empty();
final Stream<Integer> right = Stream.of(1, 2, 3);
final Stream<String> left = Stream.of("A");
final Stream<Integer> right = Stream.of(1, 2, 3, 4);

// Act
final List<Pair<String, Integer>> output = left
.gather(Gatherers4j.zipWith(right))
.gather(Gatherers4j.<String, Integer>zipWith(right).sourceWhenArgumentLonger(String::valueOf))
.toList();

// Assert
assertThat(output).isEmpty();
assertThat(output)
.hasSize(4)
.containsExactly(
new Pair<>("A", 1),
new Pair<>("2", 2),
new Pair<>("3", 3),
new Pair<>("4", 4)
);
}

@Test
void zipGathererOtherEmpty() {
void zipWhenArgumentIsLongerNull() {
// Arrange
final Stream<String> left = Stream.of("A");
final Stream<Integer> right = Stream.of(1, 2, 3, 4);

// Act
final List<Pair<String, Integer>> output = left
.gather(Gatherers4j.<String, Integer>zipWith(right).nullSourceWhenArgumentLonger())
.toList();

// Assert
assertThat(output)
.hasSize(4)
.containsExactly(
new Pair<>("A", 1),
new Pair<>(null, 2),
new Pair<>(null, 3),
new Pair<>(null, 4)
);
}

@Test
void zipWhenOtherIsEmpty() {
// Arrange
final Stream<String> left = Stream.of("A", "B", "C");
final Stream<Integer> right = Stream.empty();
Expand All @@ -80,10 +122,69 @@ void zipGathererOtherEmpty() {
}

@Test
void zipWithCollectionGatherer() {
void zipWhenSourceIsLongerFromFunction() {
// Arrange
final Stream<String> left = Stream.of("A", "Bb", "Ccc", "Dddd");
final Stream<Integer> right = Stream.of(1);

// Act
final List<Pair<String, Integer>> output = left
.gather(Gatherers4j.<String, Integer>zipWith(right).argumentWhenSourceLonger(String::length))
.toList();

// Assert
assertThat(output)
.hasSize(4)
.containsExactly(
new Pair<>("A", 1),
new Pair<>("Bb", 2),
new Pair<>("Ccc", 3),
new Pair<>("Dddd", 4)
);
}

@Test
void zipWhenSourceIsLongerNull() {
// Arrange
final Stream<String> left = Stream.of("A", "B", "C", "D");
final Stream<Integer> right = Stream.of(1);

// Act
final List<Pair<String, Integer>> output = left
.gather(Gatherers4j.<String, Integer>zipWith(right).nullArgumentWhenSourceLonger())
.toList();

// Assert
assertThat(output)
.hasSize(4)
.containsExactly(
new Pair<>("A", 1),
new Pair<>("B", null),
new Pair<>("C", null),
new Pair<>("D", null)
);
}

@Test
void zipWhenThisIsEmpty() {
// Arrange
final Stream<String> left = Stream.empty();
final Stream<Integer> right = Stream.of(1, 2, 3);

// Act
final List<Pair<String, Integer>> output = left
.gather(Gatherers4j.zipWith(right))
.toList();

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

@Test
void zipWithIterableGatherer() {
// Arrange
final Stream<String> left = Stream.of("A", "B", "C");
final Collection<Integer> right = List.of(1, 2, 3);
final Iterable<Integer> right = List.of(1, 2, 3);

// Act
final List<Pair<String, Integer>> output = left
Expand Down

0 comments on commit aa8396f

Please sign in to comment.