¿Cuál es la forma más corta de simplemente ordenar una matriz de estructuras por nombres de campo (arbitrarios)?

129

Acabo de tener un problema en el que tenía una variedad de estructuras, por ejemplo

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

Digamos que quieres ordenarlo Axis. ¿Cómo haces eso?

(Nota: he visto http://golang.org/pkg/sort/ y parece funcionar, pero tengo que agregar unas 20 líneas solo para ordenarlas por una clave muy simple. Tengo un fondo de Python donde está tan simple como sorted(planets, key=lambda n: n.Axis): ¿hay algo similar simple en Go?)

Martin Thoma
fuente
Aquí otro paquete de github.com/patrickmn/sortutil de terceros . Puede hacer una ordenación normal y también una ordenación anidada. Aquí cito de la documentación sobre el rendimiento: "Si bien sortutil es conveniente, no superará a un tipo dedicado. Interfaz en términos de rendimiento. Implementación de tipo. Interfaz para un tipo ByName que incrusta, por ejemplo, [] MyStruct y sort.ort (ByName {MySlice}) debe considerarse cuando se requiere un alto rendimiento ".
Tutompita

Respuestas:

63

ACTUALIZACIÓN: Esta respuesta se refiere a versiones anteriores de go. Para Go 1.8 y versiones posteriores, consulte la respuesta de AndreKR a continuación .


Si desea algo un poco menos detallado que el sortpaquete de biblioteca estándar , puede usar el github.com/bradfitz/slicepaquete de terceros . Utiliza algunos trucos para generar los métodos Leny Swapnecesarios para clasificar su porción, por lo que solo necesita proporcionar un Lessmétodo.

Con este paquete, puede realizar el ordenamiento con:

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

La planets[:]parte es necesaria para producir un corte que cubra su matriz. Si hace planetsun corte en lugar de una matriz, puede omitir esa parte.

James Henstridge
fuente
28
¿Tengo que usar un paquete de terceros para ordenar una matriz (a menos que quiera escribir una cantidad increíble de código detallado)? ¿Qué le pasa a este idioma? Quiero decir ... ¡Es solo una especie! No hay magia negra.
Jendas
8
@jendas Go está destinado a ser simple, no fácil. Ruby es fácil. Incluso cuando no sepa exactamente cómo funciona algo, puede intentarlo y funcionará como se espera. Pero no se atreva a tratar de comprender las especificaciones del lenguaje y construir un intérprete, o leer el código de rails mientras aprende ruby. Ir es simple. Se recomienda, después del recorrido, leer las especificaciones del idioma, incluso los principiantes pueden. Y pueden leer el código más avanzado y obtenerlo. Porque es simple
kik
44
@kik Eso no tiene sentido. Simple no significa sin características. Sort es una de las características más importantes y fundamentales, pero simples , que podría tener una biblioteca. Golang tiene una biblioteca estándar para plantillas html, hash crc32, impresoras, escáneres, etc. Eso NO hace que sea MENOS simple. No tener que ordenar en su biblioteca estándar no es una cuestión de simplicidad, es una cuestión de características fundamentales que faltan que todos los idiomas consideran un estándar. Incluso C tiene una función de clasificación. Deja de ser tan elitista con Golang y comienza a considerar que Golang podría estar equivocado en este caso (si es que no lo tenía).
Eksapsy
318

A partir de Go 1.8 ahora puede usar sort.Slice para ordenar un segmento:

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

Normalmente no hay razón para usar una matriz en lugar de una división, pero en su ejemplo está utilizando una matriz, por lo que debe superponerla con una división (agregar [:]) para que funcione con sort.Slice:

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

La clasificación cambia la matriz, por lo que si realmente lo desea, puede continuar utilizando la matriz en lugar del segmento después de la clasificación.

AndreKR
fuente
sort.SliceEs un poco sorprendente. La lessfunción solo toma índices, por lo que debe (en esta respuesta) usar una planetsmatriz capturada por separado . Parece que no hay nada que obligue a que el segmento ordenado y la lessfunción estén operando en los mismos datos. Para que esto funcione, debe escribir planetstres veces (DRY).
Brent Bradburn
planets[:]Es crucial. Pero no entiendo por qué. Funciona sin embargo.
STEEL
@STEEL Por lo general, en primer lugar, debería usar un segmento en lugar de una matriz. Entonces no lo necesitas [:].
AndreKR
37

A partir de Go 1.8, la respuesta de @ AndreKR es la mejor solución.


Puede implementar un tipo de colección que implementa la interfaz de clasificación .

Aquí hay un ejemplo de dos de estos tipos que le permiten ordenar por Eje o Nombre:

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
fuente
Esta es exactamente la solución detallada que he vinculado, ¿no?
Martin Thoma
1
Lo vinculaste mientras escribía esto. Mis disculpas. Pero usando solo las herramientas estándar, no hay una forma más corta de hacerlo.
jimt
5

Puede, en lugar de aplicar el Sort interfacesobre []Planetque poner en práctica en un tipo que contiene la colección y un cierre que va a hacer la comparación. Debe proporcionar la implementación para el cierre de comparación de cada propiedad.

Creo que este método es mejor que implementar un tipo Ordenar para cada propiedad de la estructura.

Esta respuesta está casi arrancada de los documentos de clasificación, por lo que no puedo dar mucho crédito por ello

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

Cómo llamarlo

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

Aquí hay una demostración

robbmj
fuente
3

Aquí hay otra forma de reducir parte de la placa de la caldera. Descargo de responsabilidad, utiliza reflexiones y pérdidas tipo seguridad.

Aquí hay una demostración

Toda la magia sucede en la Propfunción. Toma la propiedad de estructura para ordenar y el orden que desea ordenar (ascendente, descendente) y devuelve una función que realizará las comparaciones.

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
fuente