How to sort struct with multiple sort parameters?

Viewed 43429

I have an array/slice of members:

type Member struct {
    Id int
    LastName string
    FirstName string
}

var members []Member

My question is how to sort them by LastName and then by FirstName.

12 Answers

Another pattern, which I find slightly cleaner:

if members[i].LastName != members[j].LastName {
    return members[i].LastName < members[j].LastName
}

return members[i].FirstName < members[j].FirstName

The shortest and still comprehensible code I managed to write for this is:

package main

import (
    "fmt"
    "sort"
)

type Member struct {
    Id        int
    LastName  string
    FirstName string
}

func sortByLastNameAndFirstName(members []Member) {
    sort.SliceStable(members, func(i, j int) bool {
        mi, mj := members[i], members[j]
        switch {
        case mi.LastName != mj.LastName:
            return mi.LastName < mj.LastName
        default:
            return mi.FirstName < mj.FirstName
        }
    })
}

The pattern using the switch statement easily extends to more than two sorting criteria and is still short enough to be read.

Here's the rest of the program:

func main() {
    members := []Member{
        {0, "The", "quick"},
        {1, "brown", "fox"},
        {2, "jumps", "over"},
        {3, "brown", "grass"},
        {4, "brown", "grass"},
        {5, "brown", "grass"},
        {6, "brown", "grass"},
        {7, "brown", "grass"},
        {8, "brown", "grass"},
        {9, "brown", "grass"},
        {10, "brown", "grass"},
        {11, "brown", "grass"},
    }

    sortByLastNameAndFirstNameFunctional(members)

    for _, member := range members {
        fmt.Println(member)
    }
}

A completely different idea, but same API

If you want to avoid mentioning the fields LastName and FirstName multiple times and if you want to avoid mixing i and j (which can happen all the way), I played around a bit and the basic idea is:

func sortByLastNameAndFirstNameFunctional(members []Member) {
    NewSorter().
        AddStr(member -> member.LastName).
        AddStr(member -> member.FirstName).
        AddInt(member -> member.Id).
        SortStable(members)
}

Since Go doesn't support the -> operator for creating anonymous functions and also doesn't have generics like Java, a bit of syntactical overhead is needed:

func sortByLastNameAndFirstNameFunctional(members []Member) {
    NewSorter().
        AddStr(func(i interface{}) string { return i.(Member).LastName }).
        AddStr(func(i interface{}) string { return i.(Member).FirstName }).
        AddInt(func(i interface{}) int { return i.(Member).Id}).
        SortStable(members)
}

The implementation as well as the API is a bit ugly using interface{} and reflection, but it only mentions each field once, and the application code does not have a single chance to accidentally mix the indexes i and j since it doesn't deal with them.

I designed this API in the spirit of Java's Comparator.comparing.

The infrastructure code for the above sorter is:

type Sorter struct{ keys []Key }

func NewSorter() *Sorter { return new(Sorter) }

func (l *Sorter) AddStr(key StringKey) *Sorter { l.keys = append(l.keys, key); return l }
func (l *Sorter) AddInt(key IntKey) *Sorter    { l.keys = append(l.keys, key); return l }

func (l *Sorter) SortStable(slice interface{}) {
    value := reflect.ValueOf(slice)
    sort.SliceStable(slice, func(i, j int) bool {
        si := value.Index(i).Interface()
        sj := value.Index(j).Interface()
        for _, key := range l.keys {
            if key.Less(si, sj) {
                return true
            }
            if key.Less(sj, si) {
                return false
            }
        }
        return false
    })
}

type Key interface {
    Less(a, b interface{}) bool
}

type StringKey func(interface{}) string

func (k StringKey) Less(a, b interface{}) bool  { return k(a) < k(b) }

type IntKey func(interface{}) int

func (k IntKey) Less(a, b interface{}) bool  { return k(a) < k(b) }

A slightly cleaner solution than the currently-accepted answer is to do something more like this:

sort.Slice(members, func(i, j int) bool {
    if members[i].FirstName != members[j].FirstName {
        return members[i].FirstName < members[j].FirstName
    }
    return members[i].LastName < members[j].LastName
})

The nice thing about doing it this way is that it's much clearer how you would extend it if you added a new field Age (or something):

sort.Slice(members, func(i, j int) bool {
    if members[i].FirstName != members[j].FirstName {
        return members[i].FirstName < members[j].FirstName
    }
    if members[i].Age != members[j].Age {
        return members[i].Age < members[j].Age
    }
    return members[i].LastName < members[j].LastName
})

So the pattern is that you add an if X[i] != X[j] statement for all properties until the last one, which is just compared normally.

You can create a slice of functions:

package main

type (
   mFunc func(a, b member) bool
   member struct { lastName, firstName string }
)

var members = []member{
   {"Mullen", "Larry"},
   {"Howard", "James"},
   {"Clayton", "Adam"},
   {"Howard", "Ben"},
}

