From 449d54f9118e3294843a15b9b2ffc2582685362d Mon Sep 17 00:00:00 2001 From: Georgi Krastev Date: Tue, 24 Sep 2019 23:12:47 +0200 Subject: [PATCH] Add syntax for minimumBy/maximumBy/Option * `mimimumByOption` / `maximumByOption` on `Foldable` * `minumumBy` / `maximumBy` on `Reducible` --- core/src/main/scala/cats/Foldable.scala | 28 +++++++++++++++++++ core/src/main/scala/cats/Reducible.scala | 16 +++++++++++ .../test/scala/cats/tests/FoldableSuite.scala | 14 ++++++++++ 3 files changed, 58 insertions(+) diff --git a/core/src/main/scala/cats/Foldable.scala b/core/src/main/scala/cats/Foldable.scala index 190ac117a6..b0a5fb2701 100644 --- a/core/src/main/scala/cats/Foldable.scala +++ b/core/src/main/scala/cats/Foldable.scala @@ -193,6 +193,34 @@ import Foldable.sentinel def maximumOption[A](fa: F[A])(implicit A: Order[A]): Option[A] = reduceLeftOption(fa)(A.max) + /** + * 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[A, B: Order](fa: F[A])(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[A, B: Order](fa: F[A])(f: A => B)(implicit F: Foldable[F]): Option[A] = + F.maximumOption(fa)(Order.by(f)) + /** * Get the element at the index of the `Foldable`. */ diff --git a/core/src/main/scala/cats/Reducible.scala b/core/src/main/scala/cats/Reducible.scala index 99b2e7fa71..a0e60b2cb5 100644 --- a/core/src/main/scala/cats/Reducible.scala +++ b/core/src/main/scala/cats/Reducible.scala @@ -156,6 +156,22 @@ import simulacrum.typeclass def maximum[A](fa: F[A])(implicit A: Order[A]): A = reduceLeft(fa)(A.max) + /** + * Find the minimum `A` item in this structure according to an `Order.by(f)`. + * + * @see [[maximumBy]] for maximum instead of minimum. + */ + def minimumBy[A, B: Order](fa: F[A])(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[A, B: Order](fa: F[A])(f: A => B)(implicit F: Reducible[F]): A = + F.maximum(fa)(Order.by(f)) + /** * Intercalate/insert an element between the existing elements while reducing. * diff --git a/tests/src/test/scala/cats/tests/FoldableSuite.scala b/tests/src/test/scala/cats/tests/FoldableSuite.scala index 5080fff6ca..6da3d78b09 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