Каков самый короткий способ просто отсортировать массив структур по (произвольным) именам полей?

130

У меня просто была проблема, когда у меня был массив структур, например

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

Допустим, вы хотите отсортировать его по Axis. Как ты это делаешь?

(Примечание: я видел http://golang.org/pkg/sort/, и, похоже, он работает, но мне нужно добавить около 20 строк только для простой сортировки с помощью очень простого ключа. У меня есть фон Python, где он просто как sorted(planets, key=lambda n: n.Axis)- есть ли что-то подобное в Go?)

Мартин Тома
источник
Вот еще один сторонний пакет github.com/patrickmn/sortutil . Он может выполнять обычную сортировку, а также вложенную сортировку. Здесь я цитирую документацию о производительности: «Хотя sortutil удобен, он не превосходит выделенную сортировку. Интерфейс с точки зрения производительности. Реализация сортировки. Интерфейс для типа ByName, который включает, например, [] MyStruct и выполнение sort.Sort. (ByName {MySlice}) следует учитывать, когда требуется высокая производительность ".
Тутомпита

Ответы:

63

ОБНОВЛЕНИЕ: этот ответ относится к более старым версиям go. Для Go 1.8 и новее см . Ответ AndreKR ниже .


Если вам нужно что-то менее подробное, чем стандартный sortпакет библиотеки , вы можете использовать сторонний github.com/bradfitz/sliceпакет. Она использует некоторые приемы для создания Lenи Swapметод , необходимых для сортировки своего куска, так что вам нужно только предоставить Lessметод.

С помощью этого пакета вы можете выполнить сортировку с помощью:

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

planets[:]Часть необходимо произвести срез , охватывающий ваш массив. Если вы сделаете planetsсрез вместо массива, вы можете пропустить эту часть.

Джеймс Хенстридж
источник
28
Мне нужно использовать сторонний пакет для сортировки массива (если я не хочу писать невероятное количество подробного кода)? Что не так с этим языком? То есть ... Это просто вроде! Никакой черной магии.
Jendas
8
@jendas Go должен быть простым, а не легким. Руби - это просто. Даже если вы точно не знаете, как что-то работает, вы можете попробовать, и все будет работать должным образом. Но не смейте пытаться понять спецификации языка и построить интерпретатор или читать код rails во время изучения ruby. Идти просто. После экскурсии вам рекомендуется прочитать спецификацию языка - даже новички могут. И они могут прочитать самый продвинутый код и получить его. Потому что это просто.
kik
4
@kik В этом нет никакого смысла. Простой не означает безликий. Сортировка - одна из самых важных и фундаментальных, но простых функций, которые может иметь библиотека. У Golang есть стандартная библиотека для html-шаблонов, хэшей crc32, принтеров, сканеров и т. Д. Это делает его НЕ МЕНЕЕ простой. Отсутствие сортировки в вашей стандартной библиотеке - это не вопрос простоты, это вопрос отсутствия основных функций, которые все языки считают стандартом. Даже C имеет функцию сортировки. Перестаньте быть такими элитарными с Голангом и начните считать, что Голанг мог просто ошибаться в этом (если у него действительно его не было).
Eksapsy
320

Начиная с Go 1.8 теперь вы можете использовать sort.Slice для сортировки фрагментов:

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

Там обычно нет оснований использовать массив вместо кусочка, но в вашем примере вы которые с помощью массива, так что вы должны наложить его с кусочком (добавить [:]) , чтобы заставить его работать с sort.Slice:

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

Сортировка изменяет массив, поэтому, если вы действительно хотите, вы можете продолжать использовать массив вместо среза после сортировки.

AndreKR
источник
sort.Sliceэто немного удивительно. lessФункция принимает только индексы поэтому он должен (в этом ответе) использовать отдельно захваченный planetsмассив. Кажется, нет ничего, что заставляет отсортированный фрагмент и lessфункция работают с одними и теми же данными. Чтобы это сработало, вам нужно набрать planetsтри раза (СУХОЙ).
Брент
planets[:]является решающим. Но я не понимаю почему. Хотя работает.
STEEL
@STEEL Обычно вы должны в первую очередь использовать срез вместо массива. Тогда тебе не нужно [:].
AndreKR
37

Что касается Go 1.8, ответ @AndreKR - лучшее решение.


Вы можете реализовать тип коллекции, который реализует интерфейс сортировки .

Вот пример двух таких типов, которые позволяют сортировать по оси или имени:

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
источник
Это как раз то подробное решение, которое я связал, не так ли?
Мартин Тома,
1
Вы связали его, пока я писал это. Мои извинения. Но с использованием только стандартных инструментов нет более короткого способа сделать это.
jimt
5

Вместо реализации Sort interfaceon []Planetвы можете реализовать тип, содержащий коллекцию, и замыкание, которое будет выполнять сравнение. Вы должны предоставить реализацию для закрытия сравнения для каждого свойства.

Я считаю, что этот метод лучше, чем реализация типа сортировки для каждого свойства структуры.

Этот ответ почти вырван прямо из документации по сортировке, поэтому я не могу поверить в это

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

Как это назвать.

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

Вот демо

robbmj
источник
3

Вот еще один способ уменьшить часть котельной плиты. Заявление об отказе от ответственности, он использует безопасность типа отражения и потерь.

Вот демо

Вся магия происходит в Propфункции. Он принимает свойство структуры для сортировки и порядок, который вы хотите отсортировать (по возрастанию, по убыванию), и возвращает функцию, которая будет выполнять сравнения.

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
источник