自然順ソート(マルチバイト対応)

2019/4/17 追記

改良版書きました!!

自然順ソート(マルチバイト対応改良版 & Windows エクスプローラー準拠) - bamch0h’s diary

前回前々回 の続きですが、マルチバイトの数字には対応できていなかったので対応させました。こちら を参考にしつつ、Windows エクスプローラー準拠の動作を実現させました。

前回のコードのままだと strconv.Atoi() でマルチバイト数字はパースエラーとなってしまうので、その前段に golang.org/x/text/width パッケージを挟むことで、全角数字を半角数字にしてから数字に変換しています。golang にはこういうパッケージが標準/準標準でたくさんあるのでありがたいですね。

package main

import (
    "fmt"
    "sort"
    "strconv"
    "unicode"

    "golang.org/x/text/width"
)

func getRune(ru []rune, i int) (rune, int) {
    for x := i; x < len(ru); x++ {
        if unicode.IsSpace(ru[x]) {
            continue
        }
        return ru[x], x
    }
    return rune(0), len(ru)
}

func natualSort0(a, b []rune) int {
    var ca, cb rune
    var ai, bi int
    for {
        if len(a) <= ai && len(b) <= bi {
            return 0
        }

        if len(a) <= ai {
            return -1
        }

        if len(b) <= bi {
            return 1
        }

        ca, ai = getRune(a, ai)
        cb, bi = getRune(b, bi)

        if unicode.IsNumber(ca) && unicode.IsNumber(cb) {
            aj := ai + 1
            for len(a) > aj {
                if !unicode.IsNumber(rune(a[aj])) {
                    aj--
                    break
                }
                aj++
            }

            bj := bi + 1
            for len(b) > bj {
                if !unicode.IsNumber(rune(b[bj])) {
                    bj--
                    break
                }
                bj++
            }

            sa := a[ai:aj]
            if len(sa) == 0 {
                sa = []rune{a[ai]}
            }
            ia, err := strconv.Atoi(width.Narrow.String(string(sa)))
            if err != nil {
                panic(err)
            }

            sb := b[bi:bj]
            if len(sb) == 0 {
                sb = []rune{b[bi]}
            }

            ib, err := strconv.Atoi(width.Narrow.String(string(sb)))
            if err != nil {
                panic(err)
            }

            if ia < ib {
                return -1
            }

            if ia > ib {
                return 1
            }

            // Windows エクスプローラーの仕様と合わせるために
            // 今回は削除
            // if len(a[ai:aj]) < len(b[bi:bj]) {
            //     return -1
            // }

            // if len(a[ai:aj]) > len(b[bi:bj]) {
            //     return -1
            // }

            // ai = aj
            // bi = bj
        }

        if ca < cb {
            return -1
        }

        if ca > cb {
            return 1
        }

        ai++
        bi++
    }
}

func naturalSort(strings []string) func(i, j int) bool {
    return func(i, j int) bool {
        return natualSort0([]rune(strings[i]), []rune(strings[j])) <= 0
    }
}

func dumpByNaturalSort(strings []string) {
    sort.Slice(strings, naturalSort(strings))
    for i, s := range strings {
        if i != 0 {
            fmt.Print(" < ")
        }
        fmt.Printf("%+v", s)
    }
    fmt.Println()
}

func main() {
    dumpByNaturalSort([]string{"a20", "a10", "a2", "a1b", "a1a", "a1", "a0", "a"})
    // a < a0 < a1 < a1a < a1b < a2 < a10 < a20

    dumpByNaturalSort([]string{"x8-y8", "x2-y08", "x2-y7", "x2-g8"})
    // x2-g8 < x2-y7 < x2-y08 < x8-y8

    dumpByNaturalSort([]string{"1.3", "1.1", "1.02", "1.010", "1.002", "1.001"})
    // 1.001 < 1.1 < 1.002 < 1.02 < 1.3 < 1.010

    dumpByNaturalSort([]string{"001", "01", "1", "10", "010", "2", "002"})
    // 001 < 01 < 1 < 002 < 2 < 010 < 10

    dumpByNaturalSort([]string{"a001", "a01", "a1", "a10", "a010", "a2", "a002"})
    // a001 < a01 < a1 < a002 < a2 < a010 < a10

    dumpByNaturalSort([]string{"3", "1", "10", "2", "20"})
    // 1 < 2 < 3 < 10 < 20
}