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

10

11

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 = ?
Lubra answered 28/9, 2012 at 12:22 Comment(0)
D
3

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).

Dowable answered 29/9, 2012 at 5:23 Comment(3)
I think this is probably the best way, though it's unfortunate that the idiomatic style makes it four lines long. I tried to shorten it (by removing match) and came up with ((xs:List[Int]) => {if (xs == Nil) defaultValue else xs.min})(List(i1, i2).flatten) but this is clearly obfuscation for the sake of brevity.Lubra
why not List(Option(defaultValue), i1, i2).flatten.min ?Lau
flatten.min would throw an UnsupportedOperationException with empty.min if List is empty, when you flatten you can end up with an empty list if all elements inside the list are NoneChammy
C
9

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.

Ciprian answered 28/9, 2012 at 12:41 Comment(2)
Thanks for your answer, Travis. It's pleasing in its brevity, though there's two things that turn me off using it: it uses a third party library, and in order to understand it I need to read an article about "a relatively new area in mathematics and algebraic geometry".Lubra
I added a little more actual way using cats for that.Mahout
R
6

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
}
Roundtree answered 29/1, 2015 at 18:12 Comment(0)
D
3

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).

Dowable answered 29/9, 2012 at 5:23 Comment(3)
I think this is probably the best way, though it's unfortunate that the idiomatic style makes it four lines long. I tried to shorten it (by removing match) and came up with ((xs:List[Int]) => {if (xs == Nil) defaultValue else xs.min})(List(i1, i2).flatten) but this is clearly obfuscation for the sake of brevity.Lubra
why not List(Option(defaultValue), i1, i2).flatten.min ?Lau
flatten.min would throw an UnsupportedOperationException with empty.min if List is empty, when you flatten you can end up with an empty list if all elements inside the list are NoneChammy
P
3

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
Phenothiazine answered 23/1, 2019 at 23:4 Comment(0)
L
2
val minValue: Int = List(i1, i2).flatten.sorted.headOption getOrElse defaultValue
Lubra answered 28/9, 2012 at 12:22 Comment(0)
C
1

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.

Crumpet answered 8/12, 2012 at 20:21 Comment(0)
E
1

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
Elegancy answered 9/10, 2016 at 6:48 Comment(0)
A
0

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
}
Allman answered 28/9, 2012 at 16:22 Comment(1)
Thanks for the contribution, but I wanted the min value if either one was defined, not only when both are defined.Lubra
M
0

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.

Mahout answered 8/4, 2021 at 14:40 Comment(0)
K
0

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]]

Kancler answered 27/6 at 21:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.