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.