Swift: ひらがな・カタカナ文字列の五十音順ソートで躓いた

# はじめに

APIレスポンスから受け取ったデータを、五十音順にソートしてリスト表示するというものを行いました。
実際には不要だったのでプロダクトコードには残していませんが、ちょっとややこしくて時間食った＆やった過程を無駄にしたくないので備忘録。

# やること

- `[{"name": "お店の名前", "kana": "フリガナ"}]`というリストを五十音順ソート
- リスト表示するのは`name`のお店の名前のみ

# 内容

五十音順と言えども、以下のような順序の指定がありました。

`アイウエオ順→長音→清音→濁音→半濁音→小書きの仮名→ナミ字`

- 長音:　　ー　(直前の母音)
- 静音：　　ア　とか　ハ　とか
- 濁音：　　ガ　とか　ダ　とか
- 半濁音：　パ　とか　ペ　とか
- 小文字：　ョ　とか　ッ　とか
- ナミ字：　〜　（直前の母音）

---
`Swift`には`.sorted()`メソッドが用意されているので、配列や辞書などはこれを用いることができます。

```Swift
// array
let array: [String] = ["パン", "バ", "ハ", "ハ〜シ", "〜バ", "ハーシ", "ハバ", "ハハ", "ハパーシ", "ハョーニ", "ーバ"]
let sortedArray = array.sorted()
print("sortedArray \(sortedArray)")
// sortedArray ["〜バ", "ハ", "ハ〜シ", "ハハ", "ハバ", "ハパーシ", "ハョーニ", "ハーシ", "バ", "パン", "ーバ"]

// dictionary
let dict = ["1": "パン", "2": "バ", "3": "ハ", "4": "ハ〜シ", "5": "〜バ", "6": "ハーシ", "7": "ハバ", "8": "ハハ", "9": "ハパーシ", "10": "ハョーニ", "11": "ーバ"]
let sortedDict = dict.sorted { $0.value < $1.value }
print("sortedDict \(sortedDict)")
// sortedDict [(key: "5", value: "〜バ"), (key: "3", value: "ハ"), (key: "4", value: "ハ〜シ"), (key: "8", value: "ハハ"), (key: "7", value: "ハバ"), (key: "9", value: "ハパーシ"), (key: "10", value: "ハョーニ"), (key: "6", value: "ハーシ"), (key: "2", value: "バ"), (key: "1", value: "パン"), (key: "11", value: "ーバ")]
```

辞書型の戻り値は純粋な辞書型ではなく、
`[(key: Data, value: Data)]`
という、**keyとvalueを持ったタプルの配列**で返ってきます。

ちなみに、ひら・カナ混合だと、降順だと`ひら→カナ`の順です。

---

しかし、既存のメソッドでソートされる順序は

`アイウエオ順→ナミ字→清音→濁音→半濁音→小書きの仮名→長音`

のようにナミ字の判定が最前列、長音が最後列といった風に上記の期待とは一部逆になります。
（厳密にいうとナミ字が`ア`よりも前に来ます）

今回必要とされる条件は

1. `アイウエオ順→長音→清音→濁音→半濁音→小書きの仮名→ナミ字`の順
2. 規定に沿ってソート、お店の名前だけディスプレイ

なので

1.　静音 <-> ナミ字 をスワップしてソートする
2.　`kana`だけソートするわけにいかないので、辞書型リストにしてセットでソートする

で対応します。

# コード


```swift

struct Shop {
    let name: String
    let kana: String
}

let shopList: [Shop] = [Shop(name: "1", kana: "パン"),
                        Shop(name: "2", kana: "バ"),
                        Shop(name: "3", kana: "ハ"),
                        ...]
// 全部書くの大変なので、以下のデータを元にしてAPIレスポンス時にshopListを作成していると思ってください
// ["1": "パン", "2": "バ", "3": "ハ", "4": "ハ〜シ", "5": "〜バ", "6": "ハーシ", "7": "ハバ", "8": "ハハ", "9": "ハパーシ", "10": "ハョーニ", "11": "ーバ"]

private var shopNames: [String] {
    // name と kana をセットにするため辞書型にする
    let baseDict = shopList.flatMap { [$0.name: $0.kana] }
    // ”〜” と ”ー”　を入れ替えてソートする
    let replaced = replaceWaveAndLong(dict: baseDict).sorted { $0.value < $1.value }
    // ソートしたら元のデータに戻すために、再度”〜” と ”ー”　を入れ替える
    let dict = replaceWaveAndLong(dict: replaced)

    print("dict \(dict)")
    // dict [(key: "11", value: "ーバ"), (key: "3", value: "ハ"), (key: "6", value: "ハーシ"), (key: "8", value: "ハハ"), (key: "7", value: "ハバ"), (key: "9", value: "ハパーシ"), (key: "10", value: "ハョーニ"), (key: "4", value: "ハ〜シ"), (key: "2", value: "バ"), (key: "1", value: "パン"), (key: "5", value: "〜バ")]

    // name の部分だけ取り出す
    return dict.map { $0.key }
}

// 指定のソート順に合わせるための　”〜” と ”ー”　を入れ替えるメソッド
func replaceWaveAndLong(dict: [(key: String, value: String)]) -> [(key: String, value: String)] {
    let replaced = dict.map { elem -> (key: String, value: String) in
        let wave = "〜", long = "ー"
        
        if elem.value.contains(wave) {
            return (key: elem.key,
                    value: elem.value.replacingOccurrences(of: wave, with: long))
        }
        if elem.value.contains(long) {
            return (key: elem.key,
                    value: elem.value.replacingOccurrences(of: long, with: wave))
        }
        return elem
    }
    return replaced
}
```

といった感じです。

# まとめ

長音は大体の場合、前の文字の母音になるので、大体順序は先頭に来るように指定されることが多そう。でも既存だと後ろに行ってしまうので、このように手を加える必要がありそう。

ちなみにここに辿り着くまで、長音が来たら前の文字をローマ字化して、その母音と同じ音に変換してとか
unicodeで文字をスイッチしたりとか、`enum`で全部作ってやろうかなど思いましたが、上記の条件ならこれで大丈夫そうでした。

実装することはなかったけど少し勉強になりました、ちゃんちゃん。