Graham Lea
Graham Lea

Reputation: 6343

How do I find the min() or max() of two Option[Int]

How would you find minValue below? I have my own solution but want to see how others would do it.

val i1: Option[Int] = ...
val i2: Option[Int] = ...
val defaultValue: Int = ...
val minValue = ?

Upvotes: 11

Views: 15451

Answers (10)

Volty De Qua
Volty De Qua

Reputation: 287

Since Seq[Option[Element]].min evaluates to None if Seq[Option[Element]].exists(_.isEmpty) (None) is true, while Seq[Option[Element]].max evaluates to Some(element) if Seq[Option[Element]].exists(_.nonEmpty) is true, I resort to double reversion, with the result of finding min by using maxBy:

println((for {
  a <- Seq(None, Some(3))
  b <- Seq(None, Some(4))
} yield Seq(a, b, Seq(a, b).maxBy(_.map(-_)).getOrElse(-666))
  .mkString(" -> ")
  ).mkString("\n")
)

with the result output of:

None -> None -> -666
None -> Some(4) -> 4
Some(3) -> None -> 3
Some(3) -> Some(4) -> 3

Another solution would be that of tweaking/implementing Ordering[Option[T]]

Upvotes: 0

Boris Azanov
Boris Azanov

Reputation: 4501

tl;dr

You can do that you need elegant using custom cats Semigroup instances:

import cats.kernel.Semigroup
import cats.instances.option._ // this import is for cats std option combiner
import cats.syntax.semigroup._

object Implicits {
 implicit val intMinSemigroup: Semigroup[Int] =
   (x: Int, y: Int) => math.min(x, y)

 implicit val intMaxSemigroup: Semigroup[Int] =
   (x: Int, y: Int) => math.max(x, y)
}
 
import Implicits.intMinSemigroup
// these are results for minSemigroup
// List((Some(1),Some(1),Some(2)), (Some(1),Some(1),None), (None,Some(2),Some(2)), (None,None,None))
//import Implicits.intMaxSemigroup
// these are results for maxSemigroup
// List((Some(1),Some(2),Some(2)), (Some(1),Some(1),None), (None,Some(2),Some(2)), (None,None,None))
   
for {
 maybeA <- Seq(Some(1), None)
 maybeB <- Seq(Some(2), None)
} yield (maybeA, maybeA |+| maybeB, maybeB)

if you want replace None by default value you can use combine twice:

val defaultValue: Int = 3
val optionMin = for {
  maybeA <- Seq(Some(1), None)
  maybeB <- Seq(Some(2), None)
} yield (maybeA |+| maybeB) |+| Some(defaultValue)
// List(Some(1), Some(1), Some(2), Some(3))

How it works

Shortly, Semigroup[A] is typeclass for combining two values of the same type A into the one value of type A. Here we use std cats OptionMonoid (it extends Semigroup[Option[A]]) here source code:

class OptionMonoid[A](implicit A: Semigroup[A]) extends Monoid[Option[A]] {
  def empty: Option[A] = None
  def combine(x: Option[A], y: Option[A]): Option[A] =
    x match {
      case None => y
      case Some(a) =>
        y match {
          case None    => x
          case Some(b) => Some(A.combine(a, b))
        }
    }
}

We see that it takes option matching on his own and everything what we should give him to work is implicit A: Semigroup[A]. In our case we write two different combiners for min, max cases:

object Implicits {
 implicit val intMinSemigroup: Semigroup[Int] =
   (x: Int, y: Int) => math.min(x, y)

 implicit val intMaxSemigroup: Semigroup[Int] =
   (x: Int, y: Int) => math.max(x, y)
}

So, we import combiners (i.e. import Implicits.intMinSemigroup) and just use cats.syntax.semigroup for using combine function as operator |+|:

maybeA |+| maybeB.

In conclusion, you can just define your custom semigroup for any type (not only Int) and combine options of this type after importing some cats syntax and instances.

Upvotes: 0

Xavier Guihot
Xavier Guihot

Reputation: 61736

We can combine the 2 Options as an Iterable with Option's ++ operator, which allows us to use minOption (to nicely handle the case of the empty iterable formed by the None/None case) and fallback on a default value if necessary with getOrElse:

(optionA ++ optionB).minOption.getOrElse(-1)
// None and None       => -1
// Some(5) and None    => 5
// None and Some(5)    => 5
// Some(5) and Some(3) => 3

Upvotes: 3

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149598

Another option which wasn't mentioned is using reduceLeftOption (interchange math.max and math.min as desired):

val min = (first ++ second).reduceLeftOption(math.min).getOrElse(defaultValue)

scala> val first = Some(10)
first: Some[Int] = Some(10)

scala> val second: Option[Int] = None
second: Option[Int] = None

scala> val defaultMin = -1
defaultMin: Int = -1

scala> (first ++ second).reduceLeftOption(math.min).getOrElse(defaultMin)
res7: Int = 10

scala> val first: Option[Int] = None
first: Option[Int] = None

scala> (first ++ second).reduceLeftOption(math.min).getOrElse(defaultMin)
res8: Int = -1

scala> val first = Some(10)
first: Some[Int] = Some(10)

scala> val second = Some(42)
second: Some[Int] = Some(42)

scala> (first ++ second).reduceLeftOption(math.min).getOrElse(defaultMin)
res9: Int = 10

Upvotes: 1

Vladimir Kostyukov
Vladimir Kostyukov

Reputation: 2552

I solved a similar problem using the following approach. We handle a special case when both of the options have values, otherwise we use an API method Option.orElse.

val a: Option[Int]  = Some(10)
val b: Option[Int] = Some(20)
val c: Option[Int] = (a, b) match {
  case (Some(x), Some(y)) => Some(x min y)
  case (x, y) => x orElse y
}

Upvotes: 6

Paul Cameron
Paul Cameron

Reputation: 49

You can use patterns in for expressions, values that do not match the pattern are discarded.

(for (Some(x) <- List(None, Some(3))) yield x) max

Not as good as the List.flatten approach though.

Upvotes: 1

Luigi Plinge
Luigi Plinge

Reputation: 51109

I think this is what you're after:

val minValue = List(i1, i2).flatten match {
  case Nil => defaultValue
  case xs => xs.min
}

I'd avoid sorted since sorting requires a lot more processing than simply finding the max or min (although it probably doesn't make much difference in this case).

Upvotes: 3

Travis Brown
Travis Brown

Reputation: 139058

Update: I just noticed that my solution below and the one in your answer behave differently—I read your question as asking for the minimum of the two values when there are two values, but in your answer you're effectively treating None as if it contained a value that's either bigger (for min) or smaller (for max) than anything else.

To be more concrete: if i1 is Some(1) and i2 is None, my solution will return the default value, while yours will return 1.

If you want the latter behavior, you can use the default semigroup instance for Option[A] and the tropical semigroup for Int. In Scalaz 7, for example, you'd write:

import scalaz._, Scalaz._

optionMonoid(Semigroup.minSemigroup[Int]).append(i1, i2) getOrElse defaultValue

Or the following shorthand:

Tags.Min(i1) |+| Tags.Min(i2) getOrElse defaultValue

It's not as clean as the applicative functor solution below, but if that's your problem, that's your problem.


Here's a more idiomatic way that doesn't involve creating an extra list:

(for { x <- i1; y <- i2 } yield math.min(x, y)) getOrElse defaultValue

Or, equivalently:

i1.flatMap(x => i2.map(math.min(x, _))) getOrElse defaultValue

What you're doing is "lifting" a two-place function (min) into an applicative functor (Option). Scalaz makes this easy with its applicative builder syntax:

import scalaz._, Scalaz._

(i1 |@| i2)(math.min) getOrElse defaultValue

The standard library solution isn't much less elegant in this case, but this is a useful abstraction to know about.

Upvotes: 9

846846846
846846846

Reputation: 49

If you want to avoid using scalaz and map/for/getOrElse, you can do the following:

val minValue = (i1, i2) match {
  case (Some(x), Some(y)) => math.min(x, y)
  case _ => defaultValue
}

Upvotes: 0

Graham Lea
Graham Lea

Reputation: 6343

val minValue: Int = List(i1, i2).flatten.sorted.headOption getOrElse defaultValue

Upvotes: 2

Related Questions