5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

iOS その2Advent Calendar 2016

Day 5

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

Posted at

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

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

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

UIDatePicker使えばよいのでは?

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

日付書式は何で決まるか?

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

どうやって判定するか?

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

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

実際にやってみる

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

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

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

最終的なコード

//: 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)")
    }
}

実行結果

まとめ

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

参考

Locale Identifier

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

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

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. 実際に使われているかどうかではなく、設定できてしまうという意味。

5
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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?