This is an automated email from the ASF dual-hosted git repository. emilles pushed a commit to branch GROOVY-8499 in repository https://gitbox.apache.org/repos/asf/groovy.git
commit adc07b5daa35f64ae14b0e730b227a69aff31e96 Author: Eric Milles <[email protected]> AuthorDate: Tue Jun 13 11:33:34 2023 -0500 GROOVY-8499, GROOVY-11092: STC: `combinations` metadata --- src/main/java/groovy/util/GroovyCollections.java | 111 +++++++++++---------- .../groovy/runtime/DefaultGroovyMethods.java | 33 +++--- .../stc/ClosureParamTypeInferenceSTCTest.groovy | 13 ++- .../groovy/transform/stc/CoercionSTCTest.groovy | 14 ++- src/test/groovy/util/GroovyCollectionsTest.groovy | 42 ++++---- 5 files changed, 118 insertions(+), 95 deletions(-) diff --git a/src/main/java/groovy/util/GroovyCollections.java b/src/main/java/groovy/util/GroovyCollections.java index fc8bb8e92f..4eba93638f 100644 --- a/src/main/java/groovy/util/GroovyCollections.java +++ b/src/main/java/groovy/util/GroovyCollections.java @@ -34,50 +34,21 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; -/** - * A Collections utility class - */ public class GroovyCollections { + /** * Finds all combinations of items from the given collections. * * @param collections the given collections - * @return a List of the combinations found + * @return A list of the combinations found. * @see #combinations(Iterable) */ - public static List combinations(Object[] collections) { + public static List<List> combinations(Object[] collections) { return combinations(Arrays.asList(collections)); } /** - * Finds all non-empty subsequences of a list. - * E.g. <code>subsequences([1, 2, 3])</code> would be: - * [[1, 2, 3], [1, 3], [2, 3], [1, 2], [1], [2], [3]] - * - * @param items the List of items - * @return the subsequences from items - */ - public static <T> Set<List<T>> subsequences(List<T> items) { - // items.inject([]){ ss, h -> ss.collect { it + [h] } + ss + [[h]] } - Set<List<T>> ans = new HashSet<>(); - for (T h : items) { - Set<List<T>> next = new HashSet<>(); - for (List<T> it : ans) { - List<T> sublist = new ArrayList<>(it); - sublist.add(h); - next.add(sublist); - } - next.addAll(ans); - List<T> hlist = new ArrayList<>(); - hlist.add(h); - next.add(hlist); - ans = next; - } - return ans; - } - - /** - * Finds all combinations of items from the given Iterable aggregate of collections. + * Finds all combinations of items from the given collections. * So, <code>combinations([[true, false], [true, false]])</code> * is <code>[[true, true], [false, true], [true, false], [false, false]]</code> * and <code>combinations([['a', 'b'],[1, 2, 3]])</code> @@ -87,38 +58,38 @@ public class GroovyCollections { * If an empty collection is found within the given collections, the result will be an empty list. * * @param collections the Iterable of given collections - * @return a List of the combinations found + * @return A list of the combinations found. * @since 2.2.0 */ - public static List combinations(Iterable collections) { - List collectedCombos = new ArrayList(); - for (Object collection : collections) { - Iterable items = DefaultTypeTransformation.asCollection(collection); - if (collectedCombos.isEmpty()) { - for (Object item : items) { - List l = new ArrayList(); - l.add(item); - collectedCombos.add(l); + public static List<List> combinations(Iterable<?> collections) { + List<List<Object>> combos = new ArrayList<>(); + for (var collection : collections) { + if (combos.isEmpty()) { + for (var object : DefaultTypeTransformation.asCollection(collection)) { + List<Object> list = new ArrayList<>(); + list.add(object); + combos.add(list); } } else { - List savedCombos = new ArrayList(collectedCombos); - List newCombos = new ArrayList(); - for (Object value : items) { - for (Object savedCombo : savedCombos) { - List oldList = new ArrayList((List) savedCombo); - oldList.add(value); - newCombos.add(oldList); + List<List<Object>> next = new ArrayList<>(); // each list plus each item + for (var object : DefaultTypeTransformation.asCollection(collection)) { + for (var combo : combos) { + List<Object> list = new ArrayList<>(combo); + list.add(object); + next.add(list); } } - collectedCombos = newCombos; + combos = next; } - - if (collectedCombos.isEmpty()) + if (combos.isEmpty()) break; } - return collectedCombos; + return (List) combos; } + /** + * @since 2.5.0 + */ public static <T> List<List<T>> inits(Iterable<T> collections) { List<T> copy = DefaultGroovyMethods.toList(collections); List<List<T>> result = new ArrayList<>(); @@ -129,6 +100,9 @@ public class GroovyCollections { return result; } + /** + * @since 2.5.0 + */ public static <T> List<List<T>> tails(Iterable<T> collections) { List<T> copy = DefaultGroovyMethods.toList(collections); List<List<T>> result = new ArrayList<>(); @@ -139,6 +113,34 @@ public class GroovyCollections { return result; } + /** + * Finds all non-empty subsequences of a list. + * E.g. <code>subsequences([1, 2, 3])</code> would be: + * [[1, 2, 3], [1, 3], [2, 3], [1, 2], [1], [2], [3]] + * + * @param items the List of items + * @return the subsequences from items + * @since 1.8.0 + */ + public static <T> Set<List<T>> subsequences(List<T> items) { + // items.inject([]){ ss, h -> ss.collect { it + [h] } + ss + [[h]] } + Set<List<T>> ans = new HashSet<>(); + for (T h : items) { + Set<List<T>> next = new HashSet<>(); + for (List<T> it : ans) { + List<T> sublist = new ArrayList<>(it); + sublist.add(h); + next.add(sublist); + } + next.addAll(ans); + List<T> hlist = new ArrayList<>(); + hlist.add(h); + next.add(hlist); + ans = next; + } + return ans; + } + /** * Transposes an array of lists. * @@ -355,5 +357,4 @@ public class GroovyCollections { : new ClosureComparator<>(condition); return union(iterables, comparator); } - } diff --git a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java index 851a393ff9..8605cff657 100644 --- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java +++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyMethods.java @@ -4982,36 +4982,41 @@ public class DefaultGroovyMethods extends DefaultGroovyMethodsSupport { } /** - * Adds GroovyCollections#combinations(Iterable) as a method on Iterables. + * Finds all combinations of items from the given aggregate of collections. * <p> * Example usage: * <pre class="groovyTestCase"> - * assert [['a', 'b'],[1, 2, 3]].combinations() == [['a', 1], ['b', 1], ['a', 2], ['b', 2], ['a', 3], ['b', 3]] + * result = [['a', 'b'], [1, 2, 3]].combinations() + * assert result == [['a', 1], ['b', 1], ['a', 2], ['b', 2], ['a', 3], ['b', 3]] * </pre> * - * @param self an Iterable of collections - * @return a List of the combinations found - * @see groovy.util.GroovyCollections#combinations(java.lang.Iterable) + * @param self an iterable of collections + * @return A list of the combinations (lists). + * @see GroovyCollections#combinations(Iterable) * @since 2.2.0 */ - public static List combinations(Iterable self) { + public static List<List> combinations(Iterable self) { return GroovyCollections.combinations(self); } /** - * Adds GroovyCollections#combinations(Iterable, Closure) as a method on collections. + * Finds all combinations of items from the given aggregate of collections, + * then returns the results of the supplied transform. * <p> * Example usage: - * <pre class="groovyTestCase">assert [[2, 3],[4, 5, 6]].combinations {x,y {@code ->} x*y } == [8, 12, 10, 15, 12, 18]</pre> + * <pre class="groovyTestCase"> + * result = [[2, 3], [4, 5, 6]].combinations { x,y {@code ->} x*y } + * assert result == [8, 12, 10, 15, 12, 18] + * </pre> * - * @param self a Collection of lists - * @param function a closure to be called on each combination - * @return a List of the results of applying the closure to each combination found - * @see groovy.util.GroovyCollections#combinations(Iterable) + * @param self an iterable of collections + * @param function a closure to be called on each combination (list) + * @return A list of the results of applying the closure to each combination. + * @see GroovyCollections#combinations(Iterable) + * @see #collect(Iterable,Closure) * @since 2.2.0 */ - @SuppressWarnings("unchecked") - public static List combinations(Iterable self, Closure<?> function) { + public static <T> List<T> combinations(Iterable self, @ClosureParams(value=FromString.class, options="List") Closure<T> function) { return collect(GroovyCollections.combinations(self), function); } diff --git a/src/test/groovy/transform/stc/ClosureParamTypeInferenceSTCTest.groovy b/src/test/groovy/transform/stc/ClosureParamTypeInferenceSTCTest.groovy index f8909ba0c7..d120383055 100644 --- a/src/test/groovy/transform/stc/ClosureParamTypeInferenceSTCTest.groovy +++ b/src/test/groovy/transform/stc/ClosureParamTypeInferenceSTCTest.groovy @@ -388,10 +388,15 @@ class ClosureParamTypeInferenceSTCTest extends StaticTypeCheckingTestCase { // GROOVY-8499 void testParamCountCheck6() { - shouldFailWithMessages ''' - ['ab'.chars,'12'.chars].combinations().collect((l,n) -> "$l$n") - ''', - 'Incorrect number of parameters. Expected 1 but found 2' + assertScript ''' + def result = ['ab'.chars,'12'.chars].combinations { l,n -> "$l$n" } + assert result == ['a1','b1','a2','b2'] + ''' + // cannot know in advance how many list elements + def err = shouldFail ''' + ['ab'.chars,'12'.chars].combinations((l,n,x) -> "$l$n") + ''' + assert err =~ /No signature of method.* is applicable for argument types: \(ArrayList\)/ } // GROOVY-8816 diff --git a/src/test/groovy/transform/stc/CoercionSTCTest.groovy b/src/test/groovy/transform/stc/CoercionSTCTest.groovy index 77fa5eeaef..aa2b1a0358 100644 --- a/src/test/groovy/transform/stc/CoercionSTCTest.groovy +++ b/src/test/groovy/transform/stc/CoercionSTCTest.groovy @@ -561,10 +561,14 @@ class CoercionSTCTest extends StaticTypeCheckingTestCase { // GROOVY-11092, GROOVY-8499 void testCoerceToFunctionalInterface21() { - // TODO: if extension combinations indicates List<List<?>> this can work - shouldFailWithMessages ''' - ['ab'.chars,'12'.chars].combinations().stream().map((l,n) -> "$l$n") - ''', - 'Wrong number of parameters for method target: apply(java.lang.Object)' + assertScript ''' + def result = ['ab'.chars,'12'.chars].combinations().stream().map((l,n) -> "$l$n").toList() + assert result == ['a1','b1','a2','b2'] + ''' + // cannot know in advance how many list elements + def err = shouldFail ''' + ['ab'.chars,'12'.chars].combinations().stream().map((l,n,x) -> "").toList() + ''' + assert err =~ /No signature of method.* is applicable for argument types: \(ArrayList\)/ } } diff --git a/src/test/groovy/util/GroovyCollectionsTest.groovy b/src/test/groovy/util/GroovyCollectionsTest.groovy index 2597182bdb..4602e096ea 100644 --- a/src/test/groovy/util/GroovyCollectionsTest.groovy +++ b/src/test/groovy/util/GroovyCollectionsTest.groovy @@ -18,7 +18,7 @@ */ package groovy.util -import groovy.test.GroovyTestCase +import org.junit.Test import static groovy.util.GroovyCollections.combinations import static groovy.util.GroovyCollections.max @@ -26,34 +26,39 @@ import static groovy.util.GroovyCollections.min import static groovy.util.GroovyCollections.sum import static groovy.util.GroovyCollections.transpose -final class GroovyCollectionsTest extends GroovyTestCase { +final class GroovyCollectionsTest { - void testCombinations() { + @Test + void testCombinations0() { + // an empty iterable at any stage should result in an empty result + def input = [['a', 'b'], [1, 2, 3]] + assert combinations([[]] + input).isEmpty() + assert combinations(input + [[]]).isEmpty() + assert combinations(input + [[]] + input).isEmpty() + } + + @Test + void testCombinations1() { // use Sets because we don't care about order Set expected = [ - ['a', 1], ['a', 2], ['a', 3], - ['b', 1], ['b', 2], ['b', 3] - ] - Collection input = [['a', 'b'], [1, 2, 3]] + ['a', 1], ['a', 2], ['a', 3], ['b', 1], ['b', 2], ['b', 3] + ] + def input = [['a', 'b'], [1, 2, 3]] - // normal varargs versions should match Object[] - assert GroovyCollections.combinations(['a', 'b'], [1, 2, 3]) as Set == expected - assert combinations(['a', 'b'], [1, 2, 3]) as Set == expected + // varargs versions should match Object[] + assert GroovyCollections.combinations(input[0], input[1]) as Set == expected + assert combinations(input[0], input[1]) as Set == expected // spread versions should match Object[] assert GroovyCollections.combinations(*input) as Set == expected assert combinations(*input) as Set == expected - // collection versions should match Collection + // list versions should match Iterable assert GroovyCollections.combinations(input) as Set == expected assert combinations(input) as Set == expected - - // an empty iterable should result in no combination - assert combinations([[]] + input).isEmpty() - assert combinations(input + [[]]).isEmpty() - assert combinations(input + [[]] + input).isEmpty() } + @Test void testTranspose() { // normal varargs versions should match Object[] assert GroovyCollections.transpose(['a', 'b'], [1, 2, 3]) == [['a', 1], ['b', 2]] @@ -70,6 +75,7 @@ final class GroovyCollectionsTest extends GroovyTestCase { assert transpose([]) == [] } + @Test void testMin() { // normal varargs versions should match Object[] assert GroovyCollections.min('a', 'b') == 'a' @@ -84,6 +90,7 @@ final class GroovyCollectionsTest extends GroovyTestCase { assert min([1, 2, 3]) == 1 } + @Test void testMax() { // normal varargs versions should match Object[] assert GroovyCollections.max('a', 'b') == 'b' @@ -98,6 +105,7 @@ final class GroovyCollectionsTest extends GroovyTestCase { assert max([1, 2, 3]) == 3 } + @Test void testSum() { // normal varargs versions should match Object[] assert GroovyCollections.sum('a', 'b') == 'ab' @@ -112,7 +120,7 @@ final class GroovyCollectionsTest extends GroovyTestCase { assert sum([1, 2, 3]) == 6 } - // GROOVY-7267 + @Test // GROOVY-7267 void testHashCodeCollisionInMinus() { assert ([[1:2],[2:3]]-[["b":"a"]]) == [[1:2],[2:3]] }
