You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2022/08/12 17:58:30 UTC

[commons-lang] 01/02: Add LangCollectors

This is an automated email from the ASF dual-hosted git repository.

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git

commit 62910e4f329923c77bc8bd2156d56544d1119efd
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Fri Aug 12 11:33:10 2022 -0400

    Add LangCollectors
---
 src/changes/changes.xml                            |   1 +
 .../java/org/apache/commons/lang3/StringUtils.java |  21 +--
 .../commons/lang3/stream/LangCollectors.java       | 167 +++++++++++++++++++++
 .../commons/lang3/stream/LangCollectorsTest.java   | 141 +++++++++++++++++
 4 files changed, 314 insertions(+), 16 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index aa1afbda2..f1f71c10d 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -160,6 +160,7 @@ The <action> type attribute can be add,update,fix,remove.
     <action issue="LANG-1662" type="add" dev="ggregory" due-to="Daniel Augusto Veronezi Salvador, Gary Gregory, Bruno P. Kinoshita">Let ReflectionToStringBuilder only reflect given field names #849.</action>
     <action                   type="add" dev="ggregory" due-to="Gary Gregory">Add Streams.of(Enumeration&lt;E&gt;).</action>
     <action                   type="add" dev="ggregory" due-to="Gary Gregory">Add Streams.of(Iterable&lt;E&gt;).</action>
+    <action                   type="add" dev="ggregory" due-to="Gary Gregory">Add LangCollectors.</action>
     <!-- UPDATE -->
     <action                   type="update" dev="ggregory" due-to="Dependabot, XenoAmess, Gary Gregory">Bump actions/cache from 2.1.4 to 3.0.7 #742, #752, #764, #833, #867.</action>
     <action                   type="update" dev="ggregory" due-to="Dependabot">Bump actions/checkout from 2 to 3 #819, #825, #859.</action>
diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java b/src/main/java/org/apache/commons/lang3/StringUtils.java
index 8b49f66ab..2960c83f7 100644
--- a/src/main/java/org/apache/commons/lang3/StringUtils.java
+++ b/src/main/java/org/apache/commons/lang3/StringUtils.java
@@ -27,12 +27,13 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
-import java.util.StringJoiner;
 import java.util.function.Supplier;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 
 import org.apache.commons.lang3.function.Suppliers;
 import org.apache.commons.lang3.function.ToBooleanBiFunction;
