简体   繁体   中英

Split a list into sublist based on element types in scala

How to split a list into sublist based on element types?

In short, given:

trait Drink
final case object Coke extends Drink
final case object Pepsi extends Drink

val drinks = List(Coke,Coke,Pepsi,Coke,Pepsi,Pepsi)

I want:

List( List(Coke,Coke), List(Pepsi), List(Coke), List(Pepsi, Pepsi) )
    drinks.foldRight[List[List[Drink]](List.empty) {
       case (next, (l@(last :: _) :: tail) if next.getClass == last.getClass => 
          (next :: l)::tail
       case (next, rest) => List(next) :: rest
    }

If types are parametric you'll need TypeTags as mentioned in the comment... but at that point, it's really going to be easier to just add a method to the class itself... something like:

    class Drink[T: ClassTag] {
       def typeId = s"Drink of ${classTag[T].runtimeClass.getName}"
    }

Then you can just compare these type ids rather than actual classes.

drinks.foldRight(List.empty[List[Drink]]){
  case (c:Coke.type, ((hd:Coke.type)::tl)::acc)  => (c::hd::tl)::acc
  case (p:Pepsi.type,((hd:Pepsi.type)::tl)::acc) => (p::hd::tl)::acc
  case (d, acc) => List(d)::acc
}

Somewhat verbose partly due to them being case objects.

You can use a short tail-recursive function such as the following.

The idea is to use next to store the "next List of Drinks to append to the result" and acc to accumulate these Lists of Drinks, into a List of List of Drinks.

The base case is an empty list, in which the results are returned. Otherwise, either the next drink matches the next sublist (add it to this sublist), or it doesn't (add the sublist to the result and start a new sublist with the new drink).

Note that :+ is a List method which gives back a new List with the specified item appended.

@tailrec
def get(list:List[Drink],
        next:List[Drink]=List(),
        acc: List[List[Drink]]=List()): List[List[Drink]] =
  list match {
    case Nil => acc :+ next   // dump final results
    case head :: tail =>
      if (next.isEmpty || next.head.getClass == head.getClass) get(tail, next :+ head, acc)
      else get(tail, List(head), acc :+ next)
  }


println(get(drinks))

Result:

List(List(Coke, Coke), List(Pepsi), List(Coke), List(Pepsi, Pepsi))

Note, noticed that jwvh has a correct answer too, with proper pattern matching instead of these conditionals. Using the head method on a List can be unsafe (or hard for the compiler to determine the safety), but this approach may be more concise especially if there are many kinds of Drink.

If you want to avoid using head directly, you can write this, which I find more confusing:

...
      if (next.headOption.map(h => h.getClass == head.getClass).getOrElse(true)) get(tail, next :+ head, acc)
...

why don't use just groupBy on your list?

val rows = drinks.groupBy(x => x).values.toList

Can test here

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM