Apa cara terpendek untuk mengurutkan array struct berdasarkan nama bidang (sewenang-wenang)?

129

Saya baru saja mengalami masalah di mana saya memiliki array struct, misalnya

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

Katakanlah Anda ingin mengurutkannya Axis. Bagaimana kamu melakukannya?

(Catatan: Saya telah melihat http://golang.org/pkg/sort/ dan tampaknya berhasil, tetapi saya harus menambahkan sekitar 20 baris hanya untuk penyortiran sederhana dengan kunci yang sangat sederhana. Saya memiliki latar belakang python di mana itu sesederhana sorted(planets, key=lambda n: n.Axis)- apakah ada hal serupa yang sederhana di Go?)

Martin Thoma
sumber
Di sini paket github.com/patrickmn/sortutil pihak ketiga lainnya . Itu dapat melakukan pengurutan normal dan juga pengurutan bersarang. Di sini saya mengutip dari dokumentasi tentang kinerja: "Meskipun sortutil nyaman, itu tidak akan mengalahkan jenis khusus. Antarmuka dalam hal kinerja. Menerapkan sort. Antarmuka untuk jenis ByName yang menyematkan misalnya [] MyStruct dan melakukan sort.Sort (ByName {MySlice}) harus dipertimbangkan saat dibutuhkan kinerja tinggi. "
Tutompita

Jawaban:

63

PEMBARUAN: Jawaban ini berkaitan dengan versi yang lebih lama dari go. Untuk Go 1.8 dan yang lebih baru, lihat jawaban AndreKR di bawah .


Jika Anda menginginkan sesuatu yang sedikit lebih bertele-tele daripada sortpaket perpustakaan standar , Anda dapat menggunakan github.com/bradfitz/slicepaket pihak ketiga . Ini menggunakan beberapa trik untuk menghasilkan Lendan Swapmetode yang diperlukan untuk mengurutkan potongan Anda, jadi Anda hanya perlu menyediakan Lessmetode.

Dengan paket ini, Anda dapat melakukan pengurutan dengan:

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

Bagian planets[:]tersebut diperlukan untuk menghasilkan irisan yang menutupi array Anda. Jika Anda membuat planetsirisan alih-alih larik, Anda dapat melewati bagian itu.

James Henstridge
sumber
28
Saya harus menggunakan paket pihak ketiga untuk mengurutkan array (kecuali saya ingin menulis kode verbose dalam jumlah yang luar biasa)? Ada apa dengan bahasa ini? Maksudku ... Ini hanya semacam! Tidak ada ilmu hitam.
Jendas
8
@jendas Go dimaksudkan untuk menjadi sederhana, tidak mudah. Ruby itu mudah. Bahkan ketika tidak tahu persis bagaimana sesuatu bekerja, Anda dapat mencoba dan itu akan bekerja seperti yang diharapkan. Tapi jangan berani-berani mencoba memahami spesifikasi bahasa dan membangun penerjemah, atau membaca kode rel sambil mempelajari ruby. Pergi itu sederhana. Anda disarankan, setelah tur, untuk membaca spesifikasi bahasa - bahkan pemula pun bisa. Dan mereka dapat membaca kode paling canggih dan mendapatkannya. Karena itu sederhana.
kik
4
@kik Itu tidak masuk akal. Sederhana bukan berarti tidak berciri. Sortir adalah salah satu fitur yang paling penting dan mendasar, namun sederhana yang dapat dimiliki perpustakaan. Golang memiliki pustaka standar untuk template html, hash crc32, printer, pemindai, dll. Itu membuatnya TIDAK KURANG sederhana. Tidak memiliki penyortiran di perpustakaan standar Anda bukanlah masalah kesederhanaan, adalah masalah fitur dasar yang hilang yang dianggap semua bahasa sebagai standar. Bahkan C memiliki fungsi Penyortiran. Berhentilah bersikap elitis dengan Golang dan mulailah mempertimbangkan bahwa Golang mungkin saja salah dalam hal ini (jika memang tidak memilikinya).
Eksapsy
319

Mulai Go 1.8, Anda sekarang dapat menggunakan sort.Slice untuk mengurutkan slice:

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

Biasanya tidak ada alasan untuk menggunakan array daripada sepotong, tetapi dalam contoh Anda sedang menggunakan sebuah array, sehingga Anda harus menyalutnya dengan sepotong (add [:]) untuk membuatnya bekerja dengan sort.Slice:

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

Pengurutan mengubah larik, jadi jika Anda benar-benar ingin, Anda dapat terus menggunakan larik alih-alih potongan setelah pengurutan.

AndreKR
sumber
sort.Sliceagak mengejutkan. The lessFungsi hanya membutuhkan indeks sehingga harus (dalam jawaban ini) menggunakan secara terpisah-ditangkap planetslarik. Tampaknya tidak ada yang memaksa bahwa slice yang diurutkan dan lessfungsinya beroperasi pada data yang sama. Agar ini berfungsi, Anda harus mengetik planetstiga kali (KERING).
Brent Bradburn
planets[:]sangat penting. Tapi saya tidak mengerti kenapa. Bekerja meskipun.
BAJA
@STEEL Biasanya Anda harus menggunakan slice daripada array di tempat pertama. Maka Anda tidak perlu [:].
AndreKR
37

Mulai Go 1.8, jawaban @ AndreKR adalah solusi yang lebih baik.


Anda bisa mengimplementasikan tipe koleksi yang mengimplementasikan antarmuka sort .

Berikut adalah contoh dari dua tipe yang memungkinkan Anda untuk mengurutkan berdasarkan Axis atau Name:

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
sumber
Ini persis solusi verbose yang saya tautkan, bukan?
Martin Thoma
1
Anda menautkannya saat saya menulis ini. Permintaan maaf saya. Tetapi hanya dengan menggunakan alat standar, tidak ada cara yang lebih singkat untuk melakukan ini.
jimt
5

Anda bisa, daripada mengimplementasikan Sort interfaceon yang []PlanetAnda implementasikan pada tipe yang berisi collection dan closure yang akan melakukan perbandingan. Anda harus menyediakan implementasi untuk penutupan perbandingan untuk setiap properti.

Metode ini saya rasa lebih baik daripada menerapkan tipe Sortir untuk setiap properti struct.

Jawaban ini hampir robek langsung dari dokumen sortir jadi saya tidak bisa terlalu menghargai itu

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

Bagaimana menyebutnya.

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

Ini Demo

robbmj
sumber
3

Berikut cara lain untuk mengurangi sebagian pelat boiler. Penafian, ini menggunakan keamanan tipe refleksi dan kerugian.

Ini Demo

Semua keajaiban terjadi dalam Propfungsinya. Dibutuhkan properti struct untuk mengurutkan dan mengurutkan yang ingin Anda urutkan (naik, turun) dan mengembalikan fungsi yang akan melakukan perbandingan.

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
sumber