Was ist der kürzeste Weg, um ein Array von Strukturen einfach nach (beliebigen) Feldnamen zu sortieren?

128

Ich hatte gerade ein Problem, bei dem ich eine Reihe von Strukturen hatte, z

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

Nehmen wir an, Sie möchten es sortieren Axis. Wie machst du das?

(Hinweis: Ich habe http://golang.org/pkg/sort/ gesehen und es scheint zu funktionieren, aber ich muss ungefähr 20 Zeilen hinzufügen, nur um sie einfach nach einem sehr einfachen Schlüssel zu sortieren. Ich habe dort einen Python-Hintergrund so einfach wie sorted(planets, key=lambda n: n.Axis)- gibt es etwas ähnlich Einfaches in Go?)

Martin Thoma
quelle
Hier ein weiteres Paket von Drittanbietern github.com/patrickmn/sortutil . Es kann normal sortiert und auch verschachtelt sortiert werden. Hier zitiere ich aus der Dokumentation zur Leistung: "Sortutil ist zwar praktisch, schlägt aber keine dedizierte Sortierung. Schnittstelle in Bezug auf Leistung. Implementierung von sort. Schnittstelle für einen Typ ByName, der z. B. [] MyStruct einbettet und sort.Sort ausführt (ByName {MySlice}) sollte berücksichtigt werden, wenn eine hohe Leistung erforderlich ist. "
Tutompita

Antworten:

62

UPDATE: Diese Antwort bezieht sich auf ältere Versionen von go. Informationen zu Go 1.8 und neuer finden Sie in der Antwort des AndreKR unten .


Wenn Sie etwas weniger Ausführliches als das Standardbibliothekspaket wünschen sort, können Sie das github.com/bradfitz/slicePaket eines Drittanbieters verwenden . Es werden einige Tricks verwendet, um die Lenund SwapMethoden zu generieren, die zum Sortieren Ihres Slice erforderlich sind. Sie müssen also nur eine LessMethode angeben.

Mit diesem Paket können Sie die Sortierung durchführen mit:

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

Das planets[:]Teil ist erforderlich, um ein Slice zu erstellen, das Ihr Array abdeckt. Wenn Sie planetsanstelle eines Arrays ein Slice erstellen, können Sie diesen Teil überspringen.

James Henstridge
quelle
28
Ich muss ein Drittanbieterpaket verwenden, um ein Array zu sortieren (es sei denn, ich möchte unglaublich viel ausführlichen Code schreiben). Was ist los mit dieser Sprache? Ich meine ... Es ist nur eine Art! Keine schwarze Magie.
Jendas
8
@jendas Go soll einfach sein, nicht einfach. Ruby ist einfach. Selbst wenn Sie nicht genau wissen, wie etwas funktioniert, können Sie es versuchen und es funktioniert wie erwartet. Aber trauen Sie sich nicht, die Spezifikationen der Sprache zu verstehen und einen Dolmetscher zu bauen oder den Code von Rails zu lesen, während Sie Ruby lernen. Go ist einfach. Nach der Tour wird empfohlen, die Sprachspezifikation zu lesen - auch Anfänger können. Und sie können den fortschrittlichsten Code lesen und erhalten. Weil es einfach ist.
Kik
4
@kik Das macht keinen Sinn. Einfach heißt nicht merkwürdig. Sortieren ist eine der wichtigsten und grundlegendsten, aber einfache Funktionen könnte eine Bibliothek. Golang hat eine Standardbibliothek für HTML-Vorlagen, crc32-Hashes, Drucker, Scanner usw. Das macht es NICHT WENIGER einfach. Keine Sortierung in Ihrer Standardbibliothek ist keine Frage der Einfachheit, sondern der fehlenden grundlegenden Funktionen, die alle Sprachen als Standard betrachten. Auch C hat eine Sortierfunktion. Hören Sie auf, mit Golang so elitär zu sein, und denken Sie darüber nach, dass Golang in dieser Sache möglicherweise falsch lag (wenn es sie tatsächlich nicht gab).
Eksapsy
317

Ab Go 1.8 können Sie jetzt sort.Slice verwenden , um ein Slice zu sortieren:

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

Normalerweise gibt es keinen Grund, ein Array anstelle eines Slice zu verwenden. In Ihrem Beispiel verwenden Sie jedoch ein Array. Sie müssen es daher mit einem Slice (Add [:]) überlagern , damit es funktioniert sort.Slice:

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

Die Sortierung ändert das Array. Wenn Sie also wirklich möchten, können Sie das Array nach dem Sortieren anstelle des Slice weiter verwenden.

AndreKR
quelle
sort.Sliceist irgendwie überraschend. Die lessFunktion akzeptiert nur Indizes und muss (in dieser Antwort) ein separat erfasstes planetsArray verwenden. Es scheint nichts zu geben, was erzwingt, dass das sortierte Slice und die lessFunktion mit denselben Daten arbeiten. Damit dies funktioniert, müssen Sie planetsdreimal (DRY) eingeben.
Brent Bradburn
planets[:]ist entscheidend. Aber ich verstehe nicht warum. Funktioniert aber.
STAHL
@STEEL Normalerweise sollten Sie zunächst ein Slice anstelle eines Arrays verwenden. Dann brauchst du nicht [:].
AndreKR
37

Ab Go 1.8 ist die Antwort von @ AndreKR die bessere Lösung.


Sie können einen Sammlungstyp implementieren, der die Sortierschnittstelle implementiert .

Hier ist ein Beispiel für zwei solche Typen, mit denen Sie entweder nach Achse oder nach Name sortieren können:

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
quelle
Dies ist genau die ausführliche Lösung, die ich verlinkt habe, nicht wahr?
Martin Thoma
1
Du hast es verlinkt, während ich das geschrieben habe. Entschuldigen Sie. Es gibt jedoch keinen kürzeren Weg, dies nur mit den Standardwerkzeugen zu tun.
Jimt
5

Sie können das Sort interfaceOn, das []PlanetSie implementieren, für einen Typ implementieren, der die Auflistung und einen Abschluss enthält, der den Vergleich durchführt. Sie müssen die Implementierung für den Vergleichsabschluss für jede Eigenschaft bereitstellen.

Diese Methode ist meiner Meinung nach besser als die Implementierung eines Sortiertyps für jede Eigenschaft der Struktur.

Diese Antwort ist fast direkt aus den Sortierdokumenten herausgerissen, so dass ich nicht viel Anerkennung dafür finden kann

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

Wie man es nennt.

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

Hier ist eine Demo

robbmj
quelle
3

Hier ist eine andere Möglichkeit, einen Teil der Kesselplatte zu reduzieren. Haftungsausschluss, es verwendet Reflexion und Verluste Typ Sicherheit.

Hier ist eine Demo

Die ganze Magie geschieht in der PropFunktion. Es nimmt die struct-Eigenschaft zum Sortieren und die Reihenfolge, nach der Sie sortieren möchten (aufsteigend, absteigend), und gibt eine Funktion zurück, die die Vergleiche ausführt.

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
quelle