This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/master by this push:
new 3c319fdf6e CAUSEWAY-3818: [Commons] Can<T> to support zipStream,
collect and join
3c319fdf6e is described below
commit 3c319fdf6e379963d7abaa45589bee825a54e56d
Author: Andi Huber <[email protected]>
AuthorDate: Wed Oct 9 13:40:59 2024 +0200
CAUSEWAY-3818: [Commons] Can<T> to support zipStream, collect and join
- also dealing with corner cases, where mappers return null
---
.../apache/causeway/commons/collections/Can.java | 67 ++++++++++--
.../causeway/commons/collections/Can_Empty.java | 21 ++++
.../causeway/commons/collections/Can_Multiple.java | 28 +++++
.../commons/collections/Can_Singleton.java | 37 ++++++-
.../causeway/commons/collections/CanTest.java | 118 +++++++++++++++++++++
5 files changed, 262 insertions(+), 9 deletions(-)
diff --git
a/commons/src/main/java/org/apache/causeway/commons/collections/Can.java
b/commons/src/main/java/org/apache/causeway/commons/collections/Can.java
index 800caffae2..5e8df40fba 100644
--- a/commons/src/main/java/org/apache/causeway/commons/collections/Can.java
+++ b/commons/src/main/java/org/apache/causeway/commons/collections/Can.java
@@ -496,7 +496,7 @@ extends ImmutableCollection<T>, Comparable<Can<T>>,
Serializable {
void forEach(@NonNull Consumer<? super T> action);
/**
- * Similar to {@link #forEach(Consumer)}, but zipps in {@code zippedIn} to
iterate through
+ * Similar to {@link #forEach(Consumer)}, but zips in {@code zippedIn} to
iterate through
* its elements and passes them over as the second argument to the {@code
action}.
* @param <R>
* @param zippedIn must have at least as much elements as this {@code Can}
@@ -506,16 +506,30 @@ extends ImmutableCollection<T>, Comparable<Can<T>>,
Serializable {
<R> void zip(Iterable<R> zippedIn, BiConsumer<? super T, ? super R>
action);
/**
- * Similar to {@link #map(Function)}, but zipps in {@code zippedIn} to
iterate through
+ * Similar to {@link #map(Function)}, but zips in {@code zippedIn} to
iterate through
* its elements and passes them over as the second argument to the {@code
mapper}.
* @param <R>
* @param <Z>
* @param zippedIn must have at least as much elements as this {@code Can}
* @param mapper
* @throws NoSuchElementException if {@code zippedIn} overflows
+ * @see {@link #zipStream(Iterable, BiFunction)}
*/
<R, Z> Can<R> zipMap(Iterable<Z> zippedIn, BiFunction<? super T, ? super
Z, R> mapper);
+ /**
+ * Semantically equivalent to {@link #zipMap(Iterable,
BiFunction)}.stream().
+ * <p> (Actual implementations might be optimized.)
+ * @apiNote the resulting Stream will not contain {@code null} elements
+ * @param <R>
+ * @param <Z>
+ * @param zippedIn must have at least as much elements as this {@code Can}
+ * @param mapper
+ * @throws NoSuchElementException if {@code zippedIn} overflows
+ * @see {@link #zipMap(Iterable, BiFunction)}
+ */
+ <R, Z> Stream<R> zipStream(Iterable<Z> zippedIn, BiFunction<? super T, ?
super Z, R> mapper);
+
// -- MANIPULATION
Can<T> add(@Nullable T element);
@@ -880,9 +894,9 @@ extends ImmutableCollection<T>, Comparable<Can<T>>,
Serializable {
* into which the results will be inserted
*/
<K, M extends Map<K, T>> M toMap(
- final @NonNull Function<? super T, ? extends K> keyExtractor,
- final @NonNull BinaryOperator<T> mergeFunction,
- final @NonNull Supplier<M> mapFactory);
+ @NonNull Function<? super T, ? extends K> keyExtractor,
+ @NonNull BinaryOperator<T> mergeFunction,
+ @NonNull Supplier<M> mapFactory);
/**
* Variant of {@link #toMap(Function, BinaryOperator, Supplier)},
@@ -890,8 +904,45 @@ extends ImmutableCollection<T>, Comparable<Can<T>>,
Serializable {
* @see #toMap(Function, BinaryOperator, Supplier)
*/
<K, M extends Map<K, T>> Map<K, T> toUnmodifiableMap(
- final @NonNull Function<? super T, ? extends K> keyExtractor,
- final @NonNull BinaryOperator<T> mergeFunction,
- final @NonNull Supplier<M> mapFactory);
+ @NonNull Function<? super T, ? extends K> keyExtractor,
+ @NonNull BinaryOperator<T> mergeFunction,
+ @NonNull Supplier<M> mapFactory);
+
+ // -- COLLECT
+
+ /**
+ * Semantically equivalent to {@link #stream()}
+ * .{@link Stream#collect(Collector) collect(collector)}.
+ * <p>(Actual implementations might be optimized.)
+ * @param <R>
+ * @param <A>
+ * @param collector
+ */
+ <R, A> R collect(@NonNull Collector<? super T, A, R> collector);
+
+ // -- JOIN AS STRING
+
+ /**
+ * Semantically equivalent to {@link #map(Function) map(Object::toString)}
+ * <br>{@code .collect(Collectors.joining(delimiter));}
+ * <p>(Actual implementations might be optimized.)
+ * @param delimiter
+ * @apiNote the corner case,
+ * when the {@code Object::toString} function returns {@code null}
for some elements,
+ * results in those elements simply being ignored by the join
+ */
+ String join(@NonNull String delimiter);
+
+ /**
+ * Semantically equivalent to {@link #map(Function) map(toStringFunction)}
+ * <br>{@code .collect(Collectors.joining(delimiter));}
+ * <p>(Actual implementations might be optimized.)
+ * @param toStringFunction
+ * @param delimiter
+ * @apiNote the corner case,
+ * when given {@code toStringFunction} function returns {@code null}
for some elements,
+ * results in those elements simply being ignored by the join
+ */
+ String join(@NonNull Function<? super T, String> toStringFunction,
@NonNull String delimiter);
}
diff --git
a/commons/src/main/java/org/apache/causeway/commons/collections/Can_Empty.java
b/commons/src/main/java/org/apache/causeway/commons/collections/Can_Empty.java
index 17bd3b2c10..b885a2aea9 100644
---
a/commons/src/main/java/org/apache/causeway/commons/collections/Can_Empty.java
+++
b/commons/src/main/java/org/apache/causeway/commons/collections/Can_Empty.java
@@ -36,6 +36,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
+import java.util.stream.Collector;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@@ -162,6 +163,11 @@ final class Can_Empty<T> implements Can<T> {
return Can.empty();
}
+ @Override
+ public <R, Z> Stream<R> zipStream(final Iterable<Z> zippedIn, final
BiFunction<? super T, ? super Z, R> mapper) {
+ return Stream.empty();
+ }
+
@Override
public Can<T> add(final @Nullable T element) {
return element != null
@@ -321,4 +327,19 @@ final class Can_Empty<T> implements Can<T> {
return _Casts.uncheckedCast(Collections.emptyMap());
}
+ @Override
+ public <R, A> R collect(@NonNull final Collector<? super T, A, R>
collector) {
+ return collector.finisher().apply(collector.supplier().get());
+ }
+
+ @Override
+ public String join(@NonNull final String delimiter) {
+ return "";
+ }
+
+ @Override
+ public String join(@NonNull final Function<? super T, String>
toStringFunction, @NonNull final String delimiter) {
+ return "";
+ }
+
}
diff --git
a/commons/src/main/java/org/apache/causeway/commons/collections/Can_Multiple.java
b/commons/src/main/java/org/apache/causeway/commons/collections/Can_Multiple.java
index 593486a573..cd77635482 100644
---
a/commons/src/main/java/org/apache/causeway/commons/collections/Can_Multiple.java
+++
b/commons/src/main/java/org/apache/causeway/commons/collections/Can_Multiple.java
@@ -38,6 +38,7 @@ import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
+import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@@ -45,6 +46,7 @@ import java.util.stream.Stream;
import org.springframework.lang.Nullable;
import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.base._Objects;
import org.apache.causeway.commons.internal.collections._Lists;
import org.apache.causeway.commons.internal.collections._Sets;
@@ -225,6 +227,14 @@ final class Can_Multiple<T> implements Can<T> {
return map(t->mapper.apply(t, zippedInIterator.next()));
}
+ @Override
+ public <R, Z> Stream<R> zipStream(final @NonNull Iterable<Z> zippedIn,
final BiFunction<? super T, ? super Z, R> mapper) {
+ val zippedInIterator = zippedIn.iterator();
+ return stream()
+ .map(t->mapper.apply(t, zippedInIterator.next()))
+ .filter(_NullSafe::isPresent);
+ }
+
@Override
public Can<T> add(final @Nullable T element) {
return element!=null
@@ -496,4 +506,22 @@ final class Can_Multiple<T> implements Can<T> {
return Collections.unmodifiableMap(toMap(keyExtractor, mergeFunction,
mapFactory));
}
+ @Override
+ public <R, A> R collect(@NonNull final Collector<? super T, A, R>
collector) {
+ return stream().collect(collector);
+ }
+
+ @Override
+ public String join(@NonNull final String delimiter) {
+ return join(Object::toString, delimiter);
+ }
+
+ @Override
+ public String join(@NonNull final Function<? super T, String>
toStringFunction, @NonNull final String delimiter) {
+ return stream()
+ .map(toStringFunction)
+ .filter(_NullSafe::isPresent)
+ .collect(Collectors.joining(delimiter));
+ }
+
}
diff --git
a/commons/src/main/java/org/apache/causeway/commons/collections/Can_Singleton.java
b/commons/src/main/java/org/apache/causeway/commons/collections/Can_Singleton.java
index 18462040ef..daeeffde12 100644
---
a/commons/src/main/java/org/apache/causeway/commons/collections/Can_Singleton.java
+++
b/commons/src/main/java/org/apache/causeway/commons/collections/Can_Singleton.java
@@ -38,6 +38,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
+import java.util.stream.Collector;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@@ -169,7 +170,18 @@ final class Can_Singleton<T> implements Can<T> {
@Override
public <R, Z> Can<R> zipMap(final Iterable<Z> zippedIn, final BiFunction<?
super T, ? super Z, R> mapper) {
- return Can_Singleton.of(mapper.apply(element,
zippedIn.iterator().next()));
+ var next = mapper.apply(element, zippedIn.iterator().next());
+ return next!=null
+ ? Can_Singleton.of(next)
+ : Can.empty();
+ }
+
+ @Override
+ public <R, Z> Stream<R> zipStream(final @NonNull Iterable<Z> zippedIn,
final BiFunction<? super T, ? super Z, R> mapper) {
+ var next = mapper.apply(element, zippedIn.iterator().next());
+ return next!=null
+ ? Stream.of(next)
+ : Stream.empty();
}
@Override
@@ -429,4 +441,27 @@ final class Can_Singleton<T> implements Can<T> {
return Collections.unmodifiableMap(toMap(keyExtractor, mergeFunction,
mapFactory));
}
+ @Override
+ public <R, A> R collect(@NonNull final Collector<? super T, A, R>
collector) {
+ var container = collector.supplier().get();
+ collector.accumulator().accept(container, element);
+ return collector.finisher().apply(container);
+ }
+
+ @Override
+ public String join(@NonNull final String delimiter) {
+ var str = element.toString();
+ return str!=null
+ ? str
+ : "";
+ }
+
+ @Override
+ public String join(@NonNull final Function<? super T, String>
toStringFunction, @NonNull final String delimiter) {
+ var str = toStringFunction.apply(element);
+ return str!=null
+ ? str
+ : "";
+ }
+
}
diff --git
a/commons/src/test/java/org/apache/causeway/commons/collections/CanTest.java
b/commons/src/test/java/org/apache/causeway/commons/collections/CanTest.java
index 5b6a1d729c..92f7825cfb 100644
--- a/commons/src/test/java/org/apache/causeway/commons/collections/CanTest.java
+++ b/commons/src/test/java/org/apache/causeway/commons/collections/CanTest.java
@@ -19,12 +19,15 @@
package org.apache.causeway.commons.collections;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.BiPredicate;
import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
@@ -33,10 +36,15 @@ import org.junit.jupiter.params.provider.EnumSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.springframework.lang.Nullable;
+import org.springframework.util.StringUtils;
+
+import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.collections._Sets;
import org.apache.causeway.commons.internal.testing._SerializationTester;
@@ -405,6 +413,26 @@ class CanTest {
final String name;
}
+ @RequiredArgsConstructor
+ enum CustomerScenario {
+ EMPTY(Can.empty()),
+ ONE(Can.of(new Customer("Jeff"))),
+ MANY( Can.of(new Customer("Jeff"), new Customer("Jane"))),
+ ONE_UNNAMED(Can.of(new Customer(null))),
+ MANY_WITH_UNNAMED( Can.of(new Customer(null), new Customer("Jeff"),
new Customer("Jane"), new Customer(null)));
+ ;
+ final Can<Customer> customers;
+ int cardinality() {
+ return customers.size();
+ }
+ // simulates a zip-function with nullable result
+ @Nullable static String format(Customer customer, int ordinal) {
+ return StringUtils.hasLength(customer.name)
+ ? String.format("%d->%s", ordinal, customer.name)
+ : null;
+ }
+ }
+
@RequiredArgsConstructor
enum MapScenario {
HASH_MAP(customers->customers.toMap(Customer::getName)),
@@ -474,6 +502,96 @@ class CanTest {
scenario.assertMapType(map);
}
+ // -- ZIP
+
+ /**
+ * Tests all {@link Cardinality}(s) on
+ * {@link Can#zip(Iterable, java.util.function.BiConsumer) zip},
+ * {@link Can#zipMap(Iterable, java.util.function.BiFunction) zipMap} and
+ * {@link Can#zipStream(Iterable, java.util.function.BiFunction)
zipStream}
+ */
+ @ParameterizedTest
+ @EnumSource(CustomerScenario.class)
+ void zip(final CustomerScenario scenario) {
+ var ordinals = IntStream.range(0, scenario.cardinality())
+ .mapToObj(Integer::valueOf)
+ .collect(Collectors.toList());
+ var expectedZipped = new ArrayList<String>();
+ for (int i = 0; i < scenario.cardinality(); i++) {
+ var next =
CustomerScenario.format(scenario.customers.getElseFail(i), i);
+ if(next!=null) expectedZipped.add(next); // exclude nulls
+ }
+ { // zip
+ var list = new ArrayList<String>();
+ scenario.customers
+ .zip(
+ ordinals,
+ (customer,
ordinal)->list.add(CustomerScenario.format(customer, ordinal)));
+ assertIterableEquals(
+ expectedZipped,
+ //remove nulls
+
list.stream().filter(_NullSafe::isPresent).collect(Collectors.toList()));
+ }
+ { // zipMap
+ var actualZipped = scenario.customers
+ .zipMap(ordinals, CustomerScenario::format);
+ assertIterableEquals(expectedZipped, actualZipped);
+ }
+ { // zipStream
+ var actualZipped = scenario.customers
+ .zipStream(ordinals, CustomerScenario::format)
+ .collect(Collectors.toList());
+ assertIterableEquals(expectedZipped, actualZipped);
+ }
+
+ }
+
+ // -- COLLECT
+
+ /**
+ * Tests all {@link Cardinality}(s) on
+ * {@link Can#collect(java.util.stream.Collector) collect}
+ */
+ @ParameterizedTest
+ @EnumSource(CustomerScenario.class)
+ void collect(final CustomerScenario scenario) {
+ assertIterableEquals(
+ scenario.customers.toList(),
+ scenario.customers.collect(Collectors.toList()));
+ }
+
+ // -- JOIN
+
+ /**
+ * Tests all {@link Cardinality}(s) on
+ * {@link Can#join(String) join w/ implicit toString}
+ */
+ @ParameterizedTest
+ @EnumSource(CustomerScenario.class)
+ void join_withImplicit_toString(final CustomerScenario scenario) {
+ assertEquals(
+ scenario.customers.map(Object::toString)
+ .stream()
+ .collect(Collectors.joining(", ")),
+ scenario.customers
+ .join(", "));
+ }
+
+ /**
+ * Tests all {@link Cardinality}(s) on
+ * {@link Can#join(Function, String) join w/ explicit toString}
+ */
+ @ParameterizedTest
+ @EnumSource(CustomerScenario.class)
+ void join_withExplicit_toString(final CustomerScenario scenario) {
+ assertEquals(
+ scenario.customers.map(Customer::getName)
+ .stream()
+ .collect(Collectors.joining(", ")),
+ scenario.customers
+ .join(Customer::getName, ", "));
+ }
+
// -- HEPER
private static <T> void assertSetEquals(final Set<T> a, final Set<T> b) {