国際化に伴う日付の年月日順序判定をやってみた

今回とある要件で月日だけのDatePickerが必要になりました。実装はPickerである必要は無く、年と月が選べれば問題ないです。それだけなら簡単に実装できそうなのですが、国際化を考えると年月日の並び順を気にする必要があります。

| 国 | 書式 | 並び |
|:-:|:-:|:-:|
| 日本 | 2016年1月5日 | YMD |
| US | January 5, 2016 | MDY |

https://en.wikipedia.org/wiki/Date_format_by_country

Wikipediaを見てみると年月日の並びとしては`YMD`、`DMY`、`MDY`の3種類あることがわかります。ちなみに日本で使われている`YMD`は2番めに多く、一番多いのは`DMY`です。この3種類の中からどれになるかということを判定したいというのが今回の内容です。

## UIDatePicker使えばよいのでは？
`UIDatePicker`を使えば`Locale`を指定するだけでよしなにpickerを組み立ててくれます。しかし、`UIDatePicker`は日付型が戻り値なので年は必須になってしまいます。よって年を使わないPickerを実装するのであれば自前で`UIPickerView`を使って実装することになります。

## 日付書式は何で決まるか？
日付書式は`Locale`で決まります[^1]。`Local`は`ideitifier`を渡してあげることで指定の`Locale`を生成できます。日本であれば`ja_JP`、USであれば`en_US`がそれになります。小文字のほうが言語で、大文字のほうが国という組み合わせになっています。つまり言語ｘ国の組み合わせが存在することになります[^2]。

## どうやって判定するか？
APIがあれば一番良いのですが、特に見つかりませんでした。先に載せたWikipediaを辞書化できればよかったのですが、Countryだけで分けているので同じ地域で別の言語を使っている場合などが表現できていません。よってプログラムで導き出すことにしました。考え方は至ってシンプルです。

1. 一般的な書式で日付を出力
2. 年月日それぞればらした状態で日付を出力
3. 両者を比較して出現順を調べる

## 実際にやってみる

言語一覧と国一覧は取得できるAPIが無かったのでWikipediaから引っ張ってきました。言語が184、国・地域名が249ありました。つまり、組み合わせは45,816通りになります。

やってみるとやたらunknownが多いことがわかりました。出力結果を見てみると`DateFormatter.Style`が`long`の場合と年月日個別で出力するときとで異なる表記になっています。さらに辛いことにそもそも月表記が異なるケースもあります。

|  | long | 年 | 月 | 日 | 備考 |
|:-:|:-:|:-:|:-:|:-:|:-:|
| **日本** | 2016年2月14日 | 2016年 | 2月 | 14日 | |
| **US** | February 14, 2016 | 2016 | February | 14 | longだとカンマが入る |
| **リトアニア** | 2016 m. vasario 14 d. | 2016 | vasaris | 14 | 色々違う！ |

悩ましいのですが、とりあえず今回は年月日の並び順がわかれば良いです。実行結果をざっと眺めた感じ年と日に関してはそこまで表記が変わっていないようだったのでこちらを判定に加えてみます。

先程の条件に合致しなかった場合

- 年が先頭なら`YMD`
- 日が先頭なら`DMY`

こうやってみるとこれだけで3パターンの分岐網羅できそうなのですが、やはり3つの要素を確認したほうが確実なので元のロジックは残したままにします。

## 結果は？
ここまでやるとだいたい取れるようになります。今のロジックでunknownになってしまうものはこれらの言語でした。

- **bo** チベット
- **vi** ベトナム
- **dz** ゾンカ

今回はやりませんが、例えばベトナムであればこのように年月日がわかりやすい形になっているのでプログラムの改良で拾えるようになると思います。アラビア数字でもないものは難しいので例外対応するのが良さそうです。

```
vi_VN, "Ngày 14 tháng 02 năm 2016", UNKNOWN
```

## 最終的なコード

