1

I have two lists which contains case class objects

case class Balance(id: String, in: Int, out: Int)

val l1 = List(Balance("a", 0, 0), Balance("b", 10, 30), Balance("c", 20, 0))

val l2 = List(Balance("a", 10, 0), Balance("b", 40, 0))

I want to sumup the elements in the tuples and combine the lists like below

List((Balance(a, 10, 0), Balance(b, 50, 30), Balance(c, 20, 0))

I have came with following solution

// create list of tuples with 'id' as key 
val a = l1.map(b => (b.id, (b.in, b.out)))
val b = l2.map(b => (b.id, (b.in, b.out)))

// combine the lists 
val bl = (a ++ b).groupBy(_._1).mapValues(_.unzip._2.unzip match {
  case (ll1, ll2)  => (ll1.sum, ll2.sum)
}).toList.map(b => Balance(b._1, b._2._1, b._2._2))

// output
// List((Balance(a, 10, 0), Balance(b, 50, 30), Balance(c, 20, 0))

Are they any shorter way to do this?

eranga
  • 519
  • 7
  • 17

3 Answers3

3

You don't really need to create the tuple lists.

(l1 ++ l2).groupBy(_.id)
          .mapValues(_.foldLeft((0,0)){
             case ((a,b),Balance(id,in,out)) => (a+in,b+out)})
          .map{
            case (k,(in,out)) => Balance(k,in,out)}
          .toList
// res0: List[Balance] = List(Balance(b,50,30), Balance(a,10,0), Balance(c,20,0))

You'll note that the result appears out of order because of the intermediate representation as a Map, which, by definition, has no order.

jwvh
  • 50,871
  • 7
  • 38
  • 64
1

Another approach would be to add a Semigroup instance for Balance and use that for the combine logic. The advantage of this is that that code is in one place only, rather that sprinkled wherever you need to combine lists or maps of Balances.

So, you first add the instance:

import cats.implicits._
implicit val semigroupBalance : Semigroup[Balance] = new Semigroup[Balance] 
{
   override def combine(x: Balance, y: Balance): Balance =
     if(x.id == y.id) // I am arbitrarily deciding this: you can adapt the logic to your 
                      // use case, but if you only need it in the scenario you asked for, 
                      // the case where y.id and x.id are different will never happen.
      Balance(x.id, x.in + y.in, x.out + y.out)
     else x
}

Then, the code to combine multiple lists becomes simpler (using your example data):

(l1 ++ l2).groupBy(_.id).mapValues(_.reduce(_ |+| _)) //Map(b -> Balance(b,50,30), a -> Balance(a,10,0), c -> Balance(c,20,0))

N.B. As @jwvh already noted, the result will not be in order, in this simple case, because of the default unordered Map the groupBy returns. That could be fixed, if needed.
N.B. You might want to use Monoid instead of Semigroup, if you have a meaningful empty value for Balance.

mdm
  • 3,928
  • 3
  • 27
  • 43
0

For those who need to merge two list of case class objects, while maintaining the original ordering, here's my solution which is based on jwvh's answer to this question and this answer.

import scala.collection.immutable.SortedMap

val mergedList: List[Balance] = l1 ++ l2

val sortedListOfBalances: List[Balance] =
         SortedMap(mergedList.groupBy(_.id).toSeq:_*)
         .mapValues(_.foldLeft((0,0)){
           case ((a,b),Balance(id,in,out)) => (a+in,b+out)
         })
         .map{
           case (k,(in,out)) => Balance(k,in,out) 
         }
         .toList

This will return List(Balance(a,10,0), Balance(b,50,30), Balance(c,20,0)) while when not using SortedMap we get List(Balance(b,50,30), Balance(a,10,0), Balance(c,20,0)).

map always returns in an unspecified order unless we specifically use a subtype of SortedMap.

consuela
  • 1,675
  • 6
  • 21
  • 24