From 4db14adce152d14474fad25bf1d8cedd7a58fedb Mon Sep 17 00:00:00 2001 From: Kalra Date: Sun, 16 Feb 2020 09:02:59 +0800 Subject: [PATCH] backported #3084 Add minimumBy/maximumBy/Option to Foldable --- .../src/main/scala/cats/syntax/foldable.scala | 28 +++++++++++++++++++ .../main/scala/cats/syntax/reducible.scala | 16 +++++++++++ .../test/scala/cats/tests/FoldableSuite.scala | 14 ++++++++++ 3 files changed, 58 insertions(+) diff --git a/core/src/main/scala/cats/syntax/foldable.scala b/core/src/main/scala/cats/syntax/foldable.scala index 53157f0c99..182a247d0e 100644 --- a/core/src/main/scala/cats/syntax/foldable.scala +++ b/core/src/main/scala/cats/syntax/foldable.scala @@ -303,6 +303,34 @@ final class FoldableOps0[F[_], A](private val fa: F[A]) extends AnyVal { import cats.syntax.foldable._ F.partitionEitherM[G, A, B, C](fa)(f)(A, M) } + + /** + * Find the minimum `A` item in this structure according to an `Order.by(f)`. + * + * @return `None` if the structure is empty, otherwise the minimum element + * wrapped in a `Some`. + * + * @see [[Reducible#minimum]] for a version that doesn't need to return an + * `Option` for structures that are guaranteed to be non-empty. + * + * @see [[maximumOptionBy]] for maximum instead of minimum. + */ + def minimumOptionBy[B: Order](f: A => B)(implicit F: Foldable[F]): Option[A] = + F.minimumOption(fa)(Order.by(f)) + + /** + * Find the maximum `A` item in this structure according to an `Order.by(f)`. + * + * @return `None` if the structure is empty, otherwise the maximum element + * wrapped in a `Some`. + * + * @see [[Reducible#maximum]] for a version that doesn't need to return an + * `Option` for structures that are guaranteed to be non-empty. + * + * @see [[minimumOptionBy]] for minimum instead of maximum. + */ + def maximumOptionBy[B: Order](f: A => B)(implicit F: Foldable[F]): Option[A] = + F.maximumOption(fa)(Order.by(f)) } final private[syntax] class FoldableOps1[F[_]](private val F: Foldable[F]) extends AnyVal { diff --git a/core/src/main/scala/cats/syntax/reducible.scala b/core/src/main/scala/cats/syntax/reducible.scala index 3e5904ef52..5438e95536 100644 --- a/core/src/main/scala/cats/syntax/reducible.scala +++ b/core/src/main/scala/cats/syntax/reducible.scala @@ -31,4 +31,20 @@ final class ReducibleOps0[F[_], A](private val fa: F[A]) extends AnyVal { * */ def reduceMapK[G[_], B](f: A => G[B])(implicit F: Reducible[F], G: SemigroupK[G]): G[B] = F.reduceLeftTo(fa)(f)((b, a) => G.combineK(b, f(a))) + + /** + * Find the minimum `A` item in this structure according to an `Order.by(f)`. + * + * @see [[maximumBy]] for maximum instead of minimum. + */ + def minimumBy[B: Order](f: A => B)(implicit F: Reducible[F]): A = + F.minimum(fa)(Order.by(f)) + + /** + * Find the maximum `A` item in this structure according to an `Order.by(f)`. + * + * @see [[minimumBy]] for minimum instead of maximum. + */ + def maximumBy[B: Order](f: A => B)(implicit F: Reducible[F]): A = + F.maximum(fa)(Order.by(f)) } diff --git a/tests/src/test/scala/cats/tests/FoldableSuite.scala b/tests/src/test/scala/cats/tests/FoldableSuite.scala index 249e9f90ee..32a486b9a1 100644 --- a/tests/src/test/scala/cats/tests/FoldableSuite.scala +++ b/tests/src/test/scala/cats/tests/FoldableSuite.scala @@ -192,6 +192,20 @@ abstract class FoldableSuite[F[_]: Foldable](name: String)(implicit ArbFInt: Arb } } + test(s"Foldable[$name].maximumBy/minimumBy") { + forAll { (fa: F[Int], f: Int => Int) => + val maxOpt = fa.maximumOptionBy(f).map(f) + val minOpt = fa.minimumOptionBy(f).map(f) + val nelOpt = fa.toList.toNel + maxOpt should ===(nelOpt.map(_.maximumBy(f)).map(f)) + maxOpt should ===(nelOpt.map(_.toList.maxBy(f)).map(f)) + minOpt should ===(nelOpt.map(_.minimumBy(f)).map(f)) + minOpt should ===(nelOpt.map(_.toList.minBy(f)).map(f)) + maxOpt.forall(i => fa.forall(f(_) <= i)) should ===(true) + minOpt.forall(i => fa.forall(f(_) >= i)) should ===(true) + } + } + test(s"Foldable[$name].reduceLeftOption/reduceRightOption") { forAll { (fa: F[Int]) => val list = fa.toList