diff --git a/core/src/main/java/fj/data/List.java b/core/src/main/java/fj/data/List.java index a957bc5d..c60363c3 100644 --- a/core/src/main/java/fj/data/List.java +++ b/core/src/main/java/fj/data/List.java @@ -1237,6 +1237,112 @@ public final A mode(final Ord o) { return sort(o).group(o.equal()).maximum(intOrd.comap(List.length_())).head(); } + /** + * Groups the elements of this list by a given keyFunction into a {@link TreeMap}. + * The ordering of the keys is determined by {@link fj.Ord#hashOrd()}. + * + * @param keyFunction The function to select the keys for the map. + * @return A TreeMap containing the keys with the accumulated list of matched elements. + */ + public final TreeMap> groupBy(final F keyFunction) { + return groupBy(keyFunction, Ord.hashOrd()); + } + + /** + * Groups the elements of this list by a given keyFunction into a {@link TreeMap}. + * + * @param keyFunction The function to select the keys for the map. + * @param keyOrd An order for the keys of the tree map. + * @return A TreeMap containing the keys with the accumulated list of matched elements. + */ + public final TreeMap> groupBy(final F keyFunction, final Ord keyOrd) { + return groupBy(keyFunction, Function.identity(), keyOrd); + } + + /** + * Groups the elements of this list by a given keyFunction into a {@link TreeMap} and transforms + * the matching elements with the given valueFunction. The ordering of the keys is determined by + * {@link fj.Ord#hashOrd()}. + * + * @param keyFunction The function to select the keys for the map. + * @param valueFunction The function to apply on each matching value. + * @return A TreeMap containing the keys with the accumulated list of matched and mapped elements. + */ + public final TreeMap> groupBy( + final F keyFunction, + final F valueFunction) { + return this.groupBy(keyFunction, valueFunction, Ord.hashOrd()); + } + + /** + * Groups the elements of this list by a given keyFunction into a {@link TreeMap} and transforms + * the matching elements with the given valueFunction. The ordering of the keys is determined by + * the keyOrd parameter. + * + * @param keyFunction The function to select the keys for the map. + * @param valueFunction The function to apply on each matching value. + * @param keyOrd An order for the keys of the tree map. + * @return A TreeMap containing the keys with the accumulated list of matched and mapped elements. + */ + public final TreeMap> groupBy( + final F keyFunction, + final F valueFunction, + final Ord keyOrd) { + return this.groupBy(keyFunction, valueFunction, List.nil(), List::cons, keyOrd); + } + + /** + * Groups the elements of this list by a given keyFunction into a {@link TreeMap} and transforms + * the matching elements with the given valueFunction. The ordering of the keys is determined by + * the keyOrd parameter. + * + * @param keyFunction The function to select the keys for the map. + * @param valueFunction The function to apply on each matching value. + * @param monoid A monoid, which defines the accumulator for the values and the zero value. + * @param keyOrd An order for the keys of the tree map. + * @return A TreeMap containing the keys with the accumulated list of matched and mapped elements. + */ + public final TreeMap groupBy( + final F keyFunction, + final F valueFunction, + final Monoid monoid, + final Ord keyOrd) { + return groupBy(keyFunction, valueFunction, monoid.zero(), + Function.uncurryF2(monoid.sum()), keyOrd); + } + + /** + * Groups the elements of this list by a given keyFunction, applies the valueFunction and + * accumulates the mapped values with the given grouping accumulator function on the grouping + * identity. + * + * @param keyFunction The function to select the keys. + * @param valueFunction The function to apply on each element. + * @param groupingIdentity The identity, or start value, for the grouping. + * @param groupingAcc The accumulator to apply on each matching value. + * @param keyOrd An order for the keys of the tree map. + * @return A TreeMap containing the keys with the accumulated result of matched and mapped + * elements. + */ + public final TreeMap groupBy( + final F keyFunction, + final F valueFunction, + final D groupingIdentity, + final F2 groupingAcc, + final Ord keyOrd) { + return this.foldLeft(map -> element -> { + final B key = keyFunction.f(element); + final C value = valueFunction.f(element); + map.set(key, map.get(key) + .map(existing -> groupingAcc.f(value, existing)) + .orSome(groupingAcc.f(value, groupingIdentity))); + return map; + }, TreeMap.empty(keyOrd) + ); + } + + + /** * Returns whether or not all elements in the list are equal according to the given equality test. * diff --git a/demo/src/main/java/fj/demo/List_groupBy.java b/demo/src/main/java/fj/demo/List_groupBy.java new file mode 100644 index 00000000..1dc8866d --- /dev/null +++ b/demo/src/main/java/fj/demo/List_groupBy.java @@ -0,0 +1,48 @@ +package fj.demo; + +import fj.Function; +import fj.Ord; +import fj.data.List; +import fj.data.TreeMap; + +import static fj.data.List.list; + +public final class List_groupBy { + + public static void main(final String... args) { + keyDemo(); + keyValueDemo(); + keyValueAccDemo(); + } + + private static void keyDemo() { + System.out.println("KeyDemo"); + final List words = list("Hello", "World", "how", "are", "your", "doing"); + final TreeMap> lengthMap = words.groupBy(String::length); + + lengthMap.forEach(entry -> { + System.out.println(String.format("Words with %d chars: %s", entry._1(), entry._2())); + }); + } + + private static void keyValueDemo() { + System.out.println("KeyValueDemo"); + final List xs = list(1, 2, 3, 4, 5, 6, 7, 8, 9); + final TreeMap> result = xs.groupBy(x -> x % 3, Integer::toBinaryString); + + result.forEach(entry -> { + System.out.println(String.format("Numbers with reminder %d are %s", entry._1(), entry._2())); + }); + } + + private static void keyValueAccDemo() { + System.out.println("KeyValueAccDemo"); + final List words = list("Hello", "World", "how", "are", "your", "doing"); + final TreeMap lengthCounts = + words.groupBy(String::length, Function.identity(), 0, (word, sum) -> sum + 1, Ord.hashOrd()); + + lengthCounts.forEach(entry -> { + System.out.println(String.format("Words with %d chars: %s", entry._1(), entry._2())); + }); + } +} diff --git a/tests/src/test/scala/fj/data/CheckList.scala b/tests/src/test/scala/fj/data/CheckList.scala index 807e1293..90e7b1d7 100644 --- a/tests/src/test/scala/fj/data/CheckList.scala +++ b/tests/src/test/scala/fj/data/CheckList.scala @@ -1,6 +1,9 @@ package fj package data +import java.lang + +import fj.Monoid import org.scalacheck.Prop._ import ArbitraryList.arbitraryList import ArbitraryP.arbitraryP1 @@ -174,6 +177,25 @@ object CheckList extends Properties("List") { a.foldRight((a: List[String], b: List[String]) => a.append(b), nil[String]), join(a))) + property("groupBy") = forAll((a: List[Int]) => { + val result = a.groupBy((x: Int) => (x % 2 == 0): lang.Boolean) + result.get(true).forall((xs: List[Int]) => xs.forall((x: Int) => (x % 2 == 0): lang.Boolean): lang.Boolean) && + result.get(false).forall((xs: List[Int]) => xs.forall((x: Int) => (x % 2 != 0): lang.Boolean): lang.Boolean) + }) + + property("groupByMonoid") = forAll((a: List[Int]) => { + val result = a.groupBy((x: Int) => (x % 2 == 0): lang.Boolean, (x: Int) => x: lang.Integer, Monoid.intAdditionMonoid, Ord.booleanOrd) + result.get(true).forall((x: lang.Integer) => + x == a.filter((x: Int) => (x % 2 == 0): lang.Boolean). + map((x: Int) => x:lang.Integer). + foldLeft(Function.uncurryF2[lang.Integer, lang.Integer, lang.Integer](Monoid.intAdditionMonoid.sum), Monoid.intAdditionMonoid.zero()): lang.Boolean) && + result.get(false).forall((x: lang.Integer) => + x == a.filter((x: Int) => (x % 2 != 0): lang.Boolean). + map((x: Int) => x:lang.Integer). + foldLeft(Function.uncurryF2[lang.Integer, lang.Integer, lang.Integer](Monoid.intAdditionMonoid.sum), Monoid.intAdditionMonoid.zero()): lang.Boolean) + }) + + /*property("iterateWhile") = forAll((n: Int) => n > 0 ==> (iterateWhile(((x:Int) => x - 1), ((x:Int) => ((x > 0): java.lang.Boolean)), n).length == n))*/ }