```swift
//: Playground - noun: a place where people can play

import Foundation

/// format元になる数字がかぶらない日付
let dateString = "2016/02/14"

/// YMD並び順のenum
enum OrderType: String {
    case ymd = "YMD"
    case mdy = "MDY"
    case dmy = "DMY"
    case unknown = "UNKNOWN"
}

/// YMD, MDY, DMYを返す
func getOrderType(dateString: String, localeIdentifier: String) -> OrderType {
    // 日付型取得
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy/MM/dd"
    let date = dateFormatter.date(from: dateString)!
    
    // 一般的な日付文字列を取得
    dateFormatter.dateStyle = .long
    dateFormatter.timeStyle = .none
    dateFormatter.locale = Locale(identifier: localeIdentifier)
    let dateStr = dateFormatter.string(from: date) // -> February 14, 2016
    
    // 年月日それぞれの文字列を取得
    dateFormatter.setLocalizedDateFormatFromTemplate("yyyy")
    let y = dateFormatter.string(from: date) // -> 2016
    dateFormatter.setLocalizedDateFormatFromTemplate("MMMM")
    let m = dateFormatter.string(from: date) // -> February
    dateFormatter.setLocalizedDateFormatFromTemplate("d")
    let d = dateFormatter.string(from: date) // -> 14
    
    // 年月日それぞれの順番を確認する
    let yRange = dateStr.lowercased().range(of: y.lowercased())
    let mRange = dateStr.lowercased().range(of: m.lowercased())
    let dRange = dateStr.lowercased().range(of: d.lowercased())
    if let yRange = yRange, let mRange = mRange, let dRange = dRange {
        let yLowerBound = yRange.lowerBound // -> 13
        let mLowerBound = mRange.lowerBound // -> 0
        let dLowerBound = dRange.lowerBound // -> 9
        
        if yLowerBound < mLowerBound && mLowerBound < dLowerBound {
            return OrderType.ymd
        } else if mLowerBound < dLowerBound && dLowerBound < yLowerBound {
            return OrderType.mdy
        } else if dLowerBound < mLowerBound && mLowerBound < yLowerBound {
            return OrderType.dmy
        }
    }
    
    // 上記から漏れた場合は頭が年か日かを確認する
    if dateStr.isStart(with: y) {
        return OrderType.ymd
    } else if dateStr.isStart(with: d) {
        return OrderType.dmy
    }
    
    return OrderType.unknown
}

extension String {
    /// 引数の文字列から始まるかどうか
    func isStart(with string: String) -> Bool {
        let regex: NSRegularExpression
        do {
            let pattern = "^\(string)"
            regex = try NSRegularExpression(pattern: pattern, options: [])
            
            let results = regex.matches(in: self, options: [], range: NSMakeRange(0, self.characters.count))
            
            guard let result = results.first else { return false }
            let range = result.range
            guard range.location != NSNotFound else { return false }
            
            let matchString = (string as NSString).substring(with: range) as String
            return !matchString.isEmpty
        } catch _ as NSError {
        }
        return false
    }
}

/// 言語コード https://ja.wikipedia.org/wiki/ISO_639-1コード一覧
let languageCodes = ["ab", "aa", "af", "ak", "sq", "am", "ar", "an", "hy", "as", "av", "ae", "ay", "az", "bm", "ba", "eu", "be", "bn", "bh", "bi", "bs", "br", "bg", "my", "ca", "ch", "ce", "ny", "zh", "cv", "kw", "co", "cr", "hr", "cs", "da", "dv", "nl", "dz", "en", "eo", "et", "ee", "fo", "fj", "fi", "fr", "ff", "gl", "ka", "de", "el", "gn", "gu", "ht", "ha", "he", "hz", "hi", "ho", "hu", "ia", "id", "ie", "ga", "ig", "ik", "io", "is", "it", "iu", "ja", "jv", "kl", "kn", "kr", "ks", "kk", "km", "ki", "rw", "ky", "kv", "kg", "ko", "ku", "kj", "la", "lb", "lg", "li", "ln", "lo", "lt", "lu", "lv", "gv", "mk", "mg", "ms", "ml", "mt", "mi", "mr", "mh", "mn", "na", "nv", "nb", "nd", "ne", "ng", "nn", "no", "ii", "nr", "oc", "oj", "cu", "om", "or", "os", "pa", "pi", "fa", "pl", "ps", "pt", "qu", "rm", "rn", "ro", "ru", "sa", "sc", "sd", "se", "sm", "sg", "sr", "gd", "sn", "si", "sk", "sl", "so", "st", "es", "su", "sw", "ss", "sv", "ta", "te", "tg", "th", "ti", "bo", "tk", "tl", "tn", "to", "tr", "ts", "tt", "tw", "ty", "ug", "uk", "ur", "uz", "ve", "vi", "vo", "wa", "cy", "wo", "fy", "xh", "yi", "yo", "za", "zu"]

/// 国コード https://ja.wikipedia.org/wiki/ISO_3166-1
let countryCodes = ["IS", "IE", "AZ", "AF", "US", "VI", "AS", "AE", "DZ", "AR", "AW", "AL", "AM", "AI", "AO", "AG", "AD", "YE", "GB", "IO", "VG", "IL", "IT", "IQ", "IR", "IN", "ID", "WF", "UG", "UA", "UZ", "UY", "EC", "EG", "EE", "ET", "ER", "SV", "AU", "AT", "AX", "OM", "NL", "GH", "CV", "GG", "GY", "KZ", "QA", "UM", "CA", "GA", "CM", "GM", "KH", "MP", "GN", "GW", "CY", "CU", "CW", "GR", "KI", "KG", "GT", "GP", "GU", "KW", "CK", "GL", "CX", "GD", "HR", "KY", "KE", "CI", "CC", "CR", "KM", "CO", "CG", "CD", "SA", "GS", "WS", "ST", "BL", "ZM", "PM", "SM", "MF", "SL", "DJ", "GI", "JE", "JM", "GE", "SY", "SG", "SX", "ZW", "CH", "SE", "SD", "SJ", "ES", "SR", "LK", "SK", "SI", "SZ", "SC", "GQ", "SN", "RS", "KN", "VC", "SH", "LC", "SO", "SB", "TC", "TH", "KR", "TW", "TJ", "TZ", "CZ", "TD", "CF", "CN", "TN", "KP", "CL", "TV", "DK", "DE", "TG", "TK", "DO", "DM", "TT", "TM", "TR", "TO", "NG", "NR", "NA", "AQ", "NU", "NI", "NE", "JP", "EH", "NC", "NZ", "NP", "NF", "NO", "HM", "BH", "HT", "PK", "VA", "PA", "VU", "BS", "PG", "BM", "PW", "PY", "BB", "PS", "HU", "BD", "TL", "PN", "FJ", "PH", "FI", "BT", "BV", "PR", "FO", "FK", "BR", "FR", "GF", "PF", "TF", "BG", "BF", "BN", "BI", "VN", "BJ", "VE", "BY", "BZ", "PE", "BE", "PL", "BA", "BW", "BQ", "BO", "PT", "HK", "HN", "MH", "MO", "MK", "MG", "YT", "MW", "ML", "MT", "MQ", "MY", "IM", "FM", "ZA", "SS", "MM", "MX", "MU", "MR", "MZ", "MC", "MV", "MD", "MA", "MN", "ME", "MS", "JO", "LA", "LV", "LT", "LY", "LI", "LR", "RO", "LU", "RW", "LS", "LB", "RE", "RU"]

// 確認用スクリプト
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd"
let date = dateFormatter.date(from: dateString)!
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .none
getOrderType(dateString: dateString, localeIdentifier: "en_US")

// 全言語＆国パターン出力
// 例）ja_JP, 2015年3月4日, YMD
for languageCode in languageCodes {
    for countryCode in countryCodes {
        let localeIdentifier = "\(languageCode)_\(countryCode)"
        dateFormatter.locale = Locale(identifier: localeIdentifier)
        let dateStr = dateFormatter.string(from: date)
        let formatString2 = getOrderType(dateString:dateString, localeIdentifier: localeIdentifier)
        print("\(localeIdentifier), \"\(dateStr)\", \(formatString2.rawValue)")
    }
}
```

