4

I have 2 options, and I need to take average of the values they hold.

It is possible that one or both may be missing. If one of the value is missing, I would just take other one as the average. But if both are missing, I would resort to some default value.

How can this be done in a clean way?

I can check absence of value using isEmpty, but then won't that would be same as null check?

Mandroid
  • 6,200
  • 12
  • 64
  • 134

5 Answers5

8

I guess this is self-explanatory:

val option1 = Some(12.0)
val option2 = None
val default = 0.0

val average = (option1, option2) match {
   case (Some(val1), Some(val2)) => (val1 + val2) / 2
   case (None, Some(val2)) => val2
   case (Some(val1), None) => val1
   case (None, None) => default
}

... but if not, the basic idea is that you construct a tuple of options, and then pattern match on the tuple.

This has a benefit of explicitly capturing all the four potential cases + having support from the complier - since Option is a sealed trait, compiler can check and ensure that all the potential branches of pattern match are covered.

J0HN
  • 26,063
  • 5
  • 54
  • 85
  • Thanks for the answer. What if I want to throw an exception instead of default? – Mandroid Mar 12 '20 at 08:14
  • 2
    You just throw an exception instead of default :) `case (None, None) => throw new Exception("your exception goes here")`. – J0HN Mar 12 '20 at 08:21
4

You could treat the Options as Seq:

val o: Option[Double]
val p: Option[Double]
val default: Double

val s = o.toSeq ++ p.toSeq

val avg = s.reduceOption(_ + _).getOrElse(default) / 1.max(s.size)
cbley
  • 4,538
  • 1
  • 17
  • 32
1
val v = List(opt1, opt2).flatten

if (v.nonEmpty) {
  v.sum / v.size
} else {
  <default value>
}

This can be extended to work with any number of optional values.

Tim
  • 26,753
  • 2
  • 16
  • 29
0

Another possibility:

def avg(left: Option[Double], right: Option[Double])(default: => Double): Double =
  left.flatMap(a => right.map(b => (a + b) / 2))
    .orElse(left)
    .orElse(right)
    .getOrElse(default)

You flatMap over the left option: if it's not empty, you take the right option and map its content and average with the content of the left option. If either option is empty, the result is None, so you can defined either left or right as fallback values with orElse. Finally, the result is retrieved with getOrElse and if both inputs where empty, the default is returned.

You can adapt this to adopt any behavior. To make a function that throws if both options are empty you can do the following:

val assertAvg = avg(_ : Option[Double], _ : Option[Double])(sys.error("both operands are empty"))

This works because the type of throw expressions is Nothing, which is a subtype of any other type (including Double), i.e. it can be returned as a result of any expression, regardless the expected type.

The code (and some tests) are available here on Scastie.

stefanobaghino
  • 11,253
  • 4
  • 35
  • 63
0

In my opinion you should keep average in option and grab default afterwards.

def avgOpt(of:Option[Double]*) = {
  val s = of.flatten
  s.reduceOption(_ + _).map(_ / s.size)
}

avgOpt(Some(5), None, None).getOrElse(0)    //5
avgOpt(Some(5), Some(3), None).getOrElse(0) //4
avgOpt(None, None).getOrElse(0)             //0
Scalway
  • 1,633
  • 10
  • 18