Soumya Simanta
Soumya Simanta

Reputation: 11751

Creating a generic version of a function that groups elements of a list that satisfy certain certain conditions

I've a function that groups a List of Persons if their first names are same AND and they are adjacent to each other in the list.

  case class Person(first:String,last:String,age:Int) 

  @annotation.tailrec
  def groupPersons(persons: List[Person], results: List[List[Person]] = Nil): List[List[Person]] = persons match {
    case Nil => results.reverse
    case persons => groupPersons(persons.dropWhile(_.first == persons.head.first), persons.takeWhile(_.first == persons.head.first) :: results)
  }

I would like to create a generic version of this function that works other types as well and takes in a function that is passed to dropWhile and takeWhile.

Something like:

  def group(p: List[P], results: List[List[P]] = Nil, f: (P,P) => Boolean): List[List[P]] = {
    p match {
      case Nil => results.reverse
      case p => group(p.dropWhile(f), p.takeWhile(f) :: results, f)
    }
  }

Cannot make the above to work? Any ideas how to fix this?

Upvotes: 0

Views: 92

Answers (2)

The Archetypal Paul
The Archetypal Paul

Reputation: 41769

You are most of the way there. Adding a type parameter to group fixes one problem. Your other problem is that dropWhile and takeWhile takas a predicate (P=> Boolean), but you want to use an f that takes two parameters. We can fix this by currying f and partially applying. Finally, span will be slightly more efficient than your takeWhile/dropWhile

  def group[P](p: List[P], 
               results: List[List[P]] = Nil, 
               f: (P,P) => Boolean): List[List[P]] = {
   p match {
     case Nil => results.reverse

     case p => {
       val g = f.curried(p.head)
       val (matching, rest) = p.span(g)
       group(rest, matching :: results, f)
     }
   }
} 

val xs = List(Person("John", "Doe", 42), Person("Jane", "Smith", 50), 
              Person("Jane", "Piper", 52), Person("Mary", "Morse", 52))

def matchFirstName(a:Person,b:Person) = a.first == b.first

group(xs, Nil, matchFirstName) 
//> res0: List[List[exprs.exprs.Person]] = List(List(Person(John,Doe,42)), List(
//| Person(Jane,Smith,50), Person(Jane,Piper,52)), List(Person(Mary,Morse,52)))

Upvotes: 2

4lex1v
4lex1v

Reputation: 21557

I'm not sure if this want you need, but there is a better way:

case class Person(first:String,last:String,age:Int)
val xs = List(Person("John", "Doe", 42), Person("Jane", "Smith", 50), Person("Jane", "Piper", 52), Person("Mary", "Morse", 52))

There is a function groupBy:

scala> xs.groupBy(_.first)
res0: Map[String,List[Person]] = 
  Map(Jane -> List(Person(Jane,Smith,50), Person(Jane,Piper,52)), 
      Mary -> List(Person(Mary,Morse,52)), 
      John -> List(Person(John,Doe,42)))

you can convert to into a List: xs.groupBy(_.first).toList and then continue process your data.

Thanks to @Paul, i haven't noted the requirement to be "adjacent to each other". It looks to me like a simple sort application, i.e sortWith function which takes a function lt: (A, A) => Boolean just like your f parameter.

Upvotes: 1

Related Questions