我有一个简单的案例类:
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
,计算起来就会简单得多。如上所述,我们可以非常简单地在Option
s的列表上使用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)
}
旁注:递归在这里是不必要的,因为一旦我们有了Int
s的列表,我们就可以使用List.max
。然而,如果类型更复杂,这种模式可以很容易地应用,而且我只是觉得它很整洁。