From ef76701bf3d3dab5bb01df4c87615838cc935340 Mon Sep 17 00:00:00 2001 From: satorg Date: Thu, 24 Jun 2021 21:54:12 -0700 Subject: [PATCH 01/12] add missing docs for `toNev` --- core/src/main/scala/cats/syntax/vector.scala | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/src/main/scala/cats/syntax/vector.scala b/core/src/main/scala/cats/syntax/vector.scala index 7214fbef2a..086f206602 100644 --- a/core/src/main/scala/cats/syntax/vector.scala +++ b/core/src/main/scala/cats/syntax/vector.scala @@ -7,5 +7,23 @@ trait VectorSyntax { } final class VectorOps[A](private val va: Vector[A]) extends AnyVal { + + /** + * Returns an Option of NonEmptyVector from a Vector + * + * Example: + * {{{ + * scala> import cats.data.NonEmptyVector + * scala> import cats.implicits._ + * + * scala> val result1: Vector[Int] = Vector(1, 2) + * scala> result1.toNev + * res0: Option[NonEmptyVector[Int]] = Some(NonEmptyVector(1, 2)) + * + * scala> val result2: Vector[Int] = Vector.empty[Int] + * scala> result2.toNev + * res1: Option[NonEmptyVector[Int]] = None + * }}} + */ def toNev: Option[NonEmptyVector[A]] = NonEmptyVector.fromVector(va) } From 2a1040472fca4d60e9eac50e339a85fb0cee89d4 Mon Sep 17 00:00:00 2001 From: satorg Date: Thu, 24 Jun 2021 21:56:13 -0700 Subject: [PATCH 02/12] add `groupByNev` --- core/src/main/scala/cats/syntax/vector.scala | 25 +++++++++++++++++++ .../test/scala/cats/tests/VectorSuite.scala | 6 +++++ 2 files changed, 31 insertions(+) diff --git a/core/src/main/scala/cats/syntax/vector.scala b/core/src/main/scala/cats/syntax/vector.scala index 086f206602..6542acb60d 100644 --- a/core/src/main/scala/cats/syntax/vector.scala +++ b/core/src/main/scala/cats/syntax/vector.scala @@ -1,7 +1,10 @@ package cats.syntax +import cats.Order import cats.data.NonEmptyVector +import scala.collection.immutable.SortedMap + trait VectorSyntax { implicit final def catsSyntaxVectors[A](va: Vector[A]): VectorOps[A] = new VectorOps(va) } @@ -26,4 +29,26 @@ final class VectorOps[A](private val va: Vector[A]) extends AnyVal { * }}} */ def toNev: Option[NonEmptyVector[A]] = NonEmptyVector.fromVector(va) + + /** + * Groups elements inside this `Vector` according to the `Order` of the keys + * produced by the given mapping function. + * + * {{{ + * scala> import cats.data.NonEmptyVector + * scala> import scala.collection.immutable.SortedMap + * scala> import cats.implicits._ + * + * scala> val vector = Vector(12, -2, 3, -5) + * + * scala> val expectedResult = SortedMap(false -> NonEmptyVector.of(-2, -5), true -> NonEmptyVector.of(12, 3)) + * + * scala> vector.groupByNev(_ >= 0) === expectedResult + * res0: Boolean = true + * }}} + */ + def groupByNev[B](f: A => B)(implicit B: Order[B]): SortedMap[B, NonEmptyVector[A]] = { + implicit val ordering: Ordering[B] = B.toOrdering + toNev.fold(SortedMap.empty[B, NonEmptyVector[A]])(_.groupBy(f)) + } } diff --git a/tests/src/test/scala/cats/tests/VectorSuite.scala b/tests/src/test/scala/cats/tests/VectorSuite.scala index 67fc488582..61300e0f0f 100644 --- a/tests/src/test/scala/cats/tests/VectorSuite.scala +++ b/tests/src/test/scala/cats/tests/VectorSuite.scala @@ -67,6 +67,12 @@ class VectorSuite extends CatsSuite { assert(Vector.empty[Int].toNev == None) } + test("groupByNev should be consistent with groupBy")( + forAll { (fa: Vector[Int], f: Int => Int) => + assert((fa.groupByNev(f).map { case (k, v) => (k, v.toVector) }: Map[Int, Vector[Int]]) === fa.groupBy(f)) + } + ) + test("traverse is stack-safe") { val vec = (0 until 100000).toVector val sumAll = Traverse[Vector] From 8437bdca96226d3b80db3f87824c2fec926683e5 Mon Sep 17 00:00:00 2001 From: satorg Date: Thu, 24 Jun 2021 22:43:44 -0700 Subject: [PATCH 03/12] add `groupByNevA` --- core/src/main/scala/cats/syntax/vector.scala | 33 ++++++++++++++++++- .../test/scala/cats/tests/VectorSuite.scala | 6 ++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/syntax/vector.scala b/core/src/main/scala/cats/syntax/vector.scala index 6542acb60d..5f86ffc96e 100644 --- a/core/src/main/scala/cats/syntax/vector.scala +++ b/core/src/main/scala/cats/syntax/vector.scala @@ -1,7 +1,7 @@ package cats.syntax -import cats.Order import cats.data.NonEmptyVector +import cats.{Applicative, Functor, Order, Traverse} import scala.collection.immutable.SortedMap @@ -51,4 +51,35 @@ final class VectorOps[A](private val va: Vector[A]) extends AnyVal { implicit val ordering: Ordering[B] = B.toOrdering toNev.fold(SortedMap.empty[B, NonEmptyVector[A]])(_.groupBy(f)) } + + /** + * Groups elements inside this `Vector` according to the `Order` of the keys + * produced by the given mapping monadic function. + * + * {{{ + * scala> import cats.data.NonEmptyVector + * scala> import scala.collection.immutable.SortedMap + * scala> import cats.implicits._ + * + * scala> val vector = Vector(12, -2, 3, -5) + * + * scala> val expectedResult = Option(SortedMap(false -> NonEmptyVector.of(-2, -5), true -> NonEmptyVector.of(12, 3))) + * + * scala> vector.groupByNevA(num => Option(0).map(num >= _)) === expectedResult + * res0: Boolean = true + * }}} + */ + def groupByNevA[F[_], B]( + f: A => F[B] + )(implicit F: Applicative[F], B: Order[B]): F[SortedMap[B, NonEmptyVector[A]]] = { + implicit val ordering: Ordering[B] = B.toOrdering + val mapFunctor = Functor[SortedMap[B, *]] + val nevTraverse = Traverse[NonEmptyVector] + + toNev.fold(F.pure(SortedMap.empty[B, NonEmptyVector[A]])) { nev => + F.map(nevTraverse.traverse(nev)(a => F.tupleLeft(f(a), a))) { vector => + mapFunctor.map(vector.groupBy(_._2))(_.map(_._1)) + } + } + } } diff --git a/tests/src/test/scala/cats/tests/VectorSuite.scala b/tests/src/test/scala/cats/tests/VectorSuite.scala index 61300e0f0f..38129675e1 100644 --- a/tests/src/test/scala/cats/tests/VectorSuite.scala +++ b/tests/src/test/scala/cats/tests/VectorSuite.scala @@ -73,6 +73,12 @@ class VectorSuite extends CatsSuite { } ) + test("groupByNevA should be consistent with groupByNev")( + forAll { (fa: Vector[Int], f: Int => Int) => + assert(fa.groupByNevA(f.andThen(Option(_))) === Option(fa.groupByNev(f))) + } + ) + test("traverse is stack-safe") { val vec = (0 until 100000).toVector val sumAll = Traverse[Vector] From 21c99aa1ff8759628a6cab0ce9628d2df4d36070 Mon Sep 17 00:00:00 2001 From: satorg Date: Thu, 24 Jun 2021 23:14:20 -0700 Subject: [PATCH 04/12] add `scanLeftNev` and `scanRightNev` --- core/src/main/scala/cats/syntax/vector.scala | 42 +++++++++++++++++++ .../test/scala/cats/tests/VectorSuite.scala | 12 ++++++ 2 files changed, 54 insertions(+) diff --git a/core/src/main/scala/cats/syntax/vector.scala b/core/src/main/scala/cats/syntax/vector.scala index 5f86ffc96e..9185f29927 100644 --- a/core/src/main/scala/cats/syntax/vector.scala +++ b/core/src/main/scala/cats/syntax/vector.scala @@ -82,4 +82,46 @@ final class VectorOps[A](private val va: Vector[A]) extends AnyVal { } } } + + /** + * Produces a `NonEmptyVector` containing cumulative results of applying the + * operator going left to right. + * + * Example: + * {{{ + * scala> import cats.data.NonEmptyVector + * scala> import cats.implicits._ + * + * scala> val result1: Vector[Int] = Vector(1, 2) + * scala> result1.scanLeftNev(100)(_ + _) + * res0: NonEmptyVector[Int] = NonEmptyVector(100, 101, 103) + * + * scala> val result2: Vector[Int] = Vector.empty[Int] + * scala> result2.scanLeftNev(1)(_ + _) + * res1: NonEmptyVector[Int] = NonEmptyVector(1) + * }}} + */ + def scanLeftNev[B](b: B)(f: (B, A) => B): NonEmptyVector[B] = + NonEmptyVector.fromVectorUnsafe(va.scanLeft(b)(f)) + + /** + * Produces a `NonEmptyVector` containing cumulative results of applying the + * operator going right to left. + * + * Example: + * {{{ + * scala> import cats.data.NonEmptyVector + * scala> import cats.implicits._ + * + * scala> val result1: Vector[Int] = Vector(1, 2) + * scala> result1.scanRightNev(100)(_ + _) + * res0: NonEmptyVector[Int] = NonEmptyVector(103, 102, 100) + * + * scala> val result2: Vector[Int] = Vector.empty[Int] + * scala> result2.scanRightNev(1)(_ + _) + * res1: NonEmptyVector[Int] = NonEmptyVector(1) + * }}} + */ + def scanRightNev[B](b: B)(f: (A, B) => B): NonEmptyVector[B] = + NonEmptyVector.fromVectorUnsafe(va.scanRight(b)(f)) } diff --git a/tests/src/test/scala/cats/tests/VectorSuite.scala b/tests/src/test/scala/cats/tests/VectorSuite.scala index 38129675e1..8581022f65 100644 --- a/tests/src/test/scala/cats/tests/VectorSuite.scala +++ b/tests/src/test/scala/cats/tests/VectorSuite.scala @@ -79,6 +79,18 @@ class VectorSuite extends CatsSuite { } ) + test("scanLeftNev should be consistent with scanLeft")( + forAll { (fa: Vector[Int], b: Int, f: (Int, Int) => Int) => + assert(fa.scanLeftNev(b)(f).toVector === fa.scanLeft(b)(f)) + } + ) + + test("scanRightNev should be consistent with scanRight")( + forAll { (fa: Vector[Int], b: Int, f: (Int, Int) => Int) => + assert(fa.scanRightNev(b)(f).toVector === fa.scanRight(b)(f)) + } + ) + test("traverse is stack-safe") { val vec = (0 until 100000).toVector val sumAll = Traverse[Vector] From 19f5a891ac4fa1af769ab686806c30cefe2200a7 Mon Sep 17 00:00:00 2001 From: satorg Date: Fri, 25 Jun 2021 16:25:12 -0700 Subject: [PATCH 05/12] add tests for the `ListOps`'s `scanLeftNel` and `scanRightNel` --- tests/src/test/scala/cats/tests/ListSuite.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/src/test/scala/cats/tests/ListSuite.scala b/tests/src/test/scala/cats/tests/ListSuite.scala index 1a9680632a..ff17f0ca22 100644 --- a/tests/src/test/scala/cats/tests/ListSuite.scala +++ b/tests/src/test/scala/cats/tests/ListSuite.scala @@ -70,6 +70,18 @@ class ListSuite extends CatsSuite { } ) + test("scanLeftNel should be consistent with scanLeft")( + forAll { (fa: List[Int], b: Int, f: (Int, Int) => Int) => + assert(fa.scanLeftNel(b)(f).toList === fa.scanLeft(b)(f)) + } + ) + + test("scanRightNel should be consistent with scanRight")( + forAll { (fa: List[Int], b: Int, f: (Int, Int) => Int) => + assert(fa.scanRightNel(b)(f).toList === fa.scanRight(b)(f)) + } + ) + test("show") { assert(List(1, 2, 3).show === "List(1, 2, 3)") assert((Nil: List[Int]).show === "List()") From 71ce7fb0c16c70a89afd8fcdf5cda714ea03858d Mon Sep 17 00:00:00 2001 From: satorg Date: Wed, 22 Sep 2021 17:04:07 -0700 Subject: [PATCH 06/12] add `prependList` to `NonEmptyList` --- core/src/main/scala/cats/data/NonEmptyList.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/src/main/scala/cats/data/NonEmptyList.scala b/core/src/main/scala/cats/data/NonEmptyList.scala index 62612e5f29..86e3295af3 100644 --- a/core/src/main/scala/cats/data/NonEmptyList.scala +++ b/core/src/main/scala/cats/data/NonEmptyList.scala @@ -108,9 +108,21 @@ final case class NonEmptyList[+A](head: A, tail: List[A]) extends NonEmptyCollec def ::[AA >: A](a: AA): NonEmptyList[AA] = prepend(a) + /** + * Prepend an item to this, producing a new `NonEmptyList`. + */ def prepend[AA >: A](a: AA): NonEmptyList[AA] = NonEmptyList(a, head :: tail) + /** + * Prepend a `List` to this, producing a new `NonEmptyList`. + */ + def prependList[AA >: A](list: List[AA]): NonEmptyList[AA] = + list match { + case Nil => this + case lh :: lt => NonEmptyList(lh, lt ::: toList) + } + /** * Alias for append * From ca53c58102130c656243f19a834dbf953628e50a Mon Sep 17 00:00:00 2001 From: satorg Date: Wed, 22 Sep 2021 19:50:07 -0700 Subject: [PATCH 07/12] add tests to `NonEmptyListSuite` --- tests/src/test/scala/cats/tests/NonEmptyListSuite.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala b/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala index 80efe31f40..e7a5623ecf 100644 --- a/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala +++ b/tests/src/test/scala/cats/tests/NonEmptyListSuite.scala @@ -257,13 +257,19 @@ class NonEmptyListSuite extends NonEmptyCollectionSuite[List, NonEmptyList, NonE } } - test(":: consistent with List") { + test("NonEmptyList#`::` and prepend consistent with List#`::`") { forAll { (nel: NonEmptyList[Int], i: Int) => assert((i :: nel).toList === (i :: nel.toList)) assert(nel.prepend(i).toList === (i :: nel.toList)) } } + test("NonEmptyList#prependList consisntent with List#`:::`") { + forAll { (nel: NonEmptyList[Int], l: List[Int]) => + assert(nel.prependList(l).toList === l ::: nel.toList) + } + } + test("NonEmptyList#distinct is consistent with List#distinct") { forAll { (nel: NonEmptyList[Int]) => assert(nel.distinct.toList === (nel.toList.distinct)) From 209c34f97a8758f987839624f9414b82b59fdd48 Mon Sep 17 00:00:00 2001 From: satorg Date: Wed, 22 Sep 2021 23:04:30 -0700 Subject: [PATCH 08/12] add `ListOps#concatNel` --- core/src/main/scala/cats/syntax/list.scala | 14 ++++++++++++++ tests/src/test/scala/cats/tests/ListSuite.scala | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/core/src/main/scala/cats/syntax/list.scala b/core/src/main/scala/cats/syntax/list.scala index f1878ec29e..d49fe5c03b 100644 --- a/core/src/main/scala/cats/syntax/list.scala +++ b/core/src/main/scala/cats/syntax/list.scala @@ -29,6 +29,20 @@ final class ListOps[A](private val la: List[A]) extends AnyVal { */ def toNel: Option[NonEmptyList[A]] = NonEmptyList.fromList(la) + /** + * Concatenates this `List` with a `NonEmptyList` producing a new `NonEmptyList`. + * + * Example: + * {{{ + * scala> import cats.data.NonEmptyList + * scala> import cats.implicits._ + * + * scala> List(1, 2, 3).concatNel(NonEmptyList.of(4, 5, 6)) + * res0: NonEmptyList[Int] = NonEmptyList(1, 2, 3, 4, 5, 6) + * }}} + */ + def concatNel[AA >: A](nel: NonEmptyList[AA]): NonEmptyList[AA] = nel.prependList(la) + /** * Groups elements inside this `List` according to the `Order` of the keys * produced by the given mapping function. diff --git a/tests/src/test/scala/cats/tests/ListSuite.scala b/tests/src/test/scala/cats/tests/ListSuite.scala index ff17f0ca22..2984574764 100644 --- a/tests/src/test/scala/cats/tests/ListSuite.scala +++ b/tests/src/test/scala/cats/tests/ListSuite.scala @@ -58,6 +58,12 @@ class ListSuite extends CatsSuite { assert(List.empty[Int].toNel === None) } + test("concatNel should be consistent with List#`:::`") { + forAll { (fa: List[Int], nel: NonEmptyList[Int]) => + assert(fa.concatNel(nel).toList === fa ::: nel.toList) + } + } + test("groupByNel should be consistent with groupBy")( forAll { (fa: List[Int], f: Int => Int) => assert((fa.groupByNel(f).map { case (k, v) => (k, v.toList) }: Map[Int, List[Int]]) === fa.groupBy(f)) From e8656093aba95fd549f0af947d39508e19677af6 Mon Sep 17 00:00:00 2001 From: satorg Date: Wed, 22 Sep 2021 23:47:02 -0700 Subject: [PATCH 09/12] add `VectorOps#concatNev` --- core/src/main/scala/cats/syntax/vector.scala | 14 ++++++++++++++ tests/src/test/scala/cats/tests/VectorSuite.scala | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/core/src/main/scala/cats/syntax/vector.scala b/core/src/main/scala/cats/syntax/vector.scala index 9185f29927..c34de06bd6 100644 --- a/core/src/main/scala/cats/syntax/vector.scala +++ b/core/src/main/scala/cats/syntax/vector.scala @@ -30,6 +30,20 @@ final class VectorOps[A](private val va: Vector[A]) extends AnyVal { */ def toNev: Option[NonEmptyVector[A]] = NonEmptyVector.fromVector(va) + /** + * Concatenates this `Vector` with a `NonEmptyVector` producing a new `NonEmptyVector`. + * + * Example: + * {{{ + * scala> import cats.data.NonEmptyVector + * scala> import cats.implicits._ + * + * scala> Vector(1, 2, 3).concatNev(NonEmptyVector.of(4, 5, 6)) + * res0: NonEmptyVector[Int] = NonEmptyVector(1, 2, 3, 4, 5, 6) + * }}} + */ + def concatNev[AA >: A](nev: NonEmptyVector[AA]): NonEmptyVector[AA] = nev.prependVector(va) + /** * Groups elements inside this `Vector` according to the `Order` of the keys * produced by the given mapping function. diff --git a/tests/src/test/scala/cats/tests/VectorSuite.scala b/tests/src/test/scala/cats/tests/VectorSuite.scala index 8581022f65..016daedff8 100644 --- a/tests/src/test/scala/cats/tests/VectorSuite.scala +++ b/tests/src/test/scala/cats/tests/VectorSuite.scala @@ -67,6 +67,13 @@ class VectorSuite extends CatsSuite { assert(Vector.empty[Int].toNev == None) } + test("concatNev should be consistent with Vector#`++`") { + forAll { (fa: Vector[Int], nev: NonEmptyVector[Int]) => + // Note: Scala 2.12.x does not have `Vector#concat`. + assert(fa.concatNev(nev).toVector === (fa ++ nev.toVector)) + } + } + test("groupByNev should be consistent with groupBy")( forAll { (fa: Vector[Int], f: Int => Int) => assert((fa.groupByNev(f).map { case (k, v) => (k, v.toVector) }: Map[Int, Vector[Int]]) === fa.groupBy(f)) From e70f22a659ae8ab9a5b179716bfad1f1a9e5a6ac Mon Sep 17 00:00:00 2001 From: satorg Date: Thu, 23 Sep 2021 00:15:36 -0700 Subject: [PATCH 10/12] add `SeqOps#concatNeSeq` --- core/src/main/scala/cats/syntax/seq.scala | 34 +++++++++++++++++++ .../src/test/scala/cats/tests/SeqSuite.scala | 7 ++++ 2 files changed, 41 insertions(+) diff --git a/core/src/main/scala/cats/syntax/seq.scala b/core/src/main/scala/cats/syntax/seq.scala index e3601232c4..fbcc04f34f 100644 --- a/core/src/main/scala/cats/syntax/seq.scala +++ b/core/src/main/scala/cats/syntax/seq.scala @@ -8,5 +8,39 @@ trait SeqSyntax { } final class SeqOps[A](private val va: Seq[A]) extends AnyVal { + + /** + * Returns an `Option` of `NonEmptySeq` from a `Seq` + * + * Example: + * {{{ + * scala> import cats.data.NonEmptySeq + * scala> import cats.syntax.all._ + * scala> import scala.collection.immutable.Seq + * + * scala> val result1: Seq[Int] = Seq(1, 2) + * scala> result1.toNeSeq + * res0: Option[NonEmptySeq[Int]] = Some(NonEmptySeq(1, 2)) + * + * scala> val result2: Seq[Int] = Seq.empty[Int] + * scala> result2.toNeSeq + * res1: Option[NonEmptySeq[Int]] = None + * }}} + */ def toNeSeq: Option[NonEmptySeq[A]] = NonEmptySeq.fromSeq(va) + + /** + * Concatenates this `Seq` with a `NonEmptySeq` producing a new `NonEmptySeq`. + * + * Example: + * {{{ + * scala> import cats.data.NonEmptySeq + * scala> import cats.syntax.all._ + * scala> import scala.collection.immutable.Seq + * + * scala> Seq(1, 2, 3).concatNeSeq(NonEmptySeq.of(4, 5, 6)) + * res0: NonEmptySeq[Int] = NonEmptySeq(1, 2, 3, 4, 5, 6) + * }}} + */ + def concatNeSeq[AA >: A](neseq: NonEmptySeq[AA]): NonEmptySeq[AA] = neseq.prependSeq(va) } diff --git a/tests/src/test/scala/cats/tests/SeqSuite.scala b/tests/src/test/scala/cats/tests/SeqSuite.scala index 19374872d1..6f6cffb82c 100644 --- a/tests/src/test/scala/cats/tests/SeqSuite.scala +++ b/tests/src/test/scala/cats/tests/SeqSuite.scala @@ -64,6 +64,13 @@ class SeqSuite extends CatsSuite { assert(Seq.empty[Int].toNeSeq == None) } + test("concatNeSeq should be consistent with Seq#`++`") { + forAll { (fa: Seq[Int], neseq: NonEmptySeq[Int]) => + // Note: Scala 2.12.x does not have `Seq#concat`. + assert(fa.concatNeSeq(neseq).toSeq === (fa ++ neseq.toSeq)) + } + } + test("traverse is stack-safe") { val seq = (0 until 100000).toSeq val sumAll = Traverse[Seq] From c70efc8b0dc1c63f408fb7dc5598b8ef8fb94156 Mon Sep 17 00:00:00 2001 From: satorg Date: Fri, 24 Sep 2021 00:18:34 -0700 Subject: [PATCH 11/12] add `groupByNeSeq` and `groupByNeSeqA` to `SeqOps` --- core/src/main/scala/cats/syntax/seq.scala | 63 +++++++++++++++++++ .../src/test/scala/cats/tests/SeqSuite.scala | 56 ++++++++++++----- 2 files changed, 103 insertions(+), 16 deletions(-) diff --git a/core/src/main/scala/cats/syntax/seq.scala b/core/src/main/scala/cats/syntax/seq.scala index fbcc04f34f..aa96f54d15 100644 --- a/core/src/main/scala/cats/syntax/seq.scala +++ b/core/src/main/scala/cats/syntax/seq.scala @@ -1,7 +1,13 @@ package cats.syntax +import cats.Applicative +import cats.Functor +import cats.Order +import cats.Traverse import cats.data.NonEmptySeq + import scala.collection.immutable.Seq +import scala.collection.immutable.SortedMap trait SeqSyntax { implicit final def catsSyntaxSeqs[A](va: Seq[A]): SeqOps[A] = new SeqOps(va) @@ -43,4 +49,61 @@ final class SeqOps[A](private val va: Seq[A]) extends AnyVal { * }}} */ def concatNeSeq[AA >: A](neseq: NonEmptySeq[AA]): NonEmptySeq[AA] = neseq.prependSeq(va) + + /** + * Groups elements inside this `Seq` according to the `Order` of the keys + * produced by the given mapping function. + * + * {{{ + * scala> import cats.data.NonEmptySeq + * scala> import cats.syntax.all._ + * scala> import scala.collection.immutable.Seq + * scala> import scala.collection.immutable.SortedMap + * + * scala> val seq = Seq(12, -2, 3, -5) + * scala> val res = SortedMap(false -> NonEmptySeq.of(-2, -5), true -> NonEmptySeq.of(12, 3)) + * scala> seq.groupByNeSeq(_ >= 0) === res + * res0: Boolean = true + * }}} + */ + def groupByNeSeq[B](f: A => B)(implicit B: Order[B]): SortedMap[B, NonEmptySeq[A]] = { + implicit val ordering: Ordering[B] = B.toOrdering + toNeSeq.fold(SortedMap.empty[B, NonEmptySeq[A]])(_.groupBy(f)) + } + + /** + * Groups elements inside this `Seq` according to the `Order` of the keys + * produced by the given mapping monadic function. + * + * {{{ + * scala> import cats.data.NonEmptySeq + * scala> import cats.syntax.all._ + * scala> import scala.collection.immutable.Seq + * scala> import scala.collection.immutable.SortedMap + * + * scala> def f(n: Int) = n match { case 0 => None; case n => Some(n > 0) } + * + * scala> val seq = Seq(12, -2, 3, -5) + * scala> val res = Some(SortedMap(false -> NonEmptySeq.of(-2, -5), true -> NonEmptySeq.of(12, 3))) + * scala> seq.groupByNeSeqA(f) === res + * res0: Boolean = true + * + * scala> // `f(0)` returns `None` + * scala> (seq :+ 0).groupByNeSeqA(f) === None + * res1: Boolean = true + * }}} + */ + def groupByNeSeqA[F[_], B]( + f: A => F[B] + )(implicit F: Applicative[F], B: Order[B]): F[SortedMap[B, NonEmptySeq[A]]] = { + implicit val ordering: Ordering[B] = B.toOrdering + val mapFunctor = Functor[SortedMap[B, *]] + val nesTraverse = Traverse[NonEmptySeq] + + toNeSeq.fold(F.pure(SortedMap.empty[B, NonEmptySeq[A]])) { nes => + F.map(nesTraverse.traverse(nes)(a => F.tupleRight(f(a), a))) { seq => + mapFunctor.map(seq.groupBy(_._1))(_.map(_._2)) + } + } + } } diff --git a/tests/src/test/scala/cats/tests/SeqSuite.scala b/tests/src/test/scala/cats/tests/SeqSuite.scala index 6f6cffb82c..edc97301b2 100644 --- a/tests/src/test/scala/cats/tests/SeqSuite.scala +++ b/tests/src/test/scala/cats/tests/SeqSuite.scala @@ -1,24 +1,18 @@ package cats.tests -import cats.{Align, Alternative, CoflatMap, Monad, Semigroupal, Traverse, TraverseFilter} -import cats.data.{NonEmptySeq, ZipSeq} -import cats.laws.discipline.{ - AlignTests, - AlternativeTests, - CoflatMapTests, - CommutativeApplyTests, - MonadTests, - SemigroupalTests, - SerializableTests, - ShortCircuitingTests, - TraverseFilterTests, - TraverseTests -} +import cats._ +import cats.data.NonEmptySeq +import cats.data.OneAnd +import cats.data.ZipSeq +import cats.laws.discipline._ import cats.laws.discipline.arbitrary._ -import cats.syntax.show._ -import cats.syntax.seq._ import cats.syntax.eq._ +import cats.syntax.functor._ +import cats.syntax.seq._ +import cats.syntax.show._ +import cats.syntax.traverse._ import org.scalacheck.Prop._ + import scala.collection.immutable.Seq class SeqSuite extends CatsSuite { @@ -71,6 +65,36 @@ class SeqSuite extends CatsSuite { } } + test("groupByNeSeq should be consistent with groupBy") { + forAll { (fa: Seq[String], f: String => Int) => + assert((fa.groupByNeSeq(f).map { _.map(_.toSeq) }: Map[Int, Seq[String]]) === fa.groupBy(f)) + } + } + + test("groupByNeSeqA[Id] should be consistent with groupByNeSeq") { + forAll { (fa: Seq[String], f: String => Id[Int]) => + assert(fa.groupByNeSeqA(f) === fa.groupByNeSeq(f)) + } + } + + def unwrapGroupByNeSeq[G[_]: Applicative, A, B](fa: Seq[A], f: A => G[B])(implicit ord: Order[B]) = { + implicit val ordering = ord.toOrdering + fa + .traverse(a => f(a).tupleRight(a)) + .map(_.groupByNeSeq(_._1).map(_.map(_.map(_._2)))) + } + + test("groupByNeSeqA[Option] should be consistent with groupByNeSeq") { + forAll { (fa: Seq[String], f: String => Option[Int]) => + assert(fa.groupByNeSeqA(f) === unwrapGroupByNeSeq(fa, f)) + } + } + test("groupByNeSeqA[OneAnd] should be consistent with groupByNeSeq") { + forAll { (fa: Seq[String], f: String => OneAnd[Option, Int]) => + assert(fa.groupByNeSeqA(f) === unwrapGroupByNeSeq(fa, f)) + } + } + test("traverse is stack-safe") { val seq = (0 until 100000).toSeq val sumAll = Traverse[Seq] From 87b26cb177d2a32d4c6276b4fb59cc354f20e0a7 Mon Sep 17 00:00:00 2001 From: satorg Date: Sat, 25 Sep 2021 17:36:55 -0700 Subject: [PATCH 12/12] add `scanLeftNeSeq` and `scanRightNeSeq` to `SeqOps` --- core/src/main/scala/cats/syntax/seq.scala | 62 ++++++++++++++++--- .../src/test/scala/cats/tests/SeqSuite.scala | 12 ++++ 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/core/src/main/scala/cats/syntax/seq.scala b/core/src/main/scala/cats/syntax/seq.scala index aa96f54d15..60f41dfd81 100644 --- a/core/src/main/scala/cats/syntax/seq.scala +++ b/core/src/main/scala/cats/syntax/seq.scala @@ -24,13 +24,13 @@ final class SeqOps[A](private val va: Seq[A]) extends AnyVal { * scala> import cats.syntax.all._ * scala> import scala.collection.immutable.Seq * - * scala> val result1: Seq[Int] = Seq(1, 2) - * scala> result1.toNeSeq - * res0: Option[NonEmptySeq[Int]] = Some(NonEmptySeq(1, 2)) + * scala> val seq1 = Seq(1, 2) + * scala> seq1.toNeSeq + * res1: Option[NonEmptySeq[Int]] = Some(NonEmptySeq(1, 2)) * - * scala> val result2: Seq[Int] = Seq.empty[Int] - * scala> result2.toNeSeq - * res1: Option[NonEmptySeq[Int]] = None + * scala> val seq2 = Seq.empty[Int] + * scala> seq2.toNeSeq + * res2: Option[NonEmptySeq[Int]] = None * }}} */ def toNeSeq: Option[NonEmptySeq[A]] = NonEmptySeq.fromSeq(va) @@ -63,7 +63,7 @@ final class SeqOps[A](private val va: Seq[A]) extends AnyVal { * scala> val seq = Seq(12, -2, 3, -5) * scala> val res = SortedMap(false -> NonEmptySeq.of(-2, -5), true -> NonEmptySeq.of(12, 3)) * scala> seq.groupByNeSeq(_ >= 0) === res - * res0: Boolean = true + * res1: Boolean = true * }}} */ def groupByNeSeq[B](f: A => B)(implicit B: Order[B]): SortedMap[B, NonEmptySeq[A]] = { @@ -86,11 +86,11 @@ final class SeqOps[A](private val va: Seq[A]) extends AnyVal { * scala> val seq = Seq(12, -2, 3, -5) * scala> val res = Some(SortedMap(false -> NonEmptySeq.of(-2, -5), true -> NonEmptySeq.of(12, 3))) * scala> seq.groupByNeSeqA(f) === res - * res0: Boolean = true + * res1: Boolean = true * * scala> // `f(0)` returns `None` * scala> (seq :+ 0).groupByNeSeqA(f) === None - * res1: Boolean = true + * res2: Boolean = true * }}} */ def groupByNeSeqA[F[_], B]( @@ -106,4 +106,48 @@ final class SeqOps[A](private val va: Seq[A]) extends AnyVal { } } } + + /** + * Produces a `NonEmptySeq` containing cumulative results of applying the + * operator going left to right. + * + * Example: + * {{{ + * scala> import cats.data.NonEmptySeq + * scala> import cats.syntax.all._ + * scala> import scala.collection.immutable.Seq + * + * scala> val seq1 = Seq(1, 2) + * scala> seq1.scanLeftNeSeq(100)(_ + _) + * res1: NonEmptySeq[Int] = NonEmptySeq(100, 101, 103) + * + * scala> val seq2 = Seq.empty[Int] + * scala> seq2.scanLeftNeSeq(123)(_ + _) + * res2: NonEmptySeq[Int] = NonEmptySeq(123) + * }}} + */ + def scanLeftNeSeq[B](b: B)(f: (B, A) => B): NonEmptySeq[B] = + NonEmptySeq.fromSeqUnsafe(va.scanLeft(b)(f)) + + /** + * Produces a `NonEmptySeq` containing cumulative results of applying the + * operator going right to left. + * + * Example: + * {{{ + * scala> import cats.data.NonEmptySeq + * scala> import cats.syntax.all._ + * scala> import scala.collection.immutable.Seq + * + * scala> val seq = Seq(1, 2) + * scala> seq.scanRightNeSeq(100)(_ + _) + * res0: NonEmptySeq[Int] = NonEmptySeq(103, 102, 100) + * + * scala> val seq2 = Seq.empty[Int] + * scala> seq2.scanRightNeSeq(123)(_ + _) + * res1: NonEmptySeq[Int] = NonEmptySeq(123) + * }}} + */ + def scanRightNeSeq[B](b: B)(f: (A, B) => B): NonEmptySeq[B] = + NonEmptySeq.fromSeqUnsafe(va.scanRight(b)(f)) } diff --git a/tests/src/test/scala/cats/tests/SeqSuite.scala b/tests/src/test/scala/cats/tests/SeqSuite.scala index edc97301b2..1372d7aa3a 100644 --- a/tests/src/test/scala/cats/tests/SeqSuite.scala +++ b/tests/src/test/scala/cats/tests/SeqSuite.scala @@ -95,6 +95,18 @@ class SeqSuite extends CatsSuite { } } + test("scanLeftNeSeq should be consistent with scanLeft") { + forAll { (fa: Seq[Int], b: String, f: (String, Int) => String) => + assert(fa.scanLeftNeSeq(b)(f).toSeq === fa.scanLeft(b)(f)) + } + } + + test("scanRightNeSeq should be consistent with scanRight") { + forAll { (fa: Seq[Int], b: String, f: (Int, String) => String) => + assert(fa.scanRightNeSeq(b)(f).toSeq === fa.scanRight(b)(f)) + } + } + test("traverse is stack-safe") { val seq = (0 until 100000).toSeq val sumAll = Traverse[Seq]