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

asf-gitbox-commits pushed a commit to branch feature_NUMBERS-215
in repository https://gitbox.apache.org/repos/asf/commons-numbers.git

commit a3bd5892dd419424e6f5c09fee5b87695873614e
Author: Gilles Sadowski <[email protected]>
AuthorDate: Thu Sep 17 04:12:56 2026 +0200

    NUMBERS-215:  Iterate over partitions that contain k subsets.
---
 .../commons/numbers/combinatorics/Stirling.java    | 350 +++++++++++++++++++++
 .../numbers/combinatorics/StirlingTest.java        | 119 +++++++
 2 files changed, 469 insertions(+)

diff --git 
a/commons-numbers-combinatorics/src/main/java/org/apache/commons/numbers/combinatorics/Stirling.java
 
b/commons-numbers-combinatorics/src/main/java/org/apache/commons/numbers/combinatorics/Stirling.java
index bad11ac5..df04d8a6 100644
--- 
a/commons-numbers-combinatorics/src/main/java/org/apache/commons/numbers/combinatorics/Stirling.java
+++ 
b/commons-numbers-combinatorics/src/main/java/org/apache/commons/numbers/combinatorics/Stirling.java
@@ -17,6 +17,16 @@
 
 package org.apache.commons.numbers.combinatorics;
 
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.NoSuchElementException;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
 /**
  * Computation of <a 
href="https://en.wikipedia.org/wiki/Stirling_number";>Stirling numbers</a>.
  *
@@ -292,4 +302,344 @@ public final class Stirling {
             ((b >>> 1) * a) >>> 1 :
             ((a >>> 1) * b) >>> 1;
     }
+
+    /**
+     * From a collection of {@code n} items, generates all partitions that 
contains {@code k} subsets.
+     * The number of partitions is {@link #stirlingS2(int,int) stirlingS2(n, 
k)}.
+     * A <a 
href="https://mathworld.wolfram.com/RestrictedGrowthString.html";>restrictive 
growth string
+     * (RGS)</a> is used internally.  RGS uses integers to represent items:  
Position (index) in the
+     * RGS array is the same as in the original list to be partitioned, value 
is the "group" to which
+     * this element belongs in a given partition.
+     */
+    public static final class S2 {
+        /** Number of sublists in every partition (aka "k"). */
+        private final int numberOfSubsets;
+        /** Number of partitions. */
+        private final long stirlingS2;
+        /** Number of elements in the original list (aka "n"). */
+        private final int numberOfElements;
+        /** Difference between the number of items and the required number of 
subsets. */
+        private final int nMinusK;
+        /** Helper .*/
+        private final int nMinusOne;
+        /** Helper .*/
+        private final int kMinusOne;
+
+        /**
+         * Constructor.
+         *
+         * @param n Number of elements.
+         * @param k Number of sublists in each partition.
+         * @throws IllegalArgumentException if {@code n < 0}, {@code k < 0} or 
{@code k > n}.
+         */
+        private S2(int n,
+                   int k) {
+            stirlingS2 = stirlingS2(n, k);
+            numberOfElements = n;
+            numberOfSubsets = k;
+            nMinusK = n - k;
+            nMinusOne = n - 1;
+            kMinusOne = k - 1;
+        }
+
+        /**
+         * Factory.
+         *
+         * @param n Number of items to partition.
+         * @param k Number of sublists in each partition.
+         * @return a new instance.
+         */
+        public static S2 of(int n,
+                            int k) {
+            return new S2(n, k);
+        }
+
+        /**
+         * Get the number of partitions (aka "Stirling number of the second 
kind").
+         *
+         * @return the number of partitions.
+         */
+        public long get() {
+            return stirlingS2;
+        }
+
+        /**
+         * Iteration wrapped in a stream.
+         * This method must be kept "internal" to ensure consistency: Argument
+         * {@code gen} must be a {@link PartitionGenerator} instance tied to
+         * {@code this} instance.
+         *
+         * @param gen Partition generator.
+         * @return a stream (without duplicate or "null" elements).
+         *
+         * @param <T> Partition representation.
+         */
+        private <T> Stream<T> streamInternal(Iterable<T> gen) {
+            final int characteristics = Spliterator.DISTINCT | 
Spliterator.NONNULL;
+            return 
StreamSupport.stream(Spliterators.spliterator(gen.iterator(),
+                                                                 stirlingS2,
+                                                                 
characteristics),
+                                        false);
+        }
+
+        /**
+         * Iteration wrapped in a stream, where each element is a partition,
+         * into {@code k} subsets of a set of {@code n} elements.
+         *
+         * @return a stream (without duplicate or "null" elements).
+         */
+        public Stream<int[][]> stream() {
+            return streamInternal(partitionGenerator());
+        }
+
+        /**
+         * Iteration wrapped in a stream, where each element is a partition
+         * of the given {@code items}.
+         *
+         * @param items Items to be partitioned.
+         * @return a stream (without duplicate or "null" elements).
+         * @throws IllegalArgumentException if the number of {@code items} does
+         * not match the {@link #of(int,int) first argument of the factory 
method}.
+         *
+         * @param <T> Item type.
+         */
+        public <T> Stream<List<List<T>>> stream(List<T> items) {
+            if (items.size() != numberOfElements) {
+                throw new 
CombinatoricsException(CombinatoricsException.MISMATCH,
+                                                 numberOfElements, 
items.size());
+            }
+
+            return stream().map(o -> mapPartition(o, items));
+        }
+
+        /**
+         * Iteration wrapped in a stream.
+         *
+         * @param items Items to be partitioned.
+         * @return a stream (without duplicate or "null" elements).
+         * @throws IllegalArgumentException if the number of {@code items} does
+         * not match the {@link #of(int,int) first argument of the factory 
method}.
+         *
+         * @param <T> Item type.
+         */
+        public <T> Stream<List<List<T>>> stream(T... items) {
+            return stream(Arrays.asList(items));
+        }
+
+        /**
+         * Creates a partition generator that returns each partition as
+         * indices between {@code 0} (included) and {@code n} (excluded).
+         *
+         * @return a new instance.
+         */
+        public Iterable<int[][]> partitionGenerator() {
+            return new Iterable<int[][]>() {
+                /** {@inheritDoc} */
+                @Override
+                public Iterator<int[][]> iterator() {
+                    return new PartitionIterator();
+                }
+            };
+        }
+
+        // Commented out: Should RGS functionality be implemented in a 
dedicated class?
+        // /**
+        //  * Creates a partition generator that returns each partition as a 
list
+        //  * of {@code n} elements whose value, between {@code 0} (included) 
and
+        //  * {@code k} (excluded), indicates to which subsets that element 
belongs.
+        //  *
+        //  * @return a new instance.
+        //  */
+        // public Iterable<int[]> restrictedGrowthStringGenerator() {
+        //     return new Iterable<int[]>() {
+        //         /** {@inheritDoc} */
+        //         @Override
+        //         public Iterator<int[]> iterator() {
+        //             return new RestrictedGrowthStringIterator();
+        //         }
+        //     };
+        // }
+
+        /**
+         * Maps a given partition to a user-defined list of objects.
+         *
+         * @param p Partition.
+         * @param items List of objects.
+         * @return the mapped partition.
+         *
+         * @param <T> Item type.
+         */
+        private <T> List<List<T>> mapPartition(int[][] p,
+                                               List<T> items) {
+            final List<List<T>> out = new ArrayList<>(numberOfSubsets);
+
+            for (int[] subset : p) {
+                final List<T> customSubset = new ArrayList<>(subset.length);
+
+                for (int i : subset) {
+                    customSubset.add(items.get(i));
+                }
+
+                out.add(customSubset);
+            }
+
+            return out;
+        }
+
+        /**
+         * Iterator.
+         */
+        private final class PartitionIterator implements Iterator<int[][]> {
+            /** Delegate to RGS implementation. */
+            private final RestrictedGrowthStringIterator delegate = new 
RestrictedGrowthStringIterator();
+
+            /** {@inheritDoc} */
+            @Override
+            public boolean hasNext() {
+                return delegate.hasNext();
+            }
+
+            /** {@inheritDoc} */
+            @Override
+            public int[][] next() {
+                return rgs2partition(delegate.next());
+            }
+
+            /**
+             * Maps a given RGS to a list of indices.
+             *
+             * @param rgs RGS.
+             * @return the partition.
+             */
+            private int[][] rgs2partition(int[] rgs) {
+                // Size of every subsets of the partition described by "rgs".
+                final int[] sizes = new int[numberOfSubsets];
+                for (int i = 0; i < numberOfElements; i++) {
+                    ++sizes[rgs[i]];
+                }
+
+                final int[][] out = new int[numberOfSubsets][];
+                for (int i = 0; i < numberOfSubsets; i++) {
+                    out[i] = new int[sizes[i]];
+                }
+
+                final int[] counts = new int[numberOfSubsets];
+                for (int i = 0; i < numberOfElements; i++) {
+                    final int groupIdx = rgs[i];
+                    out[groupIdx][counts[groupIdx]++] = i;
+                }
+
+                return out;
+            }
+        }
+
+        /**
+         * Iterator.
+         */
+        private final class RestrictedGrowthStringIterator implements 
Iterator<int[]> {
+            /** RGS array (current state). */
+            private final int[] rgs = new int[numberOfElements];
+            /** RGS array (current state). */
+            private final int[] maxRgs = new int[numberOfElements];
+            /** Current partition. */
+            private final int[] currentRGS = new int[numberOfElements];
+            /** Number of generated partitions. */
+            private long partitionCount = 0;
+            /** Whether there is another partition. */
+            private boolean hasNext = true;
+
+            /**
+             * Constructor.
+             */
+            /* package-private */ RestrictedGrowthStringIterator() {
+                // Initialize the first valid lexicographical RGS matching 
k-subsets.
+                for (int i = nMinusK + 1; i < numberOfElements; i++) {
+                    rgs[i] = i - nMinusK;
+                }
+                for (int i = 1; i < numberOfElements; i++) {
+                    final int iMinusOne = i - 1;
+                    maxRgs[i] = Math.max(maxRgs[iMinusOne],
+                                         rgs[iMinusOne]);
+                }
+
+                calculateNext();
+            }
+
+            /** {@inheritDoc} */
+            @Override
+            public boolean hasNext() {
+                return hasNext;
+            }
+
+            /** {@inheritDoc} */
+            @Override
+            public int[] next() {
+                if (!hasNext) {
+                    throw new NoSuchElementException();
+                }
+
+                // Copy to prevent exposing internal data.
+                final int[] c = Arrays.copyOf(currentRGS, numberOfElements);
+
+                calculateNext();
+
+                return c;
+            }
+
+            /** Generates next partition. */
+            private void calculateNext() {
+                hasNext = partitionCount < stirlingS2;
+
+                if (numberOfElements > 0) {
+                    while (true) {
+                        if (maxRgs[nMinusOne] == kMinusOne ||
+                            rgs[nMinusOne] == kMinusOne) { // Partition 
contains "k" blocks.
+                            build();
+                            updateRgs();
+                            return;
+                        }
+
+                        updateRgs();
+                    }
+                } else {
+                    build();
+                }
+            }
+
+            /** Updates RGS. */
+            private void updateRgs() {
+                int i = nMinusOne;
+                while (i > 0) {
+                    if (rgs[i] < kMinusOne &&
+                        rgs[i] <= maxRgs[i]) {
+                        ++rgs[i];
+                        break;
+                    }
+                    --i;
+                }
+
+                if (i == 0) {
+                    return;
+                }
+
+                Arrays.fill(rgs, i + 1, numberOfElements, 0);
+
+                for (int j = i; j < numberOfElements; j++) {
+                    final int jMinusOne = j - 1;
+                    maxRgs[j] = Math.max(maxRgs[jMinusOne],
+                                         rgs[jMinusOne]);
+                }
+            }
+
+            /**
+             * Generates state to be returned by {@link #next()}.
+             */
+            private void build() {
+                System.arraycopy(rgs, 0, currentRGS, 0, numberOfElements);
+
+                // Keep count to prevent infinite loop in "calculateNext()".
+                ++partitionCount;
+            }
+        }
+    }
 }
