LoginSignup
1
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-15

はじめに

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

やること

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

内容

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

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

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

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

// 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だけソートするわけにいかないので、辞書型リストにしてセットでソートする

で対応します。

コード


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で全部作ってやろうかなど思いましたが、上記の条件ならこれで大丈夫そうでした。

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

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3