+import org.apache.commons.lang3.stream.LangCollectors;
 
 /**
  * <p>Operations on {@link java.lang.String} that are
@@ -4705,10 +4706,7 @@ public class StringUtils {
      * @return the joined String, {@code null} if null array input
      */
     public static String join(final Object[] array, final String delimiter) {
-        if (array == null) {
-            return null;
-        }
-        return join(array, delimiter, 0, array.length);
+        return array != null ? join(array, toStringOrEmpty(delimiter), 0, array.length) : null;
     }
 
     /**
@@ -4747,17 +4745,8 @@ public class StringUtils {
      * {@code endIndex > array.length()}
      */
     public static String join(final Object[] array, final String delimiter, final int startIndex, final int endIndex) {
-        if (array == null) {
-            return null;
-        }
-        if (endIndex - startIndex <= 0) {
-            return EMPTY;
-        }
-        final StringJoiner joiner = new StringJoiner(toStringOrEmpty(delimiter));
-        for (int i = startIndex; i < endIndex; i++) {
-            joiner.add(toStringOrEmpty(array[i]));
-        }
-        return joiner.toString();
+        return array != null ? Stream.of(array).skip(startIndex).limit(Math.max(0, endIndex - startIndex))
+            .collect(LangCollectors.joining(delimiter, EMPTY, EMPTY, StringUtils::toStringOrEmpty)) : null;
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/lang3/stream/LangCollectors.java b/src/main/java/org/apache/commons/lang3/stream/LangCollectors.java
new file mode 100644
index 000000000..abd3b10f8
--- /dev/null
+++ b/src/main/java/org/apache/commons/lang3/stream/LangCollectors.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.lang3.stream;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Implementations of {@link Collector} that implement various useful reduction operations.
+ * <p>
+ * This class is called {@code LangCollectors} instead of {@code Collectors} to avoid clashes with {@link Collectors}.
+ * </p>
+ *
+ * @since 3.13.0
+ */
+public final class LangCollectors {
+
+    /**
+     * Simple implementation class for {@code Collector}.
+     *
+     * @param <T> the type of elements to be collected
+     * @param <R> the type of the result
+     */
+    private static class SimpleCollector<T, A, R> implements Collector<T, A, R> {
+
+        private final BiConsumer<A, T> accumulator;
+        private final Set<Characteristics> characteristics;
+        private final BinaryOperator<A> combiner;
+        private final Function<A, R> finisher;
+        private final Supplier<A> supplier;
+
+        private SimpleCollector(final Supplier<A> supplier, final BiConsumer<A, T> accumulator, final BinaryOperator<A> combiner, final Function<A, R> finisher,
+            final Set<Characteristics> characteristics) {
+            this.supplier = supplier;
+            this.accumulator = accumulator;
+            this.combiner = combiner;
+            this.finisher = finisher;
+            this.characteristics = characteristics;
+        }
+
+        @Override
+        public BiConsumer<A, T> accumulator() {
+            return accumulator;
+        }
+
+        @Override
+        public Set<Characteristics> characteristics() {
+            return characteristics;
+        }
+
+        @Override
+        public BinaryOperator<A> combiner() {
+            return combiner;
+        }
+
+        @Override
+        public Function<A, R> finisher() {
+            return finisher;
+        }
+
+        @Override
+        public Supplier<A> supplier() {
+            return supplier;
+        }
+    }
+
+    private static final Set<Collector.Characteristics> CH_NOID = Collections.emptySet();
+
+    /**
+     * Returns a {@code Collector} that concatenates the input elements, separated by the specified delimiter, in encounter
+     * order.
+     * <p>
+     * This is a variation of {@link Collectors#joining()} that works with any element class, not just {@code CharSequence}.
+     * </p>
+     *
+     * @return A {@code Collector} which concatenates Object elements, separated by the specified delimiter, in encounter
+     *         order.
+     */
+    public static Collector<Object, ?, String> joining() {
+        return new SimpleCollector<>(StringBuilder::new, StringBuilder::append, StringBuilder::append, StringBuilder::toString, CH_NOID);
+    }
+
+    /**
+     * Returns a {@code Collector} that concatenates the input elements, separated by the specified delimiter, in encounter
+     * order.
+     * <p>
+     * This is a variation of {@link Collectors#joining(CharSequence)} that works with any element class, not just
+     * {@code CharSequence}.
+     * </p>
+     *
+     * @param delimiter the delimiter to be used between each element.
+     * @return A {@code Collector} which concatenates Object elements, separated by the specified delimiter, in encounter
+     *         order.
+     */
+    public static Collector<Object, ?, String> joining(final CharSequence delimiter) {
+        return joining(delimiter, StringUtils.EMPTY, StringUtils.EMPTY);
+    }
+
+    /**
+     * Returns a {@code Collector} that concatenates the input elements, separated by the specified delimiter, with the
+     * specified prefix and suffix, in encounter order.
+     * <p>
+     * This is a variation of {@link Collectors#joining(CharSequence, CharSequence, CharSequence)} that works with any
+     * element class, not just {@code CharSequence}.
+     * </p>
+     *
+     * @param delimiter the delimiter to be used between each element
+     * @param prefix the sequence of characters to be used at the beginning of the joined result
+     * @param suffix the sequence of characters to be used at the end of the joined result
+     * @return A {@code Collector} which concatenates CharSequence elements, separated by the specified delimiter, in
+     *         encounter order
+     */
+    public static Collector<Object, ?, String> joining(final CharSequence delimiter, final CharSequence prefix, final CharSequence suffix) {
+        return joining(delimiter, prefix, suffix, Objects::toString);
+    }
+
+    /**
+     * Returns a {@code Collector} that concatenates the input elements, separated by the specified delimiter, with the
+     * specified prefix and suffix, in encounter order.
+     * <p>
+     * This is a variation of {@link Collectors#joining(CharSequence, CharSequence, CharSequence)} that works with any
+     * element class, not just {@code CharSequence}.
+     * </p>
+     *
+     * @param delimiter the delimiter to be used between each element
+     * @param prefix the sequence of characters to be used at the beginning of the joined result
+     * @param suffix the sequence of characters to be used at the end of the joined result
+     * @param toString A function that takes an Object and returns a non-null String.
+     * @return A {@code Collector} which concatenates CharSequence elements, separated by the specified delimiter, in
+     *         encounter order
+     */
+    public static Collector<Object, ?, String> joining(final CharSequence delimiter, final CharSequence prefix, final CharSequence suffix,
+        final Function<Object, String> toString) {
+        return new SimpleCollector<>(() -> new StringJoiner(delimiter, prefix, suffix), (a, t) -> a.add(toString.apply(t)), StringJoiner::merge,
+            StringJoiner::toString, CH_NOID);
+    }
+
+    private LangCollectors() {
+        // No instance
+    }
+
+}
diff --git a/src/test/java/org/apache/commons/lang3/stream/LangCollectorsTest.java b/src/test/java/org/apache/commons/lang3/stream/LangCollectorsTest.java
new file mode 100644
index 000000000..e8f408498
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/stream/LangCollectorsTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.lang3.stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link LangCollectors}
+ */
+public class LangCollectorsTest {
+
+    private static class Fixture {
+        int value;
+
+        private Fixture(final int value) {
+            this.value = value;
+        }
+
+        @Override
+        public String toString() {
+            return Integer.toString(value);
+        }
+    }
+
+    private static final Long _1L = Long.valueOf(1);
+    private static final Long _2L = Long.valueOf(2);
+    private static final Long _3L = Long.valueOf(3);
+
+    private static final Function<Object, String> TO_STRING = Objects::toString;
+
+    private static final Collector<Object, ?, String> JOINING_0 = LangCollectors.joining();
+    private static final Collector<Object, ?, String> JOINING_1 = LangCollectors.joining("-");
+    private static final Collector<Object, ?, String> JOINING_3 = LangCollectors.joining("-", "<", ">");
+    private static final Collector<Object, ?, String> JOINING_4 = LangCollectors.joining("-", "<", ">", TO_STRING);
+    private static final Collector<Object, ?, String> JOINING_4_NUL = LangCollectors.joining("-", "<", ">", o -> Objects.toString(o, "NUL"));
+
+    @Test
+    public void testJoiningNonStrings0Arg() {
+        assertEquals("", Stream.of().collect(JOINING_0));
+        assertEquals("1", Stream.of(_1L).collect(JOINING_0));
+        assertEquals("12", Stream.of(_1L, _2L).collect(JOINING_0));
+        assertEquals("123", Stream.of(_1L, _2L, _3L).collect(JOINING_0));
+        assertEquals("1null3", Stream.of(_1L, null, _3L).collect(JOINING_0));
+        assertEquals("12", Stream.of(new AtomicLong(1), new AtomicLong(2)).collect(JOINING_0));
+        assertEquals("12", Stream.of(new Fixture(1), new Fixture(2)).collect(JOINING_0));
+    }
+
+    @Test
+    public void testJoiningNonStrings1Arg() {
+        assertEquals("", Stream.of().collect(JOINING_1));
+        assertEquals("1", Stream.of(_1L).collect(JOINING_1));
+        assertEquals("1-2", Stream.of(_1L, _2L).collect(JOINING_1));
+        assertEquals("1-2-3", Stream.of(_1L, _2L, _3L).collect(JOINING_1));
+        assertEquals("1-null-3", Stream.of(_1L, null, _3L).collect(JOINING_1));
+        assertEquals("1-2", Stream.of(new AtomicLong(1), new AtomicLong(2)).collect(JOINING_1));
+        assertEquals("1-2", Stream.of(new Fixture(1), new Fixture(2)).collect(JOINING_1));
+    }
+
+    @Test
+    public void testJoiningNonStrings3Args() {
+        assertEquals("<>", Stream.of().collect(JOINING_3));
+        assertEquals("<1>", Stream.of(_1L).collect(JOINING_3));
+        assertEquals("<1-2>", Stream.of(_1L, _2L).collect(JOINING_3));
+        assertEquals("<1-2-3>", Stream.of(_1L, _2L, _3L).collect(JOINING_3));
+        assertEquals("<1-null-3>", Stream.of(_1L, null, _3L).collect(JOINING_3));
+        assertEquals("<1-2>", Stream.of(new AtomicLong(1), new AtomicLong(2)).collect(JOINING_3));
+        assertEquals("<1-2>", Stream.of(new Fixture(1), new Fixture(2)).collect(JOINING_3));
+    }
+
+    @Test
+    public void testJoiningNonStrings4Args() {
+        assertEquals("<>", Stream.of().collect(JOINING_4));
+        assertEquals("<1>", Stream.of(_1L).collect(JOINING_4));
+        assertEquals("<1-2>", Stream.of(_1L, _2L).collect(JOINING_4));
+        assertEquals("<1-2-3>", Stream.of(_1L, _2L, _3L).collect(JOINING_4));
+        assertEquals("<1-null-3>", Stream.of(_1L, null, _3L).collect(JOINING_4));
+        assertEquals("<1-NUL-3>", Stream.of(_1L, null, _3L).collect(JOINING_4_NUL));
+        assertEquals("<1-2>", Stream.of(new AtomicLong(1), new AtomicLong(2)).collect(JOINING_4));
+        assertEquals("<1-2>", Stream.of(new Fixture(1), new Fixture(2)).collect(JOINING_4));
+    }
+
+    @Test
+    public void testJoiningStrings0Arg() {
+        assertEquals("", Stream.of().collect(JOINING_0));
+        assertEquals("1", Stream.of("1").collect(JOINING_0));
+        assertEquals("12", Stream.of("1", "2").collect(JOINING_0));
+        assertEquals("123", Stream.of("1", "2", "3").collect(JOINING_0));
+        assertEquals("1null3", Stream.of("1", null, "3").collect(JOINING_0));
+    }
+
+    @Test
+    public void testJoiningStrings1Arg() {
+        assertEquals("", Stream.of().collect(JOINING_1));
+        assertEquals("1", Stream.of("1").collect(JOINING_1));
+        assertEquals("1-2", Stream.of("1", "2").collect(JOINING_1));
+        assertEquals("1-2-3", Stream.of("1", "2", "3").collect(JOINING_1));
+        assertEquals("1-null-3", Stream.of("1", null, "3").collect(JOINING_1));
+    }
+
+    @Test
+    public void testJoiningStrings3Args() {
+        assertEquals("<>", Stream.of().collect(JOINING_3));
+        assertEquals("<1>", Stream.of("1").collect(JOINING_3));
+        assertEquals("<1-2>", Stream.of("1", "2").collect(JOINING_3));
+        assertEquals("<1-2-3>", Stream.of("1", "2", "3").collect(JOINING_3));
+        assertEquals("<1-null-3>", Stream.of("1", null, "3").collect(JOINING_3));
+    }
+
+    @Test
+    public void testJoiningStrings4Args() {
+        assertEquals("<>", Stream.of().collect(JOINING_4));
+        assertEquals("<1>", Stream.of("1").collect(JOINING_4));
+        assertEquals("<1-2>", Stream.of("1", "2").collect(JOINING_4));
+        assertEquals("<1-2-3>", Stream.of("1", "2", "3").collect(JOINING_4));
+        assertEquals("<1-null-3>", Stream.of("1", null, "3").collect(JOINING_4));
+        assertEquals("<1-NUL-3>", Stream.of("1", null, "3").collect(JOINING_4_NUL));
+    }
+}