Scala-在自定义对象列表中查找值的最大值和总和



我有一个简单的案例类:

case class Task (
val name: String,
val value: Option[Int],
val description: String
)

我有一个这些对象的列表,其中相同的name可能是多次。我想找到具有相同name的相同对象的最大value,并对这些值求和。

但我有一个问题,因为我不知道如何找到最大值为Options的值。如果它们是"无"怎么办?最简单的方法是什么?我试过类似的东西:

val max = list.filter(_.name == "Homework").reduceLeft(_.value max _.value).get

但它不起作用,因为如果某个value为None,那么我就会出错。

如果您使用的是Scala 2.13.x,那么您可以使用groupMapReduce()

val maxAndSumByName: Map[String,(Int,Int)] =
tasks.groupMapReduce(_.name
)(_.value.fold(Int.MinValue,0)(n=>(n,n))
){case ((ax,as),(bx,bs)) =>
(ax max bx, as+bs)
}

元组中的第一个Int是最大值,第二个Int是和。

当列表中有一些任务出现一次并且值为None时,条目没有最大值。因此,如果我们将最大值表示为Option[Int],这是有意义的。我想出的解决方案是,

val nameMaxMap: Map[String, (Option[Int], Int)] =
tasks.groupBy(t => t.name)
.map(entry => (
entry._1,
entry._2.foldLeft((Option.empty[Int], 0)) {
case ((None, sum), Task(_, None, _)) => (None, sum)
case ((None, sum), Task(_, Some(value), _)) => (Some(value), sum + value)
case ((Some(soFarMax), sum), Task(_, None, _)) => (Some(soFarMax), sum)
case ((Some(soFarMax), sum), Task(_, Some(value), _)) => (Some(value max soFarMax), sum + value)
}))

正如我在注释中提到的,只要选项中的类型是可排序的,stdlib就会为可选值提供默认排序。

因此,使用groupMap可以很容易地解决这个问题,生成从任务名称到其所有值的Map,然后从每个组中选择max,最后将所有这些最大值相加。

def sumByMax(data: List[Task]): Int =
data
.groupMap(_.name)(_.value)
.valuesIterator
.map(_.max.getOrElse(0))
.sum

运行在Scastie中的代码。

正如其他人所提到的,分组是一种非常方便的工具:

假设我有一个名为tasks的任务列表,我们可以按名称对它们进行分组,如下所示:


val tasksGroupedByName: Map[String, List[Task]] = tasks.groupBy(_.name)

为了计算总和,对于每个名称(或使用0作为默认值[即空列表](,我们还可以使用一些内置函数:


val sums: Map[String, Int] = tasksGroupedByName.map {
case nameToTaskMap: (String, List[Task]) =>
// Destructure to get the name and the taskList (each map entry)
val (name, taskList) = nameToTaskMap
// Use map to get only the value, and use flatten to get only the Ints
val valueList: List[Int] = taskList.map(_.value).flatten
// We're not reinventing the wheel here
val sum: Int = valueList.sum
(name, sum)
}

我们可以使用类似的逻辑来计算最大值。其他人提供了一些很好的解决方案,但我想分享另一种可能性。

如果我们去掉Option,计算起来就会简单得多。如上所述,我们可以非常简单地在Options的列表上使用flatten来实现这一点,这将产生Option中包含的类型的List,在本例中为Int


def maxValue(valueList: List[Int]): Option[Int] = {
valueList match {
// BASE CASE: Empty list
case Nil => None
// BASE CASE: Single value
case singleValue :: Nil => Some(singleValue)
// First value is larger than second value
// Repeat, but remove second value (Size: n - 1)
case firstValue :: secondValue :: tailValues if firstValue > secondValue =>
maxValue(firstValue :: tailValues)
// First value is NOT larger than second value
// Repeat, but without first value (Size: n - 1)
case _ :: tailValues => maxValue(tailValues)
}
}
val maxValues: Map[String, Option[Int]] = tasksGroupedByName.map{
case nameToTaskMap: (String, List[Task]) => 
val (name, taskList) = nameToTaskMap
val valueList: List[Int] = taskList.map(_.value).flatten
val maxVal: Option[Int] = maxValue(valueList)
(name, maxVal)
}

旁注:递归在这里是不必要的,因为一旦我们有了Ints的列表,我们就可以使用List.max。然而,如果类型更复杂,这种模式可以很容易地应用,而且我只是觉得它很整洁。

最新更新