From bd9daa10627d03f9e2f98f0105e207022d96eddb Mon Sep 17 00:00:00 2001 From: Arnav Sharma <2006arnavsharma@gmail.com> Date: Fri, 20 Feb 2026 09:06:30 +0000 Subject: [PATCH] Add groupMap/groupMapNem/groupMapReduce/groupMapReduceNem/groupMapReduceWith/groupMapReduceWithNem to NonEmptyVector and NonEmptySeq NonEmptyList and NonEmptyChain both expose a rich groupMap family of methods: - groupMap / groupMapNem - groupMapReduce / groupMapReduceNem - groupMapReduceWith / groupMapReduceWithNem NonEmptyVector and NonEmptySeq only had groupBy / groupByNem, creating an inconsistency across the four primary NonEmpty collection types. This commit brings NonEmptyVector and NonEmptySeq up to parity by adding all six missing methods to each, following the same implementation pattern used in NonEmptyList (explicit TreeMap accumulation loop) and the same scaladoc style used throughout the codebase. --- .../main/scala/cats/data/NonEmptySeq.scala | 157 ++++++++++++++++++ .../main/scala/cats/data/NonEmptyVector.scala | 157 ++++++++++++++++++ 2 files changed, 314 insertions(+) diff --git a/core/src/main/scala/cats/data/NonEmptySeq.scala b/core/src/main/scala/cats/data/NonEmptySeq.scala index a4945c4eef..acd76636d1 100644 --- a/core/src/main/scala/cats/data/NonEmptySeq.scala +++ b/core/src/main/scala/cats/data/NonEmptySeq.scala @@ -349,6 +349,163 @@ final class NonEmptySeq[+A] private (val toSeq: Seq[A]) extends AnyVal with NonE final def groupByNem[B](f: A => B)(implicit B: Order[B]): NonEmptyMap[B, NonEmptySeq[A]] = NonEmptyMap.fromMapUnsafe(groupBy(f)) + /** + * Groups elements inside this `NonEmptySeq` according to the `Order` + * of the keys produced by the given key function. + * And each element in a group is transformed into a value of type B + * using the mapping function. + * + * {{{ + * scala> import scala.collection.immutable.SortedMap + * scala> import cats.data.NonEmptySeq + * scala> import cats.syntax.all._ + * scala> val neSeq = NonEmptySeq.of(12, -2, 3, -5) + * scala> val expectedResult = SortedMap(false -> NonEmptySeq.of("-2", "-5"), true -> NonEmptySeq.of("12", "3")) + * scala> val result = neSeq.groupMap(_ >= 0)(_.toString) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMap[K, B](key: A => K)(f: A => B)(implicit K: Order[K]): SortedMap[K, NonEmptySeq[B]] = { + implicit val ordering: Ordering[K] = K.toOrdering + var m = TreeMap.empty[K, mutable.Builder[B, Seq[B]]] + + for { elem <- toSeq } { + val k = key(elem) + + m.get(k) match { + case None => m += ((k, Seq.newBuilder[B] += f(elem))) + case Some(builder) => builder += f(elem) + } + } + + m.map { case (k, v) => + (k, NonEmptySeq.fromSeqUnsafe(v.result())) + }: TreeMap[K, NonEmptySeq[B]] + } + + /** + * Groups elements inside this `NonEmptySeq` according to the `Order` + * of the keys produced by the given key function. + * And each element in a group is transformed into a value of type B + * using the mapping function. + * + * {{{ + * scala> import cats.data.{NonEmptyMap, NonEmptySeq} + * scala> import cats.syntax.all._ + * scala> val neSeq = NonEmptySeq.of(12, -2, 3, -5) + * scala> val expectedResult = NonEmptyMap.of(false -> NonEmptySeq.of("-2", "-5"), true -> NonEmptySeq.of("12", "3")) + * scala> val result = neSeq.groupMapNem(_ >= 0)(_.toString) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMapNem[K, B](key: A => K)(f: A => B)(implicit K: Order[K]): NonEmptyMap[K, NonEmptySeq[B]] = + NonEmptyMap.fromMapUnsafe(groupMap(key)(f)) + + /** + * Groups elements inside this `NonEmptySeq` according to the `Order` + * of the keys produced by the given key function. + * Then each element in a group is transformed into a value of type B + * using the mapping function. + * And finally they are all reduced into a single value + * using their `Semigroup`. + * + * {{{ + * scala> import scala.collection.immutable.SortedMap + * scala> import cats.data.NonEmptySeq + * scala> import cats.syntax.all._ + * scala> val neSeq = NonEmptySeq.of("Hello", "World", "Goodbye", "World") + * scala> val expectedResult = SortedMap("goodbye" -> 1, "hello" -> 1, "world" -> 2) + * scala> val result = neSeq.groupMapReduce(_.trim.toLowerCase)(_ => 1) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMapReduce[K, B](key: A => K)(f: A => B)(implicit K: Order[K], B: Semigroup[B]): SortedMap[K, B] = + groupMapReduceWith(key)(f)(B.combine) + + /** + * Groups elements inside this `NonEmptySeq` according to the `Order` + * of the keys produced by the given key function. + * Then each element in a group is transformed into a value of type B + * using the mapping function. + * And finally they are all reduced into a single value + * using their `Semigroup`. + * + * {{{ + * scala> import cats.data.{NonEmptyMap, NonEmptySeq} + * scala> import cats.syntax.all._ + * scala> val neSeq = NonEmptySeq.of("Hello", "World", "Goodbye", "World") + * scala> val expectedResult = NonEmptyMap.of("goodbye" -> 1, "hello" -> 1, "world" -> 2) + * scala> val result = neSeq.groupMapReduceNem(_.trim.toLowerCase)(_ => 1) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMapReduceNem[K, B](key: A => K)(f: A => B)(implicit K: Order[K], B: Semigroup[B]): NonEmptyMap[K, B] = + NonEmptyMap.fromMapUnsafe(groupMapReduce(key)(f)) + + /** + * Groups elements inside this `NonEmptySeq` according to the `Order` + * of the keys produced by the given key function. + * Then each element in a group is transformed into a value of type B + * using the mapping function. + * And finally they are all reduced into a single value + * using the provided combine function. + * + * {{{ + * scala> import scala.collection.immutable.SortedMap + * scala> import cats.data.NonEmptySeq + * scala> import cats.syntax.all._ + * scala> val neSeq = NonEmptySeq.of("Hello", "World", "Goodbye", "World") + * scala> val expectedResult = SortedMap("goodbye" -> 1, "hello" -> 1, "world" -> 2) + * scala> val result = neSeq.groupMapReduceWith(_.trim.toLowerCase)(_ => 1)(_ + _) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMapReduceWith[K, B](key: A => K)(f: A => B)(combine: (B, B) => B)(implicit + K: Order[K] + ): SortedMap[K, B] = { + implicit val ordering: Ordering[K] = K.toOrdering + var m = TreeMap.empty[K, B] + + for { elem <- toSeq } { + val k = key(elem) + + m.get(k) match { + case Some(b) => m = m.updated(key = k, value = combine(b, f(elem))) + case None => m += (k -> f(elem)) + } + } + + m + } + + /** + * Groups elements inside this `NonEmptySeq` according to the `Order` + * of the keys produced by the given key function. + * Then each element in a group is transformed into a value of type B + * using the mapping function. + * And finally they are all reduced into a single value + * using the provided combine function. + * + * {{{ + * scala> import cats.data.{NonEmptyMap, NonEmptySeq} + * scala> import cats.syntax.all._ + * scala> val neSeq = NonEmptySeq.of("Hello", "World", "Goodbye", "World") + * scala> val expectedResult = NonEmptyMap.of("goodbye" -> 1, "hello" -> 1, "world" -> 2) + * scala> val result = neSeq.groupMapReduceWithNem(_.trim.toLowerCase)(_ => 1)(_ + _) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMapReduceWithNem[K, B](key: A => K)(f: A => B)(combine: (B, B) => B)(implicit + K: Order[K] + ): NonEmptyMap[K, B] = + NonEmptyMap.fromMapUnsafe(groupMapReduceWith(key)(f)(combine)) + /** * Partitions elements in fixed size `NonEmptySeq`s. * diff --git a/core/src/main/scala/cats/data/NonEmptyVector.scala b/core/src/main/scala/cats/data/NonEmptyVector.scala index b6ccec8e2a..eba2500b9e 100644 --- a/core/src/main/scala/cats/data/NonEmptyVector.scala +++ b/core/src/main/scala/cats/data/NonEmptyVector.scala @@ -342,6 +342,163 @@ final class NonEmptyVector[+A] private (val toVector: Vector[A]) final def groupByNem[B](f: A => B)(implicit B: Order[B]): NonEmptyMap[B, NonEmptyVector[A]] = NonEmptyMap.fromMapUnsafe(groupBy(f)) + /** + * Groups elements inside this `NonEmptyVector` according to the `Order` + * of the keys produced by the given key function. + * And each element in a group is transformed into a value of type B + * using the mapping function. + * + * {{{ + * scala> import scala.collection.immutable.SortedMap + * scala> import cats.data.NonEmptyVector + * scala> import cats.syntax.all._ + * scala> val nev = NonEmptyVector.of(12, -2, 3, -5) + * scala> val expectedResult = SortedMap(false -> NonEmptyVector.of("-2", "-5"), true -> NonEmptyVector.of("12", "3")) + * scala> val result = nev.groupMap(_ >= 0)(_.toString) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMap[K, B](key: A => K)(f: A => B)(implicit K: Order[K]): SortedMap[K, NonEmptyVector[B]] = { + implicit val ordering: Ordering[K] = K.toOrdering + var m = TreeMap.empty[K, mutable.Builder[B, Vector[B]]] + + for { elem <- toVector } { + val k = key(elem) + + m.get(k) match { + case None => m += ((k, Vector.newBuilder[B] += f(elem))) + case Some(builder) => builder += f(elem) + } + } + + m.map { case (k, v) => + (k, NonEmptyVector.fromVectorUnsafe(v.result())) + }: TreeMap[K, NonEmptyVector[B]] + } + + /** + * Groups elements inside this `NonEmptyVector` according to the `Order` + * of the keys produced by the given key function. + * And each element in a group is transformed into a value of type B + * using the mapping function. + * + * {{{ + * scala> import cats.data.{NonEmptyMap, NonEmptyVector} + * scala> import cats.syntax.all._ + * scala> val nev = NonEmptyVector.of(12, -2, 3, -5) + * scala> val expectedResult = NonEmptyMap.of(false -> NonEmptyVector.of("-2", "-5"), true -> NonEmptyVector.of("12", "3")) + * scala> val result = nev.groupMapNem(_ >= 0)(_.toString) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMapNem[K, B](key: A => K)(f: A => B)(implicit K: Order[K]): NonEmptyMap[K, NonEmptyVector[B]] = + NonEmptyMap.fromMapUnsafe(groupMap(key)(f)) + + /** + * Groups elements inside this `NonEmptyVector` according to the `Order` + * of the keys produced by the given key function. + * Then each element in a group is transformed into a value of type B + * using the mapping function. + * And finally they are all reduced into a single value + * using their `Semigroup`. + * + * {{{ + * scala> import scala.collection.immutable.SortedMap + * scala> import cats.data.NonEmptyVector + * scala> import cats.syntax.all._ + * scala> val nev = NonEmptyVector.of("Hello", "World", "Goodbye", "World") + * scala> val expectedResult = SortedMap("goodbye" -> 1, "hello" -> 1, "world" -> 2) + * scala> val result = nev.groupMapReduce(_.trim.toLowerCase)(_ => 1) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMapReduce[K, B](key: A => K)(f: A => B)(implicit K: Order[K], B: Semigroup[B]): SortedMap[K, B] = + groupMapReduceWith(key)(f)(B.combine) + + /** + * Groups elements inside this `NonEmptyVector` according to the `Order` + * of the keys produced by the given key function. + * Then each element in a group is transformed into a value of type B + * using the mapping function. + * And finally they are all reduced into a single value + * using their `Semigroup`. + * + * {{{ + * scala> import cats.data.{NonEmptyMap, NonEmptyVector} + * scala> import cats.syntax.all._ + * scala> val nev = NonEmptyVector.of("Hello", "World", "Goodbye", "World") + * scala> val expectedResult = NonEmptyMap.of("goodbye" -> 1, "hello" -> 1, "world" -> 2) + * scala> val result = nev.groupMapReduceNem(_.trim.toLowerCase)(_ => 1) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMapReduceNem[K, B](key: A => K)(f: A => B)(implicit K: Order[K], B: Semigroup[B]): NonEmptyMap[K, B] = + NonEmptyMap.fromMapUnsafe(groupMapReduce(key)(f)) + + /** + * Groups elements inside this `NonEmptyVector` according to the `Order` + * of the keys produced by the given key function. + * Then each element in a group is transformed into a value of type B + * using the mapping function. + * And finally they are all reduced into a single value + * using the provided combine function. + * + * {{{ + * scala> import scala.collection.immutable.SortedMap + * scala> import cats.data.NonEmptyVector + * scala> import cats.syntax.all._ + * scala> val nev = NonEmptyVector.of("Hello", "World", "Goodbye", "World") + * scala> val expectedResult = SortedMap("goodbye" -> 1, "hello" -> 1, "world" -> 2) + * scala> val result = nev.groupMapReduceWith(_.trim.toLowerCase)(_ => 1)(_ + _) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMapReduceWith[K, B](key: A => K)(f: A => B)(combine: (B, B) => B)(implicit + K: Order[K] + ): SortedMap[K, B] = { + implicit val ordering: Ordering[K] = K.toOrdering + var m = TreeMap.empty[K, B] + + for { elem <- toVector } { + val k = key(elem) + + m.get(k) match { + case Some(b) => m = m.updated(key = k, value = combine(b, f(elem))) + case None => m += (k -> f(elem)) + } + } + + m + } + + /** + * Groups elements inside this `NonEmptyVector` according to the `Order` + * of the keys produced by the given key function. + * Then each element in a group is transformed into a value of type B + * using the mapping function. + * And finally they are all reduced into a single value + * using the provided combine function. + * + * {{{ + * scala> import cats.data.{NonEmptyMap, NonEmptyVector} + * scala> import cats.syntax.all._ + * scala> val nev = NonEmptyVector.of("Hello", "World", "Goodbye", "World") + * scala> val expectedResult = NonEmptyMap.of("goodbye" -> 1, "hello" -> 1, "world" -> 2) + * scala> val result = nev.groupMapReduceWithNem(_.trim.toLowerCase)(_ => 1)(_ + _) + * scala> result === expectedResult + * res0: Boolean = true + * }}} + */ + final def groupMapReduceWithNem[K, B](key: A => K)(f: A => B)(combine: (B, B) => B)(implicit + K: Order[K] + ): NonEmptyMap[K, B] = + NonEmptyMap.fromMapUnsafe(groupMapReduceWith(key)(f)(combine)) + /** * Partitions elements in fixed size `NonEmptyVector`s. *