Dynamic Sort Direction

This is a follow-up to my "Multisort via Composition" post, and won't make sense if you haven't read that post.

When sorting on a single dimension, changing the sort direction can usually be handled by changing the sorter function directly.

const data = [33, 1, 95, 77, 54, 91, 38, 89, 48, 24]

// ascending
data.sort((a, b) => a - b)

// descending
data.sort((a, b) => b - a)

making this act dynamically based on some external input can be reasonably handled by updating the function as well:

const ascending = getAscending() // true or false
data.sort((a, b) =>
    ascending
        ? a - b
        : b - a
)

This works fine for a single sorter, but quickly becomes untenable once multiple sorting dimensions are used:

const data = [{
    first: 'Emma',
    last: 'Smith',
    age: 33
}, {
    first: 'Noah',
    last: 'Johnson',
    age: 1
}, {
    first: 'Olivia',
    last: 'Williams',
    age: 95
}, {
    first: 'Liam',
    last: 'Jones',
    age: 77
}, {
    first: 'Sophia',
    last: 'Brown',
    age: 54
}, {
    first: 'Mason',
    last: 'Davis',
    age: 91
}, {
    first: 'Ava',
    last: 'Miller',
    age: 38
}, {
    first: 'Jacob',
    last: 'Smith',
    age: 89
}, {
    first: 'William',
    last: 'Moore',
    age: 48
}, {
    first: 'Isabella',
    last: 'Johnson',
    age: 24
}]

data.sort(multisort(
    (a, b) => lastNameAscending ? a.last.localeCompare(b.last) : b.last.localeCompare(a.last),
    (a, b) => firstNameAscending ? a.first.localeCompare(b.first) : b.first.localeCompare(a.first)
))

Once multisort is used, it's more convenient to just pass sorter functions around, particularly if they can be passed from an external source.

A simple way to handle dynamic direction is to toggle which sorter is used:

const byLastNameAscending = (a, b) => a.last.localeCompare(b.last)
const byLastNameDescending = (a, b) => b.last.localeCompare(a.last)
const byLastName =
    lastNameAscending
        ? byLastNameAscending
        : byLastNameDescending
...
data.sort(multisort(
    byLastName,
    byFirstName
))

This form still has issues in that it requires declaring two nearly identical sort functions, which could cause issues if an update is made to one and not the other.

A better way to handle reversing a sorter function is to use a reverser function that wraps a given sort function and reverses the arguments:

const reverseSort = sort => (a, b) => sort(b, a)

This at least avoids the need for re-declaring a given (possibly complex) sorter:

const byLastNameAscending = (a, b) => a.last.localeCompare(b.last)
const byLastNameDescending = reverseSort(byLastNameAscending)
const byLastName =
    lastNameAscending
        ? byLastNameAscending
        : byLastNameDescending
...
data.sort(multisort(
    byLastName,
    byFirstName
))

Generally speaking this will work quite well for a dynamic sort with minimal overhead.


As an aside, it can be useful to use two declarative functions for static sorting:

const ascending = sort => (a, b) => sort(a, b) //*
const descending = sort => (a, b) => sort(b, a)

data.sort(multisort(
    ascending(byLastName),
    descending(byFirstName)
))

* this can be replaced by the identity function ascending = sort => sort, but I find the symmetry to be useful for comprehension.


It's possible to abstract the dynamic sort direction even further.

Instead of using a ternary directly to choose a particular sort function, a sortDirection function can be used to generate the appropriate function based on the input of the sorting function and the direction:

const sortDirection = (sort, ascending) =>
    ascending
        ? (a, b) => sort(a, b)
        : (a, b) => sort(b, a)
data.sort(multisort(
    sortDirection(byLastName, lastNameAscending),
    sortDirection(byFirstName, firstNameAscending)
))