Sort list of objects by multiple properties and using natural sorting

Viewed 457

I would like to sort a list of objects by a Boolean property and then a String property.

The String should be compared case-insensitively, and using natural sort order as defined here

An example could be sorting persons by alive and then name:

class Person(
public val name: String,
public val alive : Boolean

)

I want the name sorting to look like this:

  • "1 Adam"
  • "13 Adam"
  • "20 adam"
  • "100 Adam"
  • "Adam 0"
  • "Adam 1"
  • "adam 10"
  • "Adam 20"
  • "Adam 100"
  • "Brian"
  • "George"

I have experimented with sorting a list of strings using the naturalOrder Comparable, and it does look promising although it seems to be case sensitive:

  val personsStr = setOf("1 Adam", "21 Adam", "13 Adam", "Adam 1", "adam 1", "Adam 0", "Adam 20", "Adam 11", "Bryan", "George")
    val personStrSorted = personsStr.sortedWith(naturalOrder())

What I am looking for is something like:

persons.sortedWith( compareBy<Person>{ it.alive }.thenBy{ it.name.toLowerCase() /*and done using natural sorting*/ } )

Question: In which way(s) can the desired sorting be achieved?

2 Answers

Somewhat hacky solution: Split your set into 2 sets by alive and sort both sets by name (this results in a natural order)::

val personsStr = setOf( Person("1 Adam", true ), Person("21 Adam", false), Person("13 Adam", true), Person( "Bryan", true) )   
val personsAlive = personsStr.filter{it.alive}.sortedBy{it.name}
val personsNotAlive = personsStr.filter{!it.alive}.sortedBy{it.name}

then you can merge them:

val result = setOf(personsAlive, personsNotAlive)

I believe you will have to implement your notion of "natural ordering" manually.

The "natural order" in Kotlin is simply defined as the order by which Comparable<T> implementations are sorted when no special comparator is provided (doc link):

First, there is natural order. It is defined for inheritors of the Comparable interface. Natural order is used for sorting them when no other order is specified.

Using naturalOrder() should give you the usual order defined by the compareTo operator for any implementor of Comparable. This means that for string, the natural order is in fact the alphabetical order.

If you know that your names are always in the form <name>, <num> <name> or <name> <num>, you should be able to implement a comparator by actually parsing the number.

One way to do it could be something like this:

fun main() {
    val personsStr = setOf("1 Adam", "21 Adam", "100 Adam", "Adam 1", "adam 1", 
                           "Adam 3", "Adam 20", "Adam 11", "Bryan", "George", "bob" )
    val persons = personsStr.map { Person(name = it, alive = kotlin.random.Random.nextBoolean()) }
    
    val personsSorted = persons.sortedWith(compareBy({ it.alive }, { it.name.parseName() }))
    
    println(personsSorted.joinToString("\n"))
}

data class Person(
    val name: String,
    val alive: Boolean
)

data class ComplexName(
    val leadingNum: Int?,
    val shortName: String,
    val trailingNum: Int?
) : Comparable<ComplexName> {

    override operator fun compareTo(other: ComplexName) = compareValuesBy(this, other, comparator, { it })
    
    companion object {
        private val comparator = compareBy<ComplexName>(
            { it.leadingNum ?: Int.MAX_VALUE }, // no leading number is behind those with leading number
            { it.shortName.toLowerCase() },
            { it.trailingNum },
        )
    }
}

fun String.parseName(): ComplexName {
    val parts = split(" ")
    return when (parts.size) {
        1 -> ComplexName(null, this, null)
        2 -> {
            val (first, second) = parts
            val firstNum = first.toIntOrNull()
            when (firstNum) {
                null -> ComplexName(null, first, second.toInt())
                else -> ComplexName(firstNum, second, null)
            }
        }
        else -> throw IllegalArgumentException("unsupported name format: $this")
    }
}

You can run this there: https://pl.kotl.in/u9O2wBFgk

Alternatively, you could define Person with a ComplexName property in the first place to avoid computing it each time when comparing.

Note that you can technically implement a comparator with natural ordering with fancier parsing, generalizing the approach for numbers in any position. It would be trickier to deal with numbers inside words though, but that depends on your goal.

Related