今回とある要件で月日だけのDatePickerが必要になりました。実装はPickerである必要は無く、年と月が選べれば問題ないです。それだけなら簡単に実装できそうなのですが、国際化を考えると年月日の並び順を気にする必要があります。
国 | 書式 | 並び |
---|---|---|
日本 | 2016年1月5日 | YMD |
US | January 5, 2016 | MDY |
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
がそれになります。小文字のほうが言語で、大文字のほうが国という組み合わせになっています。つまり言語x国の組み合わせが存在することになります2。
どうやって判定するか?
APIがあれば一番良いのですが、特に見つかりませんでした。先に載せたWikipediaを辞書化できればよかったのですが、Countryだけで分けているので同じ地域で別の言語を使っている場合などが表現できていません。よってプログラムで導き出すことにしました。考え方は至ってシンプルです。
- 一般的な書式で日付を出力
- 年月日それぞればらした状態で日付を出力
- 両者を比較して出現順を調べる
実際にやってみる
言語一覧と国一覧は取得できる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
最終的なコード
//: 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日 |