How to sort struct with multiple sort parameters?

SortingGo

Sorting Problem Overview


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.

Sorting Solutions


Solution 1 - Sorting

Use the sort.Slice (available since Go 1.8) or the sort.Sort function to sort a slice of values.

With both functions, the application provides a function that tests if one slice element is less than another slice element. To sort by last name and then first name, compare last name and then first name:

if members[i].LastName < members[j].LastName {
	return true
}
if members[i].LastName > members[j].LastName {
	return false
}
return members[i].FirstName < members[j].FirstName

The less function is specified using an anonymous function with sort.Slice:

var members []Member
sort.Slice(members, func(i, j int) bool {
	if members[i].LastName < members[j].LastName {
		return true
	}
	if members[i].LastName > members[j].LastName {
		return false
	}
	return members[i].FirstName < members[j].FirstName
})

The less function is specified with through an interface with the sort.Sort function:

type byLastFirst []Member

func (members byLastFirst) Len() int           { return len(members) }
func (members byLastFirst) Swap(i, j int)      { members[i], members[j] = members[j], members[i] }
func (members byLastFirst) Less(i, j int) bool { 
    if members[i].LastName < members[j].LastName {
       return true
    }
    if members[i].LastName > members[j].LastName {
       return false
    }
    return members[i].FirstName < members[j].FirstName
}

sort.Sort(byLastFirst(members))

Unless performance analysis shows that sorting is a hot spot, use the function that's most convenient for your application.

Solution 2 - Sorting

Use the newer sort.Slice function as such:

sort.Slice(members, func(i, j int) bool {
	switch strings.Compare(members[i].FirstName, members[j].FirstName) {
	case -1:
		return true
	case 1:
		return false
	}
	return members[i].LastName > members[j].LastName
})

or something like that.

Solution 3 - Sorting

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

Solution 4 - Sorting

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) }

Solution 5 - Sorting

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.

Solution 6 - Sorting

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>

Solution 7 - Sorting

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.

Solution 8 - Sorting

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

Solution 9 - Sorting

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)

Solution 10 - Sorting

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
}

Solution 11 - Sorting

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

Solution 12 - Sorting

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMelvinView Question on Stackoverflow
Solution 1 - SortingBayta DarellView Answer on Stackoverflow
Solution 2 - SortingabourgetView Answer on Stackoverflow
Solution 3 - SortingLVBView Answer on Stackoverflow
Solution 4 - SortingRoland IlligView Answer on Stackoverflow
Solution 5 - SortingcypharView Answer on Stackoverflow
Solution 6 - SortingZomboView Answer on Stackoverflow
Solution 7 - SortingDouglas AdolphView Answer on Stackoverflow
Solution 8 - SortingIvan TolstosheyevView Answer on Stackoverflow
Solution 9 - SortingDaniel PacakView Answer on Stackoverflow
Solution 10 - SortingalextanhongpinView Answer on Stackoverflow
Solution 11 - Sortinguser3790829View Answer on Stackoverflow
Solution 12 - SortingKurt PeekView Answer on Stackoverflow