diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index d83c8bb550..1206c628b3 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -23,6 +23,7 @@ package cats import scala.collection.mutable import cats.kernel.CommutativeMonoid +import cats.data.NonEmptyList import Foldable.{sentinel, Source} @@ -960,6 +961,60 @@ trait Foldable[F[_]] extends UnorderedFoldable[F] with FoldableNFunctions[F] { s import cats.instances.either.* partitionBifoldM[G, Either, A, B, C](fa)(f)(A, M, Bifoldable[Either]) } + + /** + * Split this Foldable into a NonEmptyList of Lists based on a predicate. + * The behaviour is aimed to be identical to that of haskell's `splitWhen` + * + * {{{ + * scala> import cats.syntax.all._, cats.Foldable, cats.data.NonEmptyList + * scala> Foldable[List].splitWhen(List(1,1))(_ == 1) + * res0: NonEmptyList[List[Int]] = NonEmptyList(List(), List(), List()) + * scala> Foldable[List].splitWhen(Nil)(_ == 1) + * res1: NonEmptyList[List[Nothing]] = NonEmptyList(List()) + * scala> Foldable[List].splitWhen(List(1, 2, 3, 1, 4, 5))(_ == 1) + * res2: NonEmptyList[List[Int]] = NonEmptyList(List(), List(2, 3), List(4, 5)) + * }}} + */ + + def splitWhen[A](fa: F[A])(f: A => Boolean)(implicit + FA: Alternative[F] + ): NonEmptyList[F[A]] = { + foldRight(fa, Eval.now(NonEmptyList.one(FA.empty[A]))) { + case (a, acc) if f(a) => acc.map(FA.empty[A] :: _) + case (a, acc) => acc.map(nel => NonEmptyList(FA.prependK(a, nel.head), nel.tail)) + }.value + } + + /** + * Split this Foldable into a NonEmptyList of Lists based on the effectufl predicate. Monadic version of `splitWhen` + * + * {{{ + * scala> import cats.syntax.all._, cats.Foldable, cats.Eval, cats.data.NonEmptyList + * scala> Foldable[List].splitWhenM(List(1,1))(x => Eval.now(x == 1)).value + * res0: NonEmptyList[List[Int]] = NonEmptyList(List(), List(), List()) + * scala> Foldable[List].splitWhenM(List.empty[Int])(x => Eval.now(x == 1)).value + * res1: NonEmptyList[List[Int]] = NonEmptyList(List()) + * scala> Foldable[List].splitWhenM(List(1, 2, 3, 1, 4, 5))(x => Eval.now(x == 1)).value + * val res2: NonEmptyList[List[Int]] = NonEmptyList(List(), List(2, 3), List(4, 5)) + * }}} + */ + + def splitWhenM[G[_], A](fa: F[A])(f: A => G[Boolean])(implicit + M: Monad[G], + FA: Alternative[F] + ): G[NonEmptyList[F[A]]] = { + foldRight(fa, Eval.now(M.pure(NonEmptyList.one(FA.empty[A])))) { (a, evalGnel) => + evalGnel.map { gnel => + M.flatMap(f(a)) { isDelimiter => + M.map(gnel) { nel => + if (isDelimiter) FA.empty[A] :: nel + else NonEmptyList(FA.prependK(a, nel.head), nel.tail) + } + } + } + }.value + } } object Foldable { diff --git a/core/src/main/scala/cats/syntax/foldable.scala b/core/src/main/scala/cats/syntax/foldable.scala index b70eb71674..a2e7ac3a21 100644 --- a/core/src/main/scala/cats/syntax/foldable.scala +++ b/core/src/main/scala/cats/syntax/foldable.scala @@ -22,6 +22,8 @@ package cats package syntax +import cats.data.NonEmptyList + trait FoldableSyntax extends Foldable.ToFoldableOps with UnorderedFoldable.ToUnorderedFoldableOps { implicit final def catsSyntaxNestedFoldable[F[_]: Foldable, G[_], A](fga: F[G[A]]): NestedFoldableOps[F, G, A] = @@ -304,6 +306,16 @@ final class FoldableOps0[F[_], A](private val fa: F[A]) extends AnyVal { )(implicit A: Alternative[F], F: Foldable[F], M: Monad[G]): G[(F[B], F[C])] = F.partitionEitherM[G, A, B, C](fa)(f)(A, M) + def splitWhen(f: A => Boolean)(implicit FF: Foldable[F], FA: Alternative[F]): NonEmptyList[F[A]] = { + FF.splitWhen[A](fa)(f)(FA) + } + + def splitWhenM[G[_]]( + f: A => G[Boolean] + )(implicit FF: Foldable[F], FA: Alternative[F], G: Monad[G]): G[NonEmptyList[F[A]]] = { + FF.splitWhenM[G, A](fa)(f)(G, FA) + } + def sliding2(implicit F: Foldable[F]): List[(A, A)] = F.sliding2(fa) def sliding3(implicit F: Foldable[F]): List[(A, A, A)] = diff --git a/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala b/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala index 73bfd06d84..2dbba313a2 100644 --- a/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/FoldableSuite.scala @@ -333,6 +333,33 @@ abstract class FoldableSuite[F[_]: Foldable](name: String)(implicit } } + test(s"Foldable[$name].splitWhen") { + forAll { (fi: F[Int], x: Int) => + val pred = (y: Int) => x == y + val li = fi.toList + val res = li.splitWhen(pred) + val expectedFiltered = li.filterNot(pred) + val expectedSize = li.size - expectedFiltered.size + 1 + assert(res.size === expectedSize) + assert(res.reduce === expectedFiltered) + assert(Reducible[NonEmptyList].nonEmptyIntercalate(res, List(x)) == li) + } + } + + test(s"Foldable[$name].splitWhenM") { + forAll { (fi: F[Int], x: Int) => + val pred = (y: Int) => x == y + val predM = (y: Int) => Eval.now(pred(y)) + val li = fi.toList + val res = li.splitWhenM(predM).value + val expectedFiltered = li.filterNot(pred) + val expectedSize = li.size - expectedFiltered.size + 1 + assert(res.size === expectedSize) + assert(res.reduce === expectedFiltered) + assert(Reducible[NonEmptyList].nonEmptyIntercalate(res, List(x)) == li) + } + } + test(s"Foldable[$name].sliding2 consistent with List#sliding(2)") { forAll { (fi: F[Int]) => val n = 2