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) {

Reply via email to