2

I have a List

    val l : List[Map[String,Any]] = List(Map("a" -> 1, "b" -> 2.8), Map("a" -> 3, "c" -> 4), Map("c" -> 5, "d" -> "abc"))

and I used the following code to find the sum for the keys "a" (Int), "b" (Double) and "c" (Int). "d" is included as noise.

    l.map(n => n.mapValues( v => if (v.isInstanceOf[Number]) {v match {
    case x:Int => x.asInstanceOf[Int]
    case x:Double => x.asInstanceOf[Double]
    }} else 0)).foldLeft((0,0.0,0))((t, m) => (
t._1 +  m.get("a").getOrElse(0), 
t._2 + m.get("b").getOrElse(0.0), 
t._3 + m.get("c").getOrElse(0)))

I expect the output would be (4, 2.8, 9) but instead I was trashed with

<console>:10: error: overloaded method value + with alternatives:
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int
 cannot be applied to (AnyVal)

I think the exception was trying to tell me that '+' doesn't work with AnyVal. How do I get this to work to get my the result that I want? Thanks

thlim
  • 2,908
  • 3
  • 34
  • 57

6 Answers6

7
m.foldLeft(0)(_+_._2)  

it's a very clear and simple solution. reference: http://ktuman.blogspot.com/2009/10/how-to-simply-sum-values-in-map-in.html

agold
  • 6,140
  • 9
  • 38
  • 54
Jiayu Wang
  • 905
  • 7
  • 5
5

You can use foldLeft function:

scala> val l : List[Map[String,Any]] = List(Map("a" -> 1, "b" -> 2.8), Map("a" -> 3, "c" -> 4), Map("c" -> 5, "d" -> "abc"))
l: List[Map[String,Any]] = List(Map(a -> 1, b -> 2.8), Map(a -> 3, c -> 4), Map(c -> 5, d -> abc))

scala> val (sa, sb, sc) = l.foldLeft((0: Int, 0: Double, 0: Int)){
     |   case ((a, b, c), m) => (
     |     a + m.get("a").collect{case i: Int => i}.getOrElse(0),
     |     b + m.get("b").collect{case i: Double => i}.getOrElse(0.),
     |     c + m.get("c").collect{case i: Int => i}.getOrElse(0)
     |     )
     |   }
sa: Int = 4
sb: Double = 2.8
sc: Int = 9

Updated using incrop's idea of collect instead of match.

senia
  • 37,745
  • 4
  • 88
  • 129
  • @paradigmatic: Not "exactly": with `Option[Any].getOrElse` you'll get `Any`, with `match/case` you'll get the type you want. – senia Apr 04 '12 at 09:47
  • @paradigmatic: Instead of `m.get("a").getOrElse(0)` you can use `m.getOrElse("a", 0)`, but you'll still get `Any`. – senia Apr 04 '12 at 10:11
  • 2
    You can use `collect` to get desired type: `m.get("a").collect{case x: Int => x}.getOrElse(0)` – incrop Apr 04 '12 at 10:24
3

First, you totally miss the point of pattern matching

{case i: Int => i
 case d: Double => d
 case _ => 0} 

is the proper replacement of all your function inside mapValues. Yet this is not the problem, your writing, while complex, does the same thing.

Your function in mapValues returns Double (because some branches return Int and others return Double, and in this case, Int is promoted to Double. If it were not, it would return AnyVal). So you get a List[Map[String, Double]]. At this point, you have lost the Ints.

When you do m.get("a"), this returns Option[Double]. Option[A] has method getOrElse(default: A) : A (actually, default: => X) but it makes no difference here).

If you call getOrElse(0.0) instead of getOrElse(0), you get a Double. Your code still fails, because your fold start with (Int, Double, Double), and you would return (Double, Double, Double). If you start your fold with (0.0, 0.0, 0.0), it works, but you have lost your Ints, you get (4.0, 2.8, 9.0)

Now, about the error message. You pass an Int to a method expecting a Double (getOrElse), the Int should normally be converted to Double, and it would be as if you called with getOrElse(0.0). Except that Option is covariant (declared trait Option[+A]). if X is an ancestor of A, an Option[A] is also an Option[X]. So an Option[Double] is also Option[AnyVal] and Option[Any]. The call getOrElse(0) works if the option is considered an Option[AnyVal], and the result is AnyVal (would work with Any too, but AnyVal is more precise and this is the one the compiler chooses). Because the expression compiles as is, there is no need to promote the 0 to 0.0. Thus m.get("a").getOrElse(0) is of type AnyVal, which cannot be added to t._1. This is what your error message says.

You have knowledge that "a" is associated with Int, "b" with double, but you don't pass this knowledge to the compiler.

Didier Dupont
  • 29,398
  • 7
  • 71
  • 90
0

In general, if you don't know the keys, but just want to sum values you can do

val filtered = for {
    map <- l
    (k, v) <- map
    if v.isInstanceOf[Number]
  } yield k -> v.asInstanceOf[Number].doubleValue

val summed = filtered.groupBy(_._1) map { case (k, v) => k -> v.map(_._2).sum }

scala> l
res1: List[Map[String,Any]] = List(Map(a -> 1, b -> 2.8), Map(a -> 3, c -> 4), Map(c -> 5, d -> abc))

scala> filtered
res2: List[(String, Double)] = List((a,1.0), (b,2.8), (a,3.0), (c,4.0), (c,5.0))

scala> summed
res3: Map[String,Double] = Map(c -> 9.0, a -> 4.0, b -> 2.8)

Update

You can filter map by type you want, for example

scala> val intMap = for (x <- l) yield x collect { case (k, v: Int) => k -> v }
intMap: List[scala.collection.immutable.Map[String,Int]] = List(Map(a -> 1), Map(a -> 3, c -> 4), Map(c -> 5))

and then sum values (see linked question)

scala> intMap reduce { _ |+| _ }
res4: scala.collection.immutable.Map[String,Int] = Map(a -> 4, c -> 9)
Community
  • 1
  • 1
4e6
  • 10,696
  • 4
  • 52
  • 62
0

A nifty one-liner:

l.map(_.filterKeys(_ != "d")).flatten groupBy(_._1) map { case (k,v) => v map { case (k2,v2: Number) => v2.doubleValue} sum }

res0: scala.collection.immutable.Iterable[Double] = List(9.0, 4.0, 2.8)
Destin
  • 1,194
  • 2
  • 10
  • 12
-2

Am I missing something or can you not just do:

map.values.sum

?

sprague44
  • 121
  • 5