何をしようとしているのか
struct(以降、構造体と表記)
を要素に持つ、配列をソートする必要がある場面に出くわした。通常というか一般的な数値や文字列のソートと異なり、構造体のAフィールドの値が大きい順番かつ、Bフィールドの値が小さい順かつ...のようなソートを行う必要があるため、結構メンドくさい
一応、調べているとgolang
にはパッケージ(組み込みではない)としてsort
というものが用意されている。なお、sort
の詳しい話は今回扱わないし、すでに素晴らしい情報が数多く公開されているのでそちらを参照して頂きたい。(参考文献にまとめています)
そんな中、こちらのブログで実装されている、構造体の複数fieldでのソート実装を見て、「なるほど、こんな感じでやればええんやな」と思い、ほとんどコピペで類似の処理を実装したものの、ソートする条件を関数化出来ない(ソートしたい各所でソート条件を持つ関数群を定義しなければならない)煩わしさに何とかならないかと思い試行錯誤した
以下は引用のコードになります。非常に参考になりましたm( )m
func main(){ // lessfuncを実装 byBast := func(p1, p2 *profile)bool{ return p1.threesize[0]<p2.threesize[0] } byClass := func(p1, p2 *profile)bool{ return p1.class<p2.class } // 逆順も同じようにここで実装する byBastDescending := func(p1, p2 *profile)bool{ return p1.threesize[0]>p2.threesize[0] } // 誕生日 time.TimeもBefore/Afterで比較してboolを返せる byDate := func(p1, p2 *profile)bool{ return p1.data.Before(p2.data) } fmt.Println("学年昇順でおっぱい降順") sort.Sort(idleSorter{idle: aqours, lessfunc: []lessFunc{byClass, byBastDescending}}) for i, v := range aqours{ fmt.Printf("[%d] name: %-7s class: %d bast: %d\n", i+1, v.name, v.class, v.threesize[0]) } }
完成物としては条件となる文字列なりを渡してsplitし、lessFuncの配列に該当するソート条件を持つ無名関数を返すという処理の実装になる
出来たやつ
このブログ投稿を仮定した構造体を対象にソートを実行する無名関数を作成した
type Post struct { // データの作成者 Name string // 投稿タイトル Title string // 投稿本文 Body string // 作成日 CreatedAt time.Time }
ここまでは参考文献の実装と全く同じで、対象の構造体を変えただけ
// for sort type lessFunc func(i, j *Post) bool type PostSorter struct { Post []*Post lessFunc []lessFunc } func (is PostSorter) Len() int { return len(is.Post) } func (is PostSorter) Swap(i, j int) { is.Post[i], is.Post[j] = is.Post[j], is.Post[i] } func (is PostSorter) Less(i, j int) bool { k := 0 p, q := is.Post[i], is.Post[j] for k = 0; k < len(is.lessFunc)-1; k++ { less := is.lessFunc[k] switch { case less(p, q): return true case less(q, p): return false } } return is.lessFunc[k](p, q) }
ここからがメイン。この関数で受け取った文字列からヒットする対象のソートを行うための無名関数を返す。なぜこのような形式になっているかというと、元々の実装がhttp経由でのGET
でquery string
を用いてsortすることを考えていたからだ
// 受け取ったstringからソートを行うための無名関数を返す func SortRelationConverter(query string) func(*Post, *Post) bool { switch query { // Nameの昇順 case "name": return func(p1, p2 *Post) bool { return p1.Name > p2.Name } // Nameの降順 case "-name": return func(p1, p2 *Post) bool { return p1.Name < p2.Name } // Titleの昇順 case "title": return func(p1, p2 *Post) bool { return p1.Title > p2.Title } // Titleの降順 case "-title": return func(p1, p2 *Post) bool { return p1.Title < p2.Title } // Bodyの昇順 case "body": return func(p1, p2 *Post) bool { return p1.Body > p2.Body } // Bodyの降順 case "-body": return func(p1, p2 *Post) bool { return p1.Body < p2.Body } // CreatedAtの昇順 case "created_at": return func(p1, p2 *Post) bool { return p1.CreatedAt > p2.CreatedAt } // CreatedAtの降順 case "-created_at": return func(p1, p2 *Post) bool { return p1.CreatedAt < p2.CreatedAt } } return nil }
最後にソート条件を渡しただけでソートの実行が出来るように上記の処理をラップする関数を用意。この関数にquery string
で受け取ったような形式で文字列を渡すだけでソートが実行される
// ソートを実行するためのラップ関数 // sortStr -> ソート条件(eg: name:-body:-created_at) // splitSymbol -> ソート条件で区切り記号に用いる記号(eg: ':') func PostSort(data []*Post, sortsStr, splitSymbol string) []*Post { // ソートを行うための無名関数を格納する配列 sortCond := make([]lessFunc, 0) // ソート条件を対象の記号をsplitして作成 sorts := strings.Split(sortsStr, splitSymbol) if len(sorts) > 0 { for _, sort := range sorts { // 条件から無名関数を作成して配列に格納 sortFunc := SortRelationConverter(sort) sortCond = append(sortCond, sortFunc) } // ソートのために構造体を作成 cond := PostSorter{ Post: data, lessFunc: sortCond, } // ソートを実行 sort.Sort(cond) } return data }
実行結果
func main(){ // 検証用のデータを作成 lst := []*Post{ &Post{ Name: "a", Title: "A-title", Body: "A-body", }, &Post{ Name: "b", Title: "B-title", Body: "B-body", }, &Post{ Name: "c", Title: "C-title", Body: "C-body", }, &Post{ Name: "d", Title: "D-title", Body: "D-body", }, &Post{ Name: "e", Title: "E-title", Body: "E-body", }, } // 結果を確認するための無名関数 debuger := func(lst []*Post) { fmt.Println("[debug] result") for _, val := range lst { fmt.Println("Name:", val.Name) fmt.Println("Title:", val.Title) fmt.Println("Body:", val.Body) } fmt.Println("------------------") } debuger(PostSort(lst, "name", ":")) debuger(PostSort(lst, "title:name", ":")) debuger(PostSort(lst, "name:-title", ":")) }
[debug] result Name: e Title: E-title Body: E-body Name: d Title: D-title Body: D-body Name: c Title: C-title Body: C-body Name: b Title: B-title Body: B-body Name: a Title: A-title Body: A-body ------------------ [debug] result Name: e Title: E-title Body: E-body Name: d Title: D-title Body: D-body Name: c Title: C-title Body: C-body Name: b Title: B-title Body: B-body Name: a Title: A-title Body: A-body ------------------ [debug] result Name: e Title: E-title Body: E-body Name: d Title: D-title Body: D-body Name: c Title: C-title Body: C-body Name: b Title: B-title Body: B-body Name: a Title: A-title Body: A-body ------------------
こちらから実際に結果を確認できます
play.golang.org
後日談
どうやら現在はもっと簡単に構造体を要素に持つ配列をソートできる模様。もっと簡単になりそう
mattn.kaoriya.net