🪜

Swiftでの年月日順序の国際対応を考える

2023/04/22に公開

DatePickerですが、年月日がひっついているせいで、年月だとか月日だとかの特定の要素だけを選ばせたり、あるいは年・月・日をばらばらに選ばせるだとか、そういう用途にちょっと使いにくいなということがあります。

年月日順序の国際対応

かといって、自前でPickerを使って年・月・日を選ばせる、となると、どの順に年・月・日を置くか、という問題が出てきます。
自分で使うだけならともかく、AppStoreにリリースするとなれば、ある程度の国際対応が必要だろうと思います。

順序なんて何か調べる方法があるのでは?と思って調べましたが、以下のような記事があるぐらいでした。

https://qiita.com/y_koh/items/541e72957d03c9e900d0

↑の記事の方法はまあ分かるのですが、もう少し調べると↓のような記事もあります。

https://developer.apple.com/forums/thread/24279

Quinn "The Eskimo!"さんの回答が元の趣旨からずれてるんじゃないかと思ったりもしますが、さらに以下のような記事も見て、色々試したところ、それなりに行けそうなコードになりました。

https://developer.apple.com/forums/thread/701972

環境

この記事は以下の環境で確認しています。

【Xcode】14.3
【Swift】5.8
【iOS】16.4.1
(【macOS】13.3.1 ※Macは開発に使っただけで、実行自体は試してません)

コード

DateOrderIdentifier.swift
class DateOrderIdentifier {
    enum DateOrder {
        case DMY
        case YMD
        case MDY
        case Unknown
    }
    
    static func identifyDateOrder(localeIdentifer: String) -> DateOrder {
        let formatter = DateFormatter()
        let locale = Locale(identifier: localeIdentifer)
        formatter.locale = locale
        formatter.dateStyle = .none
        formatter.setLocalizedDateFormatFromTemplate("yyyyMMdd")
        guard let formatString = formatter.dateFormat else {
            return .Unknown
        }
#if DEBUG
        print("locale: \(localeIdentifer) formatString: \(formatString)")
#endif
        guard let yPos = formatString.firstIndex(of: "y") else {
            return .Unknown
        }
        guard let mPos = formatString.firstIndex(of: "M") else {
            return .Unknown
        }
        guard let dPos = formatString.firstIndex(of: "d") else {
            return .Unknown
        }
        if (yPos < mPos) {
            return .YMD
        }
        if (mPos < dPos) {
            return .MDY
        }
        if (dPos < mPos) {
            return .DMY
        }
        return .Unknown
    }
}
テストケース
final class DateOrderIdentifierTests: XCTestCase {
    func testIdentifyDateOrder() throws {
    
        let order1 = DateOrderIdentifier.identifyDateOrder(localeIdentifer: "ja_JP")
        XCTAssertEqual(order1, .YMD, "invalid order")
        let order2 = DateOrderIdentifier.identifyDateOrder(localeIdentifer: "en_US")
        XCTAssertEqual(order2, .MDY, "invalid order")
        let order3 = DateOrderIdentifier.identifyDateOrder(localeIdentifer: "vi_VN")
        XCTAssertEqual(order3, .DMY, "invalid order")
        
        let identifiers = Locale.availableIdentifiers
        for identifier in identifiers {
            let order = DateOrderIdentifier.identifyDateOrder(localeIdentifer: identifier)
            XCTAssertNotEqual(order, .Unknown, "unknown order for \(identifier)")
        }
    }
}

使い方

改めて書くほどではないですが、

let order = DateOrderIdentifier.identifyDateOrder(localeIdentifier:"ja_JP")
// -> orderには.YMD, .DMY, .MDY, .Unknownのいずれかが入る

というようなことです。

説明

DateFormatter.setLocalizedDateFormatFromTemplate()は、yとかMとかdとかの書式指定文字列を渡すと、その時点のロケールに沿ってその文字列に含まれている要素を並べ替えたりセパレータを含めたりして(?)書式文字列を生成し直し(?)、dateFormatにセットしてくれるようです。今回は"yyyyMMdd"を渡しています。

もっと厳密に言うと、要素は並べ替えだけでなくて、何か足したり置き換えたりもしているようです。
上記のテストコードで、デバッグプリント文から出力されたものを見ると、以下のようになっています。
GGGGとか'г'(何でしょう?)とかが追加されたり、yyyyyに置き換えられたりしています。

(抜粋)
locale: ja_JP formatString: yyyy/MM/dd
locale: pl_PL formatString: dd.MM.yyyy
locale: uz_Arab formatString: GGGGG y-MM-dd
locale: zh_Hans_SG formatString: yyyy年MM月dd日
locale: bg formatString: dd.MM.yyyy 'г'.

判定は、この生成されたdateFormat文字列のy,M,dの出る位置の順序をチェックすることで行っています。順序だけ分かれば良いという割り切り、とも言えます。

いちおう.Unknownが返ってくるパスもあるのですが、テストコードによると.Unknownが返ってくるロケールは今のところ(iOSには)なさそうです。

Discussion