Jaki jest najkrótszy sposób po prostu posortowania tablicy struktur według (dowolnych) nazw pól?

130

Po prostu miałem problem gdzie miałem tablicę struktur np

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

Powiedzmy, że chcesz to posortować Axis. Jak to robisz?

(Uwaga: widziałem http://golang.org/pkg/sort/ i wygląda na to, że działa, ale muszę dodać około 20 wierszy tylko w celu prostego sortowania za pomocą bardzo prostego klucza. Mam tło w Pythonie, gdzie jest tak proste, jak sorted(planets, key=lambda n: n.Axis)- czy jest coś podobnego prostego w Go?)

Martin Thoma
źródło
Tutaj kolejny pakiet strony trzeciej github.com/patrickmn/sortutil . Może wykonywać normalne sortowanie, a także sortowanie zagnieżdżone. Tutaj cytuję z dokumentacji o wydajności: "Chociaż sortutil jest wygodny, nie przebije sortowania dedykowanego. Interfejs pod względem wydajności. Implementacja sortowania. Interfejs dla typu ByName, który osadza np. [] MyStruct i robi sort.Sort (ByName {MySlice}) należy wziąć pod uwagę, gdy wymagana jest wysoka wydajność. "
Tutompita

Odpowiedzi:

63

AKTUALIZACJA: Ta odpowiedź dotyczy starszych wersji go. Dla Go 1.8 i nowszych, zobacz odpowiedź AndreKR poniżej .


Jeśli chcesz czegoś mniej szczegółowego niż standardowy sortpakiet biblioteki , możesz użyć github.com/bradfitz/slicepakietu innej firmy . Wykorzystuje kilka sztuczek do generowania metod Leni Swappotrzebnych do sortowania wycinka, więc wystarczy podać Lessmetodę.

Za pomocą tego pakietu możesz przeprowadzić sortowanie za pomocą:

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

planets[:]Część jest niezbędna do wytworzenia kawałek obejmującą swoją tablicę. Jeśli utworzysz planetsplasterek zamiast tablicy, możesz pominąć tę część.

James Henstridge
źródło
28
Do sortowania tablicy muszę użyć pakietu innej firmy (chyba że chcę napisać niesamowitą ilość pełnego kodu)? Co jest nie tak z tym językiem? To znaczy ... To po prostu rodzaj! Żadnej czarnej magii.
Jendas
8
@jendas Go ma być proste, a nie łatwe. Ruby jest łatwy. Nawet jeśli nie wiesz dokładnie, jak coś działa, możesz spróbować i zadziała zgodnie z oczekiwaniami. Ale nie waż się próbować zrozumieć specyfikacji języka i budować interpretera lub czytać kod railsów podczas nauki języka Ruby. Go jest proste. Po wycieczce radzimy przeczytać specyfikację językową - nawet początkujący mogą. Potrafią przeczytać najbardziej zaawansowany kod i go pobrać. Ponieważ to proste.
kik
4
@kik To nie ma sensu. Proste nie znaczy pozbawione cech. Sortowanie to jedna z najważniejszych i podstawowych, a jednocześnie prostych funkcji, jakie może mieć biblioteka. Golang ma standardową bibliotekę szablonów html, skrótów crc32, drukarek, skanerów itp. To sprawia, że ​​jest to NIE MNIEJ proste. Brak sortowania w standardowej bibliotece nie jest kwestią prostoty, jest to kwestia brakujących podstawowych funkcji, które wszystkie języki uznają za standard. Nawet C ma funkcję sortowania. Przestańcie być tak elitarnymi z Golangiem i zacznijcie myśleć, że Golang może po prostu się mylić w tej sprawie (jeśli rzeczywiście go nie miał).
Eksapsy
320

Od wersji Go 1.8 możesz teraz sortować plasterki za pomocą sort.Slice :

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

Zwykle nie ma powodów, by używać tablicy zamiast plasterka, ale w swoim przykładzie ty za pomocą tablicy, więc trzeba nałożyć go z plasterkiem (dodaj [:]) w celu uczynienia go pracy z sort.Slice:

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

Sortowanie zmienia tablicę, więc jeśli naprawdę chcesz, możesz nadal używać tablicy zamiast wycinka po sortowaniu.

AndreKR
źródło
sort.Slicejest trochę zaskakujące. lessFunkcja zajmuje tylko indeksy więc musi (w tym odpowiedzi) używać osobno przechwycony planetstablicę. Wydaje się, że nic nie wymusza, aby posortowany wycinek i lessfunkcja działały na tych samych danych. Aby to zadziałało, musisz wpisać planetstrzy razy (DRY).
Brent Bradburn
planets[:]jest kluczowe. Ale nie rozumiem dlaczego. Ale działa.
STAL
@STEEL Zwykle w pierwszej kolejności powinieneś używać plasterka zamiast tablicy. Wtedy nie potrzebujesz [:].
AndreKR
37

Począwszy od idź 1,8 @ AndreKR za odpowiedź jest lepszym rozwiązaniem.


Możesz zaimplementować typ kolekcji, który implementuje interfejs sortowania .

Oto przykład dwóch takich typów, które umożliwiają sortowanie według osi lub nazwy:

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)
}
jimt
źródło
To jest dokładnie pełne rozwiązanie, które połączyłem, prawda?
Martin Thoma
1
Połączyłeś to, kiedy to pisałem. Przepraszam. Ale używając tylko standardowych narzędzi, nie ma na to krótszego sposobu.
jimt
5

Możesz zamiast implementować Sort interfaceon, []Planetktóry zaimplementujesz w typie zawierającym kolekcję i zamknięcie, które wykona porównanie. Musisz podać implementację zamknięcia porównania dla każdej właściwości.

Uważam, że ta metoda jest lepsza niż implementacja typu Sort dla każdej właściwości struktury.

Ta odpowiedź jest prawie wyrwana z różnych dokumentów, więc nie mogę przypisać jej zbyt wiele uznania

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

Jak to nazwać.

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

Oto demo

robbmj
źródło
3

Oto inny sposób na zmniejszenie części płyty kotła. Zastrzeżenie, wykorzystuje odbicie i bezpieczeństwo typu strat.

Oto demo

Cała magia dzieje się w Propfunkcji. Pobiera właściwość struct do sortowania i kolejność, w jakiej chcesz sortować (rosnąco, malejąco) i zwraca funkcję, która wykona porównania.

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}
}
robbmj
źródło