How can I map across multiple elements (e.g. lookahead)?

Viewed 243

I have a List[Int], perhaps like this:

List(1,2,3,3,4,5,6,6,7,8,9)

There will be occasional duplication (always 2, not more). When there's a dup I want to merge the elements together with a function. So in this simple example if my function is to add the 2 numbers together I'd get:

List(1,2,6,4,5,12,7,8,9)

What's a concise way to do this? List.map() only looks at/transforms 1 element at a time.

2 Answers

You can use .foldLeft with new list as accumulator (assuming the list has duplicates next to each other)

def mergeDuplicates(list: List[Int]): List[Int] = {
  list.foldLeft(List.empty[Int]) {
    case (l, elem) if l.lastOption.contains(elem) =>
      l.dropRight(1) :+ (2 * elem)
    case (l, elem) =>
      l :+ elem
  }
}

println(mergeDuplicates(List(1, 2, 3, 3, 4, 5, 6, 6, 7, 8, 9))) 

// output List(1, 2, 6, 4, 5, 12, 7, 8, 9)

using extension method,

implicit class ListOps(list: List[Int]) {
  def mergeDuplicates: List[Int] = {
    list.foldLeft(List.empty[Int]) {
      case (l, elem) if l.lastOption.contains(elem) =>
        l.dropRight(1) :+ (2 * elem)
      case (l, elem) =>
        l :+ elem
    }
  }
}

val mergedList = List(1, 2, 3, 3, 4, 5, 6, 6, 7, 8, 9).mergeDuplicates
println(mergedList)

Starting Scala 2.13, you could use the List#unfold combined with List#span:

// val items = List(1, 2, 3, 3, 4, 5, 6, 6, 7, 8, 9)
List.unfold(items) {
  case Nil  => None
  case rest => Some(rest.span(_ == rest.head))
}
.map(_.sum)
// List(1, 2, 6, 4, 5, 12, 7, 8, 9)

or even, coupled with Scala 2.13's Option#unless builder:

List
  .unfold(items)(rest => Option.unless(rest.isEmpty)(rest.span(_ == rest.head)))
  .map(_.sum)

  • Unfold uses an internal state which is initialized in our case with the list List(1, 2, 3, 3, 4, 5, 6, 6, 7, 8, 9) for which to sum consecutive duplicates.
  • During each iteration, we span that internal state in order to find the heading consecutive items: rest.span(_ == rest.head) (which for instance during the third iteration on the List(3, 3, 4, 5, ...) internal state gives (List(3, 3), List(5, 6, ...))).
  • Since unfold expects within each iteration an Option of a tuple whose first part is the new element to add to the list being built (during the first iteration for instance List(1)) and whose second part is the new value of the internal state (List(2, 3, 3, ...)), that span fits exactly that requirement.
  • We keep iterating the same step until the internal list is empty, in which case returning None signals the unfold that we're finished and that the iteration has to stop.
  • Finally, we replace grouped parts (List(List(1), ..., List(3, 3), ...)) by their sum with a simple .map(_.sum). This can of course also be achieved within the unfolding at the cost of some additional boilerplate.
Related