Scala convert from list to map by property with sum logic

Viewed 94

I'm new to Scala, trying to convert from list to map with some sum logic as in the following.

case class ProductProperty(name:String, value:Option[String]= None, options:List[OptionItem]=List())

case class OptionItem(title:Option[String], description:Option[String] = None , price:Int)

val properties = List(ProductProperty(name = "size", value = Some("val1") , options =  Some(List(OptionItem(price = 10) , OptionItem(price = 204))),
         ProductProperty(name = "size", value = Some("val2") , options = Some(List(OptionItem(price = 122) , OptionItem(price = 240))),
         ProductProperty(name = "color", value = Some("val3") , options = Some(List(OptionItem(price = 101) , OptionItem(price = 204))),
         ProductProperty(name = "color", value = Some("val13") , options = Some(List(OptionItem(price = 102) , OptionItem(price = 120))),
         ProductProperty(name = "Quantity", value = Some("ssval3") , options = Some(List(OptionItem(price = 1011) , OptionItem(price = 204))),
         ProductProperty(name = "Quantity", value = Some("ssval13") , options = Some(List(OptionItem(price = 1102) , OptionItem(price = 1210))
     )

I need to flatten it and create new map based on their name with calculated price.

Map {
     "size" -> total price,
     "color" -> total price,
     "Quantity" -> total price
}

What I've tried so far:

val optList =   properties.map( list =>
      list.map(op => op.options
        .flatMap(i => List(ProductProperty(name = op.name, value = Some(i.value)))))
        .flatten
    )

val totalPrice = price.add(optList.map(list =>
      list.map(_.options)
        .flatten.map(_.price.getOrElse(0)).sum)
      .getOrElse(0))
    
Map((optList.map(_.name).getOrElse(List())) -> totalPrice)

But it is not correct.

2 Answers

The first issue is to correct the sample data so that it compiles.

case class OptionItem(title       :Option[String] = None
                     ,description :Option[String] = None
                     ,price       :Int)

case class ProductProperty(name    :String
                          ,value   :Option[String] = None
                          ,options :List[OptionItem] = List())

val properties =
  List(ProductProperty("size", Some("val1"), List(OptionItem(price = 10), OptionItem(price = 204)))
      ,ProductProperty("size", Some("val2"), List(OptionItem(price = 122), OptionItem(price = 240)))
      ,ProductProperty("color", Some("val3"), List(OptionItem(price = 101), OptionItem(price = 204)))
      ,ProductProperty("color", Some("val13"), List(OptionItem(price = 102), OptionItem(price = 120)))
      ,ProductProperty("Quantity", Some("ssval3"), List(OptionItem(price = 1011), OptionItem(price = 204)))
      ,ProductProperty("Quantity", Some("ssval13"), List(OptionItem(price = 1102), OptionItem(price = 1210))))

After that it's a simple matter of applying groupBy(), map(), and fold() or reduce(). Scala 2.13.x offers all 3 at once.

properties.groupMapReduce(_.name)(_.options.map(_.price).sum)(_+_)
//res0: Map[String,Int] = Map(size -> 576, color -> 527, Quantity -> 3527)

For older Scala versions you have to break it up into smaller steps.

properties.groupBy(_.name)
          .map{case (k,v) => k -> v.map(_.options.map(_.price).sum).sum}

Complementing jwvh answer, you can use cats (also in 2.12) to simplify the code a little:

import cats.syntax.all._

val result = properties.foldMap {
  case ProductProperty(name, _, options) =>
    Map(name -> options.foldMap(_.price))
}
// result: Map[String,Int] = Map(size -> 576, color -> 527, Quantity -> 3527)

The trick here is that foldMap merges a map and a foldLeft into a single step; so options.foldMap(_.price) is essentially the same as options.map(_.price).foldLeft(0)(_ + _)

But, the most interesting part is the properties.foldMap this because the Monoid for Maps basically merges two maps together and when they have the same keys it simply combine its values together.


You can see the code running here.

Related