【サンプルコード有り】golangで複数条件のソートを無名関数を使っていい感じに実装してみた

何をしようとしているのか

struct(以降、構造体と表記)を要素に持つ、配列をソートする必要がある場面に出くわした。通常というか一般的な数値や文字列のソートと異なり、構造体のAフィールドの値が大きい順番かつ、Bフィールドの値が小さい順かつ...のようなソートを行う必要があるため、結構メンドくさい

一応、調べているとgolangにはパッケージ(組み込みではない)としてsortというものが用意されている。なお、sortの詳しい話は今回扱わないし、すでに素晴らしい情報が数多く公開されているのでそちらを参照して頂きたい。(参考文献にまとめています)

そんな中、こちらのブログで実装されている、構造体の複数fieldでのソート実装を見て、「なるほど、こんな感じでやればええんやな」と思い、ほとんどコピペで類似の処理を実装したものの、ソートする条件を関数化出来ない(ソートしたい各所でソート条件を持つ関数群を定義しなければならない)煩わしさに何とかならないかと思い試行錯誤した

or3.hatenablog.com

以下は引用のコードになります。非常に参考になりました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経由でのGETquery 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

参考文献