3

I have a requirement to write a generic code that perform sorting on the Seq[T] objects. I know it won't be possible to perform sorting operation until we know the base class and its attributes. After taking a look into this answer I took this code and my requirement is to handle as many custom data type as possible.

case class Country(name: String, id : Int)
type CountrySorter = (Country, Country) => Boolean
def byName : CountrySorter = (c1:Country, c2:Country) => c1.name < c2.name
def byId : CountrySorter = (c1:Country, c2:Country) => (c1.id < c2.id)

val sortingMap = Map[String, CountrySorter](
  "sortByCountryName" -> byName ,
  "soryByCountryId" -> byId
 )

Function call

def sort[T]( input : Seq[T], criteria : String) : Seq[T] = {
  input.sortWith(sortingMap(criteria))
}

input.sortWith(sortingMap(criteria)) here I get error as sortWith function only takes Country type and not T type.

Puneeth Reddy V
  • 1,538
  • 13
  • 28

3 Answers3

6

Here's an approach if you want to define your ordering using sortWith :

case class Country(name: String, id : Int)

type Sorter[T] = (T, T) => Boolean
type CountrySorter = Sorter[Country]

def byName : CountrySorter = (c1, c2) => c1.name < c2.name
def byId : CountrySorter = (c1, c2) => c1.id < c2.id

def sort[T](input: Seq[T], sorter: Sorter[T]): Seq[T] = {
  input.sortWith(sorter)
}

val countries = List(Country("Australia", 61), Country("USA", 1), Country("France", 33))

sort(countries, byName)
// res1: Seq[Country] = List(Country(Australia,61), Country(France,33), Country(USA,1))

sort(countries, byId)
// res2: Seq[Country] = List(Country(USA,1), Country(France,33), Country(Australia,61))
Leo C
  • 22,006
  • 3
  • 26
  • 39
3

Sorting country by using a Map with stringly typed keys is error prone. A better alternative is to leverage the mechanism for ordering in Scala via the Ordering[A] type class.

You can use it like this:

def sort[T](input : Seq[T])(implicit order: Ordering[T]): Seq[T] = {
  input.sorted
}

The catch here is to have the right ordering in scope. You can create a single ad hoc ordering in scope:

def main(args: Array[String]): Unit = {
  implicit val byIdOrdering = Ordering.by((country: Country) => country.id)

  val countries: Seq[Country] = ???
  sort(countries)
}

You can define the ordering in the companion of the case class and explicitly import it:

object Country {
  implicit val byIdOrdering: Ordering[Country] = 
     Ordering.by((country: Country) => country.id)

  implicit val byNameOrdering: Ordering[Country] = 
     Ordering.by((country: Country) => country.name)
}

import Country.byNameOrdering
def main(args: Array[String]): Unit = {
  val countries: Seq[Country] = ???
  sort(countries)
}

You can also use the low priority implicits trick if you have such ordering rules:

trait LowPriorityCountryImplicits {
  implicit val byNameOrdering: Ordering[Country] = 
    Ordering.by((country: Country) => country.name)
}

object HighPriorityCountryImplicits extends LowPriorityCountryImplicits {
  implicit val byIdOrdering: Ordering[Country] = 
    Ordering.by((country: Country) => country.id)
}

import HighPriorityCountryImplicits._
def main(args: Array[String]): Unit = {
  val countries: Seq[Country] = ???
  sort(countries)
}

Or even explicitly pass the ordering if needed:

def main(args: Array[String]): Unit = {
  val countries: Seq[Country] = ???
  sort(countries)(Country.byNameOrdering)
}
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • Using `implicit` is not a right approach for this requirement as I should be changing the sorting order dynamically i.e. I need to bring a new function in scope for every new sorting order. – Puneeth Reddy V Mar 07 '18 at 06:15
  • @PuneethReddyV How dynamic is dynamically? How do you decide? You can always pass the ordering *explicitly* by providing the it yourself. – Yuval Itzchakov Mar 07 '18 at 06:31
  • Here in my requirement I will be getting a string from UI, which describes about the sorting order. You have used explicit's for this approach there is nothing wrong with this, but for this requirement how can it be possible for me to bring a sorting order in scope just by a `string` value? – Puneeth Reddy V Mar 07 '18 at 06:47
  • @PuneethReddyV I see what you mean. We can make this work by switching/if-elseing on the string and producing the right ordering in scope, but it might end up a bit more cumbersome than using `sortWith`. – Yuval Itzchakov Mar 07 '18 at 07:00
  • 1
    Thank you, for your answer and convincing with my comment. – Puneeth Reddy V Mar 07 '18 at 07:05
0

After using the above answers I have fulfilled this requirement with below code

Generic trait which is parent for all the child case classes i.e. contains only the fields on which sorting is performed

 sealed trait Generic{
    def name : String = ???
    def id : Int = ???
    def place : String = ???
  }

 //case class which need to be sorted 
  case class Capital( 
      countryName : String, 
      override val id: Int, 
      override val place:String
 ) extends Generic

  case class Country(
         override val name: String, 
         override val id: Int
  ) extends Generic

Sorting types

  type Sorter[T] = (T, T) => Boolean
  type CountrySorter = Sorter[Generic]
  type CapitalSorter = Sorter[Generic]

Sorting orders

  def byName : CountrySorter = (c1, c2) => c1.name < c2.name

  def byId : CountrySorter = (c1, c2) => c1.id < c2.id

  def byPlace : CapitalSorter = (s1, s2) => s1.place > s2.place

Sorting method

  def sort[T](input: Seq[T], sorter: Sorter[T]): Seq[T] = {
    input.sortWith(sorter)
  }

A data structure to hold sorting order with a name.

  val mapper = Map[String, Sorter[Generic]](
        "name" -> byName, 
        "id" -> byId, 
        "place" -> byPlace
       )

Input

  val countries = List(Country("Australia", 61), Country("USA", 1), Country("France", 33))
  val headQuaters = List(
    Capital("Australia", 61, "Melbourne"), 
    Capital("America", 1, "New York"), 
    Capital("France", 33, "Paris"), 
    Capital("India", 65, "New Delhi")
 )

Output

  println(sort(countries,mapper("id")))
 //List(Country(USA,1), Country(France,33), Country(Australia,61))

  println(sort(headQuaters , mapper("place")))
  //List(Capital(France,33,Paris), Capital(America,1,New York), Capital(India,65,New Delhi), Capital(Australia,61,Melbourne))
Puneeth Reddy V
  • 1,538
  • 13
  • 28