Qual é a maneira mais curta de simplesmente classificar uma matriz de estruturas por nomes de campos (arbitrários)?

129

Eu só tive um problema em que eu tinha uma variedade de estruturas, por exemplo

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

Vamos dizer que você deseja classificá-lo por Axis. Como você faz isso?

(Nota: vi http://golang.org/pkg/sort/ e parece funcionar, mas tenho que adicionar cerca de 20 linhas apenas para uma classificação simples por uma chave muito simples. Tenho um fundo python onde está tão simples quanto sorted(planets, key=lambda n: n.Axis)- existe algo semelhante simples no Go?)

Martin Thoma
fonte
Aqui está outro pacote github.com/patrickmn/sortutil de terceiros . Pode fazer a classificação normal e também a classificação aninhada. Aqui cito a documentação sobre o desempenho: "Embora sortutil seja conveniente, ele não superará uma classificação dedicada. Interface em termos de desempenho. Implementando classificação. Interface para um tipo ByName que incorpora, por exemplo, [] MyStruct e sort sort. (ByName {MySlice}) deve ser considerado quando for necessário alto desempenho. "
Tutompita

Respostas:

63

UPDATE: Esta resposta está relacionada a versões anteriores do go. Para o Go 1.8 e mais recente, consulte a resposta do AndreKR abaixo .


Se você quiser algo um pouco menos detalhado que o sortpacote de biblioteca padrão , poderá usar o github.com/bradfitz/slicepacote de terceiros . Ele usa alguns truques para gerar os métodos Lene Swapnecessários para classificar sua fatia, portanto, você só precisa fornecer um Lessmétodo.

Com este pacote, você pode executar a classificação com:

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

A planets[:]peça é necessária para produzir uma fatia cobrindo sua matriz. Se você fizer planetsuma fatia em vez de uma matriz, poderá pular essa parte.

James Henstridge
fonte
28
Eu tenho que usar pacote de terceiros para classificar uma matriz (a menos que eu queira escrever uma quantidade incrível de código detalhado)? O que há de errado com esse idioma? Quero dizer ... É apenas uma espécie! Não há magia negra.
Jendas
8
O @jendas Go deve ser simples, não fácil. Ruby é fácil. Mesmo quando não souber exatamente como algo funciona, você pode tentar e funcionará conforme o esperado. Mas não se atreva a tentar entender as especificações da linguagem e criar um intérprete, ou ler o código dos trilhos enquanto aprende ruby. Ir é simples. Você é aconselhado, após o passeio, a ler as especificações do idioma - até os iniciantes podem. E eles podem ler o código mais avançado e obtê-lo. Porque é simples.
kik
4
@kik Isso não faz sentido. Simples não significa inexpressivo. A classificação é um dos recursos mais importantes e fundamentais, mas simples que uma biblioteca poderia ter. Golang tem uma biblioteca padrão para modelos html, hashes CRC32, impressoras, scanners, etc. Isso o torna MENOS simples. Não ter uma classificação em sua biblioteca padrão não é uma questão de simplicidade, é uma questão de recursos fundamentais ausentes que todos os idiomas consideram um padrão. Mesmo C tem uma função de classificação. Pare de ser tão elitista com Golang e comece a considerar que Golang pode estar errado nesse caso (se realmente não o tinha).
Eksapsy
317

A partir do Go 1.8, agora você pode usar o sort.Slice para classificar uma fatia:

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

Normalmente, não há motivo para usar uma matriz em vez de uma fatia, mas, no seu exemplo, você está usando uma matriz, portanto, é necessário sobrepor-la com uma fatia (adicionar [:]) para fazê-la funcionar sort.Slice:

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

A classificação altera a matriz, portanto, se você realmente deseja, pode continuar usando a matriz em vez da fatia após a classificação.

AndreKR
fonte
sort.Sliceé meio que surpreendente. A lessfunção usa apenas índices e, portanto, precisa (nesta resposta) usar uma planetsmatriz capturada separadamente . Parece não haver nada que imponha que a fatia classificada e a lessfunção estejam operando nos mesmos dados. Para que isso funcione, você deve digitar planetstrês vezes (DRY).
Brent Bradburn 7/10/19
planets[:]é crucial. Mas eu não entendo o porquê. Funciona embora.
STEEL
@ STEEL Normalmente, você deve usar uma fatia em vez de uma matriz. Então você não precisa [:].
AndreKR 3/01
37

A partir do Go 1.8, a resposta da @ AndreKR é a melhor solução.


Você pode implementar um tipo de coleção que implementa a interface de classificação .

Aqui está um exemplo de dois tipos que permitem classificar por Eixo ou Nome:

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
fonte
Esta é exatamente a solução detalhada que vinculei, não é?
Martin Thoma
1
Você o vinculou enquanto eu escrevia isso. Me desculpe. Mas, usando apenas as ferramentas padrão, não há uma maneira mais curta de fazer isso.
jimt
5

Você pode, em vez de implementar o Sort interfaceem que []Planetvocê implementa, em um tipo que contém a coleção e um fechamento que fará a comparação. Você deve fornecer a implementação para o fechamento de comparação para cada propriedade.

Este método é melhor do que implementar um tipo de classificação para cada propriedade da estrutura.

Essa resposta é quase extraída dos documentos de classificação, por isso não posso dar muito crédito a ela

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

Como chamá-lo.

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

Aqui está uma demonstração

robbmj
fonte
3

Aqui está outra maneira de reduzir parte da placa da caldeira. Isenção de responsabilidade, utiliza reflexão e perdas do tipo segurança.

Aqui está uma demonstração

Toda a mágica acontece na Propfunção. É necessária a propriedade struct para classificar e a ordem em que você deseja classificar (ascendente, descendente) e retorna uma função que realizará as comparações.

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
fonte