diff --git 
a/commons-numbers-combinatorics/src/test/java/org/apache/commons/numbers/combinatorics/StirlingTest.java
 
b/commons-numbers-combinatorics/src/test/java/org/apache/commons/numbers/combinatorics/StirlingTest.java
index 67af0ca2..bab5c8a9 100644
--- 
a/commons-numbers-combinatorics/src/test/java/org/apache/commons/numbers/combinatorics/StirlingTest.java
+++ 
b/commons-numbers-combinatorics/src/test/java/org/apache/commons/numbers/combinatorics/StirlingTest.java
@@ -16,7 +16,12 @@
  */
 package org.apache.commons.numbers.combinatorics;
 
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
 import java.util.stream.Stream;
+import java.util.stream.Collectors;
 import org.apache.commons.numbers.core.ArithmeticUtils;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -360,4 +365,118 @@ class StirlingTest {
     void testStirlingS2Overflow(int n, int k) {
         Assertions.assertThrows(ArithmeticException.class, () -> 
Stirling.stirlingS2(n, k));
     }
+
+    @Test
+    void testS2Get() {
+        final int n = 23;
+        final int k = 17;
+        Assertions.assertEquals(Stirling.stirlingS2(n, k),
+                                Stirling.S2.of(n, k).get());
+    }
+
+    @Test
+    void testS2PartitionGeneration() {
+        Assertions.assertEquals(1, s2PartitionGenerator(2, 1).size());
+        Assertions.assertEquals(1, s2PartitionGenerator(2, 2).size());
+        Assertions.assertEquals(1, s2PartitionGenerator(3, 1).size());
+        Assertions.assertEquals(3, s2PartitionGenerator(3, 2).size());
+        Assertions.assertEquals(1, s2PartitionGenerator(3, 3).size());
+        Assertions.assertEquals(1, s2PartitionGenerator(4, 1).size());
+        Assertions.assertEquals(7, s2PartitionGenerator(4, 2).size());
+        Assertions.assertEquals(6, s2PartitionGenerator(4, 3).size());
+        Assertions.assertEquals(1, s2PartitionGenerator(4, 4).size());
+        Assertions.assertEquals(1, s2PartitionGenerator(5, 1).size());
+        Assertions.assertEquals(15, s2PartitionGenerator(5, 2).size());
+        Assertions.assertEquals(25, s2PartitionGenerator(5, 3).size());
+        Assertions.assertEquals(10, s2PartitionGenerator(5, 4).size());
+        Assertions.assertEquals(1, s2PartitionGenerator(5, 5).size());
+    }
+
+    @Test
+    void testS2ItemsStream() {
+        final String a = "A";
+        final String b = "B";
+        final String c = "C";
+        final List<String> items = new ArrayList<>();
+        items.add(a);
+        items.add(b);
+        items.add(c);
+        final List<List<List<String>>> out = Stirling.S2.of(3, 2)
+            .stream(items)
+            .collect(Collectors.toList());
+
+        Assertions.assertEquals(3, out.size());
+
+        List<List<String>> part = out.get(0);
+        Assertions.assertEquals(a, part.get(0).get(0));
+        Assertions.assertEquals(b, part.get(0).get(1));
+        Assertions.assertEquals(c, part.get(1).get(0));
+
+        part = out.get(1);
+        Assertions.assertEquals(a, part.get(0).get(0));
+        Assertions.assertEquals(c, part.get(0).get(1));
+        Assertions.assertEquals(b, part.get(1).get(0));
+
+        part = out.get(2);
+        Assertions.assertEquals(a, part.get(0).get(0));
+        Assertions.assertEquals(b, part.get(1).get(0));
+        Assertions.assertEquals(c, part.get(1).get(1));
+    }
+
+    @Test
+    void testS2ItemsStreamVarArgs() {
+        final int numPartitions = Stirling.S2.of(5, 2)
+            .stream("A", "B", "C", "D", "E")
+            .collect(Collectors.toList()).size();
+        Assertions.assertEquals(15, numPartitions);
+    }
+
+    @Test
+    void testS2ItemsStreamTooManyItems() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                                () -> Stirling.S2.of(4, 2).stream("A", "B", 
"C", "D", "E"));
+    }
+
+    @Test
+    void testS2ItemsStreamTooFewItems() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                                () -> Stirling.S2.of(4, 2).stream("A", "B", 
"C"));
+    }
+
+    @Test
+    void testS2PartitionGenerationEmptyList() {
+        Assertions.assertEquals(1, s2PartitionGenerator(0, 0).size());
+    }
+
+    @Test
+    void testS2MoreRequiredSubsetThanItems() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                                () -> s2PartitionGenerator(2, 3));
+    }
+
+    @Test
+    void testS2NegativeK() {
+        Assertions.assertThrows(IllegalArgumentException.class,
+                                () -> Stirling.S2.of(3, -1));
+    }
+
+    @Test
+    void testS2InvalidNext() {
+        Assertions.assertThrows(NoSuchElementException.class,
+                                () -> {
+                                    final Iterator<int[][]> s =
+                                        Stirling.S2.of(5, 
5).partitionGenerator().iterator();
+                                    s.next(); // OK.
+                                    s.next(); // Must fail.
+                                });
+    }
+
+    /**
+     * @param n Number of items.
+     * @param k Number of sublists.
+     */
+    private List<int[][]> s2PartitionGenerator(int n,
+                                               int k) {
+        return Stirling.S2.of(n, k).stream().collect(Collectors.toList());
+    }
 }

Reply via email to