var mFuncs = []mFunc{
   func(a, b member) bool { return a.lastName < b.lastName },
   func(a, b member) bool { return a.firstName < b.firstName },
}

and then iterate through the functions, until one of the functions finds a difference:

package main

import (
   "fmt"
   "sort"
)

func main() {
   sort.Slice(members, func(a, b int) bool {
      ma, mb := members[a], members[b]
      for _, mf := range mFuncs {
         if mf(ma, mb) {
            return true
         }
         if mf(mb, ma) {
            break
         }
      }
      return false
   })
   fmt.Println(members) // [{Clayton Adam} {Howard Ben} {Howard James} {Mullen Larry}]
}

https://golang.org/pkg/sort#example__sortMultiKeys

This is how I usually do it:

// You can edit this code!
// Click here and start typing.
package main

import (
    "fmt"
    "sort"
    "strings"
)

type User struct {
    Name string
    Age  int
}

func main() {
    users := []User{
        {"A", 10},
        {"B", 10},
        {"C", 20},
        {"D", 5},
        {"A", 100},
    }
    sort.Slice(users, func(i, j int) bool {
        lhs, rhs := users[i], users[j]
        byAge := lhs.Age - rhs.Age
        byName := strings.Compare(lhs.Name, rhs.Name) // Returns 0 if equal, -1 if lhs is less than rhs, and 1 if lhs is greater than rhs
        
        // The + sign is not necessary, but adds clarity as it means increasing in value, aka ascending.
        // sortBy(+byAge, +byName) // Sort by age asc, by name asc
        // sortBy(-byAge, +byName) // Sort by age desc, by name asc
        // sortBy(+byName, +byAge) // Sort by name asc, by age asc
        return sortBy(-byAge, -byName) // Sort by age desc, by name desc
    })
    fmt.Println(users)
}

func sortBy(sc ...int) bool {
    for _, c := range sc {
        if c != 0 {
            return c < 0
        }
    }
    return sc[len(sc)-1] < 0
}

This has been very helpful. I needed to sort a slice of structs, and found my answer here. I actually extended it to triply-sorting. Although sorting this much is not optimal for runtime purposes, it's useful in some circumstances, especially when the alternative leads to code that is hard to maintain or modify and where faster runtimes are not crucial.

sort.Slice(servers, func(i, j int) bool {
        if servers[i].code < servers[j].code {
            return true
        } else if servers[i].code > servers[j].code {
            return false
        } else {
            if servers[i].group < servers[j].group {
                return true
            } else {
                if servers[i].group > servers[j].group {
                    return false
                }
            }
        }
        return servers[i].IDGroup < servers[j]. IDGroup
    })

This code sorts first by code, then by group, then by IDGroup.

Just created go project to implement it: https://github.com/itroot/keysort

You can just return a list of fields to sort:

package main

import (
    "fmt"

    "github.com/itroot/keysort"
)

type Data struct {
    A int
    B string
    C bool
}

func main() {
    slice := []Data{{1, "2", false}, {2, "2", false}, {2, "1", true}, {2, "1", false}}
    keysort.Sort(slice, func(i int) keysort.Sortable {
        e := slice[i]
        return keysort.Sequence{e.A, e.B, e.C}
    })
    fmt.Println(slice)
}

Playground link: https://play.golang.org/p/reEDcoXNiwh

Just read the code of the SortMultiKeys example in the official Go documentation at https://pkg.go.dev/sort and adapt it to your Member structure.

To keep this answer up to date I won't copy and paste the whole example here, but eventually you'll be able to write it as follows:

OrderedBy(lastName, firstName).Sort(members)

If the requirement changed to sort also by age, you'd simply add the age closure:

OrderBy(lastName, firstName, age).Sort(members)

All good answers, If you don't want to write any configuration. You can use the external package to perform sorting.

go get -d github.com/raunakjodhawat/multisort

And call the function like so:

sortedSlice, err := multisort.MultiSorted(inputSlice, inputKeys, SortOrders)

To look at a concrete example, go to: https://github.com/raunakjodhawat/multisort

Here is a slightly more concise implementation of the accepted answer:

package main

import (
    "fmt"
    "sort"
)

type Member struct {
    FirstName string
    LastName  string
}

func main() {
    members := []Member{
        {"John", "Doe"},
        {"Jane", "Doe"},
        {"Mary", "Contrary"},
    }

    fmt.Println(members)

    sort.Slice(members, func(i, j int) bool {
        return members[i].LastName < members[j].LastName || members[i].FirstName < members[j].FirstName
    })

    fmt.Println(members)
}

Running will print the following output:

[{John Doe} {Jane Doe} {Mary Contrary}]
[{Mary Contrary} {Jane Doe} {John Doe}]

This is as expected: the members are printed in order of ascending last name, where in the case of a tie, they are printed in order of first name.

Related