Quel est le moyen le plus court de trier simplement un tableau de structures par des noms de champs (arbitraires)?

129

J'ai juste eu un problème où j'avais un tableau de structures, par exemple

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

Disons que vous souhaitez le trier Axis. Comment tu fais ça?

(Remarque: j'ai vu http://golang.org/pkg/sort/ et cela semble fonctionner, mais je dois ajouter environ 20 lignes juste pour un tri simple par une clé très simple. J'ai un arrière-plan python là où il est aussi simple que sorted(planets, key=lambda n: n.Axis)- y a-t-il quelque chose de similaire simple dans Go?)

Martin Thoma
la source
Voici un autre package tiers github.com/patrickmn/sortutil . Il peut effectuer un tri normal et un tri imbriqué. Ici, je cite la documentation sur les performances: "Bien que sortutil soit pratique, il ne battra pas un tri dédié. Interface en termes de performances. Implémentation de tri. Interface pour un type ByName qui incorpore par exemple [] MyStruct et faisant tri.Sort (ByName {MySlice}) doit être pris en compte lorsque des performances élevées sont requises. "
Tutompita

Réponses:

63

MISE À JOUR: Cette réponse concerne les anciennes versions de go. Pour Go 1.8 et plus récent, consultez la réponse d'AndreKR ci-dessous .


Si vous voulez quelque chose d'un peu moins verbeux que le sortpackage de bibliothèque standard , vous pouvez utiliser le github.com/bradfitz/slicepackage tiers . Il utilise quelques astuces pour générer les méthodes Lenet Swapnécessaires pour trier votre tranche, il vous suffit donc de fournir une Lessméthode.

Avec ce package, vous pouvez effectuer le tri avec:

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

La planets[:]pièce est nécessaire pour produire une tranche couvrant votre tableau. Si vous créez planetsune tranche au lieu d'un tableau, vous pouvez ignorer cette partie.

James Henstridge
la source
28
Je dois utiliser un package tiers pour trier un tableau (sauf si je veux écrire une quantité incroyable de code verbeux)? Quel est le problème avec cette langue? Je veux dire ... C'est juste en quelque sorte! Pas de magie noire.
Jendas
8
@jendas Go se veut simple, pas facile. Ruby est facile. Même si vous ne savez pas exactement comment quelque chose fonctionne, vous pouvez essayer et cela fonctionnera comme prévu. Mais n'osez pas essayer de comprendre les spécifications du langage et construire un interpréteur, ou lire le code de rails tout en apprenant ruby. Aller est simple. Il est conseillé, après la visite, de lire les spécifications linguistiques - même les débutants le peuvent. Et ils peuvent lire le code le plus avancé et l'obtenir. Parce que c'est simple.
kik
4
@kik Cela n'a aucun sens. Simple ne veut pas dire sans traits. Trier est l' un des plus important et fondamental, mais simple , dispose d' une bibliothèque pourrait avoir. Golang a une bibliothèque standard pour les modèles html, les hachages crc32, les imprimantes, les scanners, etc. Cela ne le rend PAS MOINS simple. Ne pas avoir de tri dans votre bibliothèque standard n'est pas une question de simplicité, c'est une question de fonctionnalités fondamentales manquantes que tous les langages considèrent comme un standard. Même C a une fonction de tri. Arrêtez d'être si élitiste avec Golang et commencez à considérer que Golang s'est peut-être juste trompé sur celui-ci (s'il ne l'avait pas vraiment).
Eksapsy
319

Depuis Go 1.8, vous pouvez désormais utiliser sort.Slice pour trier une tranche:

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

Il n'y a normalement aucune raison d'utiliser un tableau au lieu d'une tranche, mais dans votre exemple , vous sont en utilisant un tableau, vous devez superposer avec une tranche (ajouter [:]) pour le faire fonctionner avec sort.Slice:

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

Le tri modifie le tableau, donc si vous le souhaitez vraiment, vous pouvez continuer à utiliser le tableau au lieu de la tranche après le tri.

AndreKR
la source
sort.Sliceest assez surprenant. La lessfonction ne prend que des indices et doit donc (dans cette réponse) utiliser un planetstableau capturé séparément . Il semble que rien n'impose que la tranche triée et la lessfonction fonctionnent sur les mêmes données. Pour que cela fonctionne, vous devez taper planetstrois fois (DRY).
Brent Bradburn
planets[:]est crucial. Mais je ne comprends pas pourquoi. Fonctionne cependant.
STEEL
@STEEL En général, vous devriez utiliser une tranche au lieu d'un tableau en premier lieu. Alors vous n'en avez pas besoin [:].
AndreKR
37

Depuis Go 1.8, la réponse de @ AndreKR est la meilleure solution.


Vous pouvez implémenter un type de collection qui implémente l' interface de tri .

Voici un exemple de deux types de ce type qui vous permettent de trier par axe ou par nom:

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
la source
C'est exactement la solution détaillée que j'ai liée, n'est-ce pas?
Martin Thoma
1
Vous l'avez lié pendant que j'écrivais ceci. Mes excuses. Mais en utilisant uniquement les outils standard, il n'y a pas de moyen plus court de le faire.
jimt
5

Vous pouvez, au lieu d'implémenter le Sort interfaceon, []Planetvous implémentez sur un type qui contient la collection et une fermeture qui fera la comparaison. Vous devez fournir l'implémentation de la clôture de comparaison pour chaque propriété.

Cette méthode est à mon avis meilleure que l'implémentation d'un type Sort pour chaque propriété de la structure.

Cette réponse est presque extraite directement des documents de tri, donc je ne peux pas m'en attribuer beaucoup de mérite

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

Comment l'appeler.

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

Voici une démo

robbmj
la source
3

Voici une autre façon de réduire une partie de la plaque de la chaudière. Clause de non-responsabilité, il utilise la sécurité de type réflexion et pertes.

Voici une démo

Toute la magie se produit dans la Propfonction. Il prend la propriété struct pour trier et l'ordre dans lequel vous voulez trier (croissant, décroissant) et retourne une fonction qui effectuera les comparaisons.

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
la source