## 実行結果
https://gist.github.com/yoonchulkoh/cd65afa2e47cfd269dd17fee1ce4b744

## まとめ
もう少し詰めは必要ですがこれで年月日の順番を知ることが出来るようになりました。最悪判定できない場合はUSに合わせておくなどデフォルト値を決めておくと良いと思います。

# 参考
## Locale Identifier
iOSのLocale Identifiersをまとめてるgistがありました。
https://gist.github.com/jacobbubu/1836273

リンク先のコメントにもありますが`availableLocaleIdentifiers`というメソッドがあるのでこちらで取得できます。

```swift
let identifiers = NSLocale.availableLocaleIdentifiers
let locale = NSLocale(localeIdentifier: "en_US")
var list = NSMutableString()
for identifier in identifiers {
    let name = locale.displayName(forKey: NSLocale.Key.identifier, value: identifier)!
    list.append("\(identifier)\t\(name)\n")
}
```

ただ、実際にはiOS上で自分で地域を変えることも出来るので組み合わせの通り数はもっと多いはずです。この`LocaleIdentifiers`はプログラム側から指定する時に使うと良さそうですね。

## グレゴリアンを使わない国
下記国はどの言語との組み合わせでもグレゴリアンにはなっていませんでした。

| identifier | 国 | 書式 |
|:-:|:-:|:-:|:-:|:-:|
| ja_AF | アフガニスタン | AP1393年11月25日 |
| ja_IR | イラン | AP1393年11月25日 |
| ja_SA | サウジアラビア | AH1436年4月25日 |
| ja_TH | タイ | 仏暦2558年2月14日 |

[^1]: `Calendar`の種類（gregorian、和暦、仏暦など）によっても変わりますが、ここでは考えないことにします。

[^2]: 実際に使われているかどうかではなく、設定できてしまうという意味。