Search code examples
sortingkotlincomparatorcomparable

How to sort based on multiple types and values in Kotlin?


Given following classes:

interface Item {
    val name: String
}

data class Server(override val name: String, val id: String) : Item
data class Local(override val name: String, val date: Int) : Item
data class Footer(override val name: String) : Item

If we create a list:

val items = arrayListOf<Item>()
items.add(Server("server", "b"))
items.add(Local("local", 2))
items.add(Footer("footer"))
items.add(Server("server", "a"))
items.add(Local("local", 1))
items.add(Footer("footer"))
items.add(Server("server", "c"))
items.add(Local("local", 0))

And sort it:

val groupBy = items.groupBy { it.name }

val partialSort = arrayListOf<Item>()

//individually sort each type
partialSort.addAll(groupBy["local"]!!.map { it as Local }.sortedWith(compareBy({ it.date })))
partialSort.addAll(groupBy["server"]!!.map { it as Server }.sortedWith(compareBy({ it.id })))
partialSort.addAll(groupBy["footer"]!!.map { it as Footer })

//this can be avoided if above three lines are rearranged
val fullSort = partialSort.sortedWith(compareBy({ it is Footer }, { it is Local }, { it is Server }))

Then I get a list which looks like if it was created by following commented code:

//        items.add(Server("server", "a"))
//        items.add(Server("server", "b"))
//        items.add(Server("server", "c"))
//        items.add(Local("local", 0))
//        items.add(Local("local", 1))
//        items.add(Local("local", 2))
//        items.add(Footer("footer"))
//        items.add(Footer("footer"))

Is there a better way to sort it this way? I read How to sort based on/compare multiple values in Kotlin? and Sort collection by multiple fields in Kotlin already but couldn't apply that to my code.


Solution

  • Yes, it can be achieved in single operation (but pretty complex one) and you're thinking in right way, compareBy can do the trick for you

    items.sortWith(compareBy({
        when (it) {
            is Server -> -1
            is Local -> 0
            is Footer -> 1
            else -> Integer.MAX_VALUE
        }
    }, {
        when (it) {
            is Server -> it.id
            is Local -> it.date
            else -> 0
        }
    }))
    

    What we do here:

    1. We're creating syntetic comparator for implementations of Item. Of course this number may be just another field in interface if that's common usecase.
    2. We're defining fields to compare on Server and Local because they have additional criterion of sorting.
    3. And we're passing comparators created on steps 1 and 2 to compareBy function.

    After this operation items collections is sorted:

    [Server(name=server, id=a), Server(name=server, id=b), Server(name=server, id=c), Local(name=local, date=0), Local(name=local, date=1), Local(name=local, date=2), Footer(name=footer), Footer(name=footer)]
    

    UPD: If name of Item shoud be subject of sorting too — you can easily add one more comparator like Item::name in appropriate place.