guh.me - gustavo's personal blog

Sorting by multiple conditions

During a recent project at work, I encountered an interesting challenge: implementing multi-condition sorting for a complex data structure. While I was familiar with this concept from SQL queries using ORDER BY clauses, I had never implemented it programmatically before. After some careful consideration, I developed a solution in Go, which I later had to port to PHP (though unfortunately without the benefit of generics support 🥲).

I started by implementing a generic struct to hold the data I am sorting along with the comparison functions:

type Sorter[T any] struct {
    items []T
    less  []func(a, b T) bool
}

Then, I implemented the Len, Swap and Less to implement the sort.Interface and be able to use the regular methods from the sort package:

func (ms Sorter[T]) Len() int {
    return len(ms.items)
}

func (ms Sorter[T]) Swap(i, j int) {
    ms.items[i], ms.items[j] = ms.items[j], ms.items[i]
}

func (ms Sorter[T]) Less(i, j int) bool {
    for _, less := range ms.less {
        switch {
        case less(ms.items[i], ms.items[j]):
            return true
        case less(ms.items[j], ms.items[i]):
            return false
        }
    }
	
    return false
}

Finally, I implemented a helper function to wrap the custom Sorter type:

func OrderedBy[T any](items []T, less ...func(T, T) bool) *Sorter[T] {
	return &Sorter[T]{
		items: items,
		less:  less,
	}
}

Then, it was a matter of using it. It can be done as follows:

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
    City string
}

// Gimme a bunch of people
people := []Person{
    {"Bob", 25, "London"},
    {"Charlie", 30, "London"},
    {"Alice", 25, "New York"},
    {"David", 20, "Paris"},
	{"Bob", 25, "Berlin"},
}

// Define some ordering criteria
byAge := func(a, b Person) bool { return a.Age < b.Age }
byName := func(a, b Person) bool { return a.Name < b.Name }
byCity := func(a, b Person) bool { return a.City < b.City }

// Sort first by age, then name, then city, in ascending order
sorter := OrderedBy(people, byAge, byName, byCity)
sort.Sort(sorter)

fmt.Println(people)
// [{David 20 Paris} {Alice 25 New York} {Bob 25 London} {Charlie 30 London}]

It was a very interesting exercise, and I honestly had a lot of fun doing it.