diff --git a/src/main/scala/scala/collection/next/NextIterableOnceOpsExtensions.scala b/src/main/scala/scala/collection/next/NextIterableOnceOpsExtensions.scala index 2bd687d..c675f5c 100644 --- a/src/main/scala/scala/collection/next/NextIterableOnceOpsExtensions.scala +++ b/src/main/scala/scala/collection/next/NextIterableOnceOpsExtensions.scala @@ -16,6 +16,26 @@ package next private[next] final class NextIterableOnceOpsExtensions[A, CC[_], C]( private val col: IterableOnceOps[A, CC, C] ) extends AnyVal { + import NextIterableOnceOpsExtensions.{GroupMapToView, GroupMapView} + + def groupBy[K](key: A => K)(implicit groupsFactory: Factory[A, C]): immutable.Map[K, C] = + viewGroupByTo(key).toMap + + def viewGroupByTo[K](key: A => K)(implicit groupsFactory: Factory[A, C]): GroupMapToView[A, K, A, C] = + viewGroupBy(key).collectGroupsTo(groupsFactory) + + def viewGroupBy[K](key: A => K): GroupMapView[A, K, A] = + viewGroupMap(key)(identity) + + def groupMap[K, V](key: A => K)(f: A => V)(implicit groupsFactory: Factory[V, CC[V]]): immutable.Map[K, CC[V]] = + viewGroupMapTo(key)(f).toMap + + def viewGroupMapTo[K, V](key: A => K)(f: A => V)(implicit groupsFactory: Factory[V, CC[V]]): GroupMapToView[A, K, V, CC[V]] = + viewGroupMap(key)(f).collectGroupsTo(groupsFactory) + + def viewGroupMap[K, V](key: A => K)(f: A => V): GroupMapView[A, K, V] = + new GroupMapView(col, key, f) + /** * Partitions this IterableOnce into a map according to a discriminator function `key`. All the values that * have the same discriminator are then transformed by the `value` function and then reduced into a @@ -28,14 +48,51 @@ private[next] final class NextIterableOnceOpsExtensions[A, CC[_], C]( * * @note This will force the evaluation of the Iterator. */ - def groupMapReduce[K, B](key: A => K)(f: A => B)(reduce: (B, B) => B): immutable.Map[K, B] = { - val m = mutable.Map.empty[K, B] - col.foreach { elem => - m.updateWith(key = key(elem)) { - case Some(b) => Some(reduce(b, f(elem))) - case None => Some(f(elem)) + def groupMapReduce[K, V](key: A => K)(f: A => V)(reduce: (V, V) => V): immutable.Map[K, V] = + viewGroupMap(key)(f).reduceValuesTo(immutable.Map)(reduce) +} + +private[next] object NextIterableOnceOpsExtensions { + final class GroupMapView[A, K, V] private[NextIterableOnceOpsExtensions]( + col: IterableOnceOps[A, AnyConstr, _], + key: A => K, + f: A => V + ) { + def reduceValuesTo[MC](resultFactory: Factory[(K, V), MC])(reduce: (V, V) => V): MC = { + val m = mutable.Map.empty[K, V] + col.foreach { elem => + m.updateWith(key = key(elem)) { + case Some(b) => Some(reduce(b, f(elem))) + case None => Some(f(elem)) + } + } + resultFactory.fromSpecific(m) + } + + def collectGroupsTo[C](groupsFactory: Factory[V, C]): GroupMapToView[A, K, V, C] = + new GroupMapToView(col, key, f, groupsFactory) + } + + final class GroupMapToView[A, K, V, C] private[NextIterableOnceOpsExtensions]( + col: IterableOnceOps[A, AnyConstr, _], + key: A => K, + f: A => V, + groupsFactory: Factory[V, C] + ) { + def toMap: immutable.Map[K, C] = + to(immutable.Map) + + def to[MC](resultFactory: Factory[(K, C), MC]): MC = { + val m = mutable.Map.empty[K, mutable.Builder[V, C]] + col.foreach { elem => + val k = key(elem) + val v = f(elem) + m.get(k) match { + case Some(builder) => builder.addOne(v) + case None => m.update(key = k, value = groupsFactory.newBuilder.addOne(v)) + } } + resultFactory.fromSpecific(m.view.mapValues(_.result())) } - m.to(immutable.Map) } } diff --git a/src/test/scala/scala/collection/next/TestIterableOnceExtensions.scala b/src/test/scala/scala/collection/next/TestIterableOnceExtensions.scala index 0d8962a..d626ec5 100644 --- a/src/test/scala/scala/collection/next/TestIterableOnceExtensions.scala +++ b/src/test/scala/scala/collection/next/TestIterableOnceExtensions.scala @@ -16,49 +16,172 @@ import org.junit.Assert._ import org.junit.Test import scala.collection.IterableOnceOps import scala.collection.generic.IsIterableOnce +import scala.collection.immutable.{ArraySeq, BitSet, SortedMap, SortedSet} final class TestIterableOnceExtensions { - import TestIterableOnceExtensions.LowerCaseString + import TestIterableOnceExtensions._ + // groupMapReduce -------------------------------------------- @Test def iteratorGroupMapReduce(): Unit = { - def occurrences[A](coll: IterableOnce[A]): Map[A, Int] = - coll.iterator.groupMapReduce(identity)(_ => 1)(_ + _) + def occurrences[A](data: IterableOnce[A]): Map[A, Int] = + data.iterator.groupMapReduce(identity)(_ => 1)(_ + _) - val xs = Seq('a', 'b', 'b', 'c', 'a', 'a', 'a', 'b') + val data = Seq('a', 'b', 'b', 'c', 'a', 'a', 'a', 'b') val expected = Map('a' -> 4, 'b' -> 3, 'c' -> 1) - assertEquals(expected, occurrences(xs)) + + assertEquals(expected, occurrences(data)) } @Test def iterableOnceOpsGroupMapReduce(): Unit = { - def occurrences[A, CC[_], C](coll: IterableOnceOps[A, CC, C]): Map[A, Int] = - coll.groupMapReduce(identity)(_ => 1)(_ + _) + def occurrences[A, CC[_], C](data: IterableOnceOps[A, CC, C]): Map[A, Int] = + data.groupMapReduce(identity)(_ => 1)(_ + _) - val xs = Seq('a', 'b', 'b', 'c', 'a', 'a', 'a', 'b') + val data = Seq('a', 'b', 'b', 'c', 'a', 'a', 'a', 'b') val expected = Map('a' -> 4, 'b' -> 3, 'c' -> 1) - assertEquals(expected, occurrences(xs)) + + assertEquals(expected, occurrences(data)) } @Test def anyLikeIterableOnceGroupMapReduce(): Unit = { - def occurrences[Repr](coll: Repr)(implicit it: IsIterableOnce[Repr]): Map[it.A, Int] = - it(coll).iterator.groupMapReduce(identity)(_ => 1)(_ + _) + def occurrences[Repr](data: Repr)(implicit it: IsIterableOnce[Repr]): Map[it.A, Int] = + it(data).iterator.groupMapReduce(identity)(_ => 1)(_ + _) - val xs = "abbcaaab" + val data = "abbcaaab" val expected = Map('a' -> 4, 'b' -> 3, 'c' -> 1) - assertEquals(expected, occurrences(xs)) + + assertEquals(expected, occurrences(data)) } @Test def customIterableOnceOpsGroupMapReduce(): Unit = { - def occurrences(coll: LowerCaseString): Map[Char, Int] = - coll.groupMapReduce(identity)(_ => 1)(_ + _) + def occurrences(data: LowerCaseString): Map[Char, Int] = + data.groupMapReduce(identity)(_ => 1)(_ + _) - val xs = LowerCaseString("abBcAaAb") + val data = LowerCaseString("abBcAaAb") val expected = Map('a' -> 4, 'b' -> 3, 'c' -> 1) - assertEquals(expected, occurrences(xs)) + + assertEquals(expected, occurrences(data)) + } + // ----------------------------------------------------------- + + // GroupMapGenGen -------------------------------------------- + @Test + def anyCollectionGroupMapToViewTo(): Unit = { + def getUniqueUsersByCountrySorted(data: List[Record]): List[(String, List[String])] = + data + .viewGroupMap(_.country)(_.user) + .collectGroupsTo(SortedSet) + .to(SortedMap) + .view + .mapValues(_.toList) + .toList + + val data = List( + Record(user = "Luis", country = "Colombia"), + Record(user = "Seth", country = "USA"), + Record(user = "April", country = "USA"), + Record(user = "Julien", country = "Suisse"), + Record(user = "Rob", country = "USA"), + Record(user = "Seth", country = "USA") + ) + + val expected = List( + "Colombia" -> List("Luis"), + "Suisse" -> List("Julien"), + "USA" -> List("April", "Rob", "Seth") + ) + + assertEquals(expected, getUniqueUsersByCountrySorted(data)) + } + + @Test + def anyCollectionGroupMapViewReduceValuesTo(): Unit = { + def getAllWordsByFirstLetterSorted(data: List[String]): List[(Char, String)] = + data + .viewGroupBy(_.head) + .reduceValuesTo(SortedMap)(_ ++ " " ++ _) + .toList + + val data = List( + "Autumn", + "Banana", + "April", + "Wilson", + "Apple", + "Apple", + "Winter", + "Banana" + ) + val expected = List( + 'A' -> "Autumn April Apple Apple", + 'B' -> "Banana Banana", + 'W' -> "Wilson Winter" + ) + + assertEquals(expected, getAllWordsByFirstLetterSorted(data)) + } + + @Test + def iterableOnceOpsViewGroupByToSpecificFactoryToMap(): Unit = { + def bitsByEven(data: BitSet): Map[Boolean, BitSet] = + data.viewGroupByTo(x => (x % 2) == 0).toMap + + val data = BitSet(1, 2, 3, 4, 5) + val expected = Map( + true -> BitSet(2, 4), + false -> BitSet(1, 3, 5) + ) + + assertEquals(expected, bitsByEven(data)) + } + + @Test + def iterableOnceOpsViewGroupMapToIterableFactoryToMap(): Unit = { + def bitsByEvenAsChars(data: BitSet): Map[Boolean, Set[Char]] = + data.viewGroupMapTo(x => (x % 2) == 0)(_.toChar).toMap + + val data = BitSet(100, 101, 102, 103, 104, 105) + val expected = Map( + true -> Set('d', 'f', 'h'), + false -> Set('e', 'g', 'i') + ) + + assertEquals(expected, bitsByEvenAsChars(data)) + } + + @Test + def iteratorGroupBy(): Unit = { + def getUniqueWordsByFirstLetter(data: IterableOnce[String]): List[(Char, Set[String])] = + data + .iterator + .groupBy(_.head) + .view + .mapValues(_.toSet) + .toList + + val data = List( + "Autumn", + "Banana", + "April", + "Wilson", + "Apple", + "Apple", + "Winter", + "Banana" + ) + + val expected = List( + 'A' -> Set("Apple", "April", "Autumn"), + 'B' -> Set("Banana"), + 'W' -> Set("Wilson", "Winter") + ) + + assertEquals(expected, getUniqueWordsByFirstLetter(data)) } + // ----------------------------------------------------------- } object TestIterableOnceExtensions { @@ -81,4 +204,6 @@ object TestIterableOnceExtensions { override def span(p: Char => Boolean): (String, String) = ??? override def tapEach[U](f: Char => U): String = ??? } + + final case class Record(user: String, country: String) }