What is the shortest way to simply sort an array of structs by (arbitrary) field names?

SortingGo

Sorting Problem Overview


I just had a problem where I had an array of structs, e.g.

package main

import "log"

type Planet struct {
	Name       string  `json:"name"`
	Aphelion   float64 `json:"aphelion"`   // in million km
	Perihelion float64 `json:"perihelion"` // in million km
	Axis       int64   `json:"Axis"`       // in km
	Radius     float64 `json:"radius"`
}

func main() {
	var mars = new(Planet)
	mars.Name = "Mars"
	mars.Aphelion = 249.2
	mars.Perihelion = 206.7
	mars.Axis = 227939100
	mars.Radius = 3389.5

	var earth = new(Planet)
	earth.Name = "Earth"
	earth.Aphelion = 151.930
	earth.Perihelion = 147.095
	earth.Axis = 149598261
	earth.Radius = 6371.0

	var venus = new(Planet)
	venus.Name = "Venus"
	venus.Aphelion = 108.939
	venus.Perihelion = 107.477
	venus.Axis = 108208000
	venus.Radius = 6051.8

	planets := [...]Planet{*mars, *venus, *earth}
	log.Println(planets)
}

Lets say you want to sort it by Axis. How do you do that?

(Note: I have seen http://golang.org/pkg/sort/ and it seems to work, but I have to add about 20 lines just for simple sorting by a very simple key. I have a python background where it is as simple as sorted(planets, key=lambda n: n.Axis) - is there something similar simple in Go?)

Sorting Solutions


Solution 1 - Sorting

As of Go 1.8 you can now use sort.Slice to sort a slice:

sort.Slice(planets, func(i, j int) bool {
  return planets[i].Axis < planets[j].Axis
})

There is normally no reason to use an array instead of a slice, but in your example you are using an array, so you have to overlay it with a slice (add [:]) to make it work with sort.Slice:

sort.Slice(planets[:], func(i, j int) bool {
  return planets[i].Axis < planets[j].Axis
})

The sorting changes the array, so if you really want you can continue to use the array instead of the slice after the sorting.

Solution 2 - Sorting

UPDATE: This answer relates to older versions of go. For Go 1.8 and newer, see the AndreKR's answer above.


If you want something a bit less verbose than the standard library sort package, you could use the third party github.com/bradfitz/slice package. It uses some tricks to generate the Len and Swap methods needed to sort your slice, so you only need to provide a Less method.

With this package, you can perform the sort with:

slice.Sort(planets[:], func(i, j int) bool {
    return planets[i].Axis < planets[j].Axis
})

The planets[:] part is necessary to produce a slice covering your array. If you make planets a slice instead of an array you could skip that part.

Solution 3 - Sorting

As of Go 1.8, @AndreKR's answer is the better solution.


You can implement a collection type which implements the sort interface.

Here's an example of two such types which allow you to sort either by Axis or Name:

package main

import "log"
import "sort"

// AxisSorter sorts planets by axis.
type AxisSorter []Planet

func (a AxisSorter) Len() int           { return len(a) }
func (a AxisSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a AxisSorter) Less(i, j int) bool { return a[i].Axis < a[j].Axis }

// NameSorter sorts planets by name.
type NameSorter []Planet

func (a NameSorter) Len() int           { return len(a) }
func (a NameSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a NameSorter) Less(i, j int) bool { return a[i].Name < a[j].Name }

type Planet struct {
	Name       string  `json:"name"`
	Aphelion   float64 `json:"aphelion"`   // in million km
	Perihelion float64 `json:"perihelion"` // in million km
	Axis       int64   `json:"Axis"`       // in km
	Radius     float64 `json:"radius"`
}

func main() {
	var mars Planet
	mars.Name = "Mars"
	mars.Aphelion = 249.2
	mars.Perihelion = 206.7
	mars.Axis = 227939100
	mars.Radius = 3389.5

	var earth Planet
	earth.Name = "Earth"
	earth.Aphelion = 151.930
	earth.Perihelion = 147.095
	earth.Axis = 149598261
	earth.Radius = 6371.0

	var venus Planet
	venus.Name = "Venus"
	venus.Aphelion = 108.939
	venus.Perihelion = 107.477
	venus.Axis = 108208000
	venus.Radius = 6051.8

	planets := []Planet{mars, venus, earth}
	log.Println("unsorted:", planets)

	sort.Sort(AxisSorter(planets))
	log.Println("by axis:", planets)

	sort.Sort(NameSorter(planets))
	log.Println("by name:", planets)
}

Solution 4 - Sorting

You can, instead of implementing the Sort interface on []Planet you implement on a type that contains the collection and a closure that will do the comparison. You have to provide the implementation for the comparison closure for each property.

This method I feel is better than implementing a Sort type for each property of the struct.

This answer is almost ripped right from the sort docs so I can't take to much credit for it

package main

import (
	"log"
	"sort"
)

type Planet struct {
	Name       string  `json:"name"`
	Aphelion   float64 `json:"aphelion"`   // in million km
	Perihelion float64 `json:"perihelion"` // in million km
	Axis       int64   `json:"Axis"`       // in km
	Radius     float64 `json:"radius"`
}
    
type By func(p1, p2 *Planet) bool

func (by By) Sort(planets []Planet) {
	ps := &planetSorter{
		planets: planets,
		by:      by, 
	}
	sort.Sort(ps)
}

type planetSorter struct {
	planets []Planet
	by      func(p1, p2 *Planet) bool 
}

func (s *planetSorter) Len() int {
	return len(s.planets)
}

func (s *planetSorter) Swap(i, j int) {
	s.planets[i], s.planets[j] = s.planets[j], s.planets[i]
}

func (s *planetSorter) Less(i, j int) bool {
	return s.by(&s.planets[i], &s.planets[j])
}
    

How to call it.

func main() {
	/* Same code as in the question */

	planets := []Planet{*mars, *venus, *earth}
	
	By(func(p1, p2 *Planet) bool {
		return p1.Name < p2.Name
	}).Sort(planets)
	
	log.Println(planets)

	By(func(p1, p2 *Planet) bool {
		return p1.Axis < p2.Axis
	}).Sort(planets)
	
	log.Println(planets)
}

Here is a Demo

Solution 5 - Sorting

Here is another way to reduce some of the boiler plate. Disclaimer, it uses reflection and losses type safety.

Here is a Demo

All the magic happens in the Prop function. It takes the struct property to sort on and the order it which you want to sort (ascending, descending) and returns a function that will perform the comparisons.

package main

import (
	"log"
	"reflect"
	"sort"
)

func test(planets []Planet) {
	log.Println("Sort Name")
	By(Prop("Name", true)).Sort(planets)
	log.Println(planets)
	
	log.Println("Sort Aphelion")
	By(Prop("Aphelion", true)).Sort(planets)
	log.Println(planets)
	
	log.Println("Sort Perihelion")
	By(Prop("Perihelion", true)).Sort(planets)
	log.Println(planets)
	
	log.Println("Sort Axis")
	By(Prop("Axis", true)).Sort(planets)
	log.Println(planets)
	
	log.Println("Sort Radius")
	By(Prop("Radius", true)).Sort(planets)
	log.Println(planets)
}

func Prop(field string, asc bool) func(p1, p2 *Planet) bool {
    return func(p1, p2 *Planet) bool {

	    v1 := reflect.Indirect(reflect.ValueOf(p1)).FieldByName(field)
	    v2 := reflect.Indirect(reflect.ValueOf(p2)).FieldByName(field)

	    ret := false

	    switch v1.Kind() {
	    case reflect.Int64:
		    ret = int64(v1.Int()) < int64(v2.Int())
	    case reflect.Float64:
		    ret = float64(v1.Float()) < float64(v2.Float())
	    case reflect.String:
		    ret = string(v1.String()) < string(v2.String())
	    }

	    if asc {
		    return ret
	    }
	    return !ret
    }
}

type Planet struct {
	Name       string  `json:"name"`
	Aphelion   float64 `json:"aphelion"`   // in million km
	Perihelion float64 `json:"perihelion"` // in million km
	Axis       int64   `json:"Axis"`       // in km
	Radius     float64 `json:"radius"`
}

type By func(p1, p2 *Planet) bool

func (by By) Sort(planets []Planet) {
	ps := &planetSorter{
		planets: planets,
		by:      by, // The Sort method's receiver is the function (closure) that defines the sort order.
	}
	sort.Sort(ps)
}

type planetSorter struct {
	planets []Planet
	by      func(p1, p2 *Planet) bool // Closure used in the Less method.
}

// Len is part of sort.Interface.
func (s *planetSorter) Len() int { return len(s.planets) }

// Swap is part of sort.Interface.
func (s *planetSorter) Swap(i, j int) {
	s.planets[i], s.planets[j] = s.planets[j], s.planets[i]
}

// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter.
func (s *planetSorter) Less(i, j int) bool {
	return s.by(&s.planets[i], &s.planets[j])
}

func main() {
	test(dataSet())
}

func dataSet() []Planet {

	var mars = new(Planet)
	mars.Name = "Mars"
	mars.Aphelion = 249.2
	mars.Perihelion = 206.7
	mars.Axis = 227939100
	mars.Radius = 3389.5

	var earth = new(Planet)
	earth.Name = "Earth"
	earth.Aphelion = 151.930
	earth.Perihelion = 147.095
	earth.Axis = 149598261
	earth.Radius = 6371.0

	var venus = new(Planet)
	venus.Name = "Venus"
	venus.Aphelion = 108.939
	venus.Perihelion = 107.477
	venus.Axis = 108208000
	venus.Radius = 6051.8

	return []Planet{*mars, *venus, *earth}
}

Solution 6 - Sorting

You can implement using quick sort as well and inside the partition func, you choose which field to sort by, I choose Name for example.

package main

import (
	"fmt"
)

type Planet struct {
	Name       string  `json:"name"`
	Aphelion   float64 `json:"aphelion"`   // in million km
	Perihelion float64 `json:"perihelion"` // in million km
	Axis       int64   `json:"Axis"`       // in km
	Radius     float64 `json:"radius"`
}

func main() {
	var mars Planet
	mars.Name = "Mars"
	mars.Aphelion = 249.2
	mars.Perihelion = 206.7
	mars.Axis = 227939100
	mars.Radius = 3389.5

	var earth Planet
	earth.Name = "Earth"
	earth.Aphelion = 151.930
	earth.Perihelion = 147.095
	earth.Axis = 149598261
	earth.Radius = 6371.0

	var venus Planet
	venus.Name = "Venus"
	venus.Aphelion = 108.939
	venus.Perihelion = 107.477
	venus.Axis = 108208000
	venus.Radius = 6051.8

	planets := []Planet{mars, venus, earth}
	fmt.Println(quickSort(&planets,0,len(planets)-1))

}

func quickSort(arr *[]Planet, start, end int)[]Planet{
	if start < end{
		partitionIndex := partition(*arr,start,end)
		quickSort(arr,start,partitionIndex-1)
		quickSort(arr,partitionIndex+1, end)
	}
	return *arr
}

func partition(arr []Planet, start, end int) int{
	pivot := arr[end].Name
	pIndex := start
	for i:= start; i<end; i++{
		if arr[i].Name <= pivot{
			//	swap
			arr[i],arr[pIndex] = arr[pIndex],arr[i]
			pIndex++
		}
	}
	arr[pIndex],arr[end] = arr[end],arr[pIndex]
	return pIndex
}

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
QuestionMartin ThomaView Question on Stackoverflow
Solution 1 - SortingAndreKRView Answer on Stackoverflow
Solution 2 - SortingJames HenstridgeView Answer on Stackoverflow
Solution 3 - SortingjimtView Answer on Stackoverflow
Solution 4 - SortingrobbmjView Answer on Stackoverflow
Solution 5 - SortingrobbmjView Answer on Stackoverflow
Solution 6 - SortingMuriithi DerrickView Answer on Stackoverflow