[Swift] あると便利だったextension達 Date編

2017.10.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

公開後にいくつかの誤記・指摘箇所、追記したい事がありましたので加筆修正しました。(2017/10/26)

モバイルアプリサービス部の中安です。

アプリを作ってきた際に、あるとなかなか便利だったextensionをボチボチとご紹介していければと思っています。 アプリの目的に合わせてやりかえる必要があったり、もっとよい実装があるかもしれませんが、何かの役に立てば光栄です。

今回は Date 編です。

作るアプリにもよりますが、日付(Date 構造体)は開発時に触れることの多い頻出の構造体です。 それゆえに強力な機能の備わるサードパーティのライブラリもいくつも存在します。 しかし、ちょっとした痒いところに手が届く程度の機能ならば、Date を拡張させることでちょっと便利にすることができます。

そんな中の一部をご紹介します。

Calendar構造体の共通化

Calendar 構造体は日付を扱う中で重要なものになります。 Calendar 構造体から DateComponent 構造体をもとにして Date 構造体を作成するからです。

ただし、Calendar 構造体はロケールやタイムゾーンをしっかり指定しておかないとバグの温床になりますので、 最初に共通化しておくと良いかと思います。

extension Date {
    
    var calendar: Calendar {
        var calendar = Calendar(identifier: .gregorian)
        calendar.timeZone = .current
        calendar.locale   = .current
        return calendar
    }
}

上記のようにプロパティを定義しておけば、常に同じ設定の Calendar を取得することができます。

ただし、上記の設定は国際的にアプリを販売する場合に有効です。

対応するロケールが多ければ多いほど検討する項目が多いため、 日本国内向けのアプリという要件であれば、下記のようにロケールは日本に固定設定してしまうのも手です。

extension TimeZone {
    
    static let japan = TimeZone(identifier: "Asia/Tokyo")!
}

extension Locale {
    
    static let japan = Locale(identifier: "ja_JP")
}

extension Date {
    
    var calendar: Calendar {
        var calendar = Calendar(identifier: .gregorian)
        calendar.timeZone = .japan
        calendar.locale   = .japan
        return calendar
    }
}

基本6コンポーネントを指定できるようにする

「基本6コンポーネント」と勝手に銘打ちましたが、いわゆる「年月日時分秒」の6つのことを指します。

Date 構造体は直接これらのコンポーネントにアクセスできるようにはできていません。 DateComponent を介してアクセスする設計になっています。

ですので、そのあたりをラップするメソッドを用意しておくと便利になります。

絶対値で指定する

extension Date {
    
    func fixed(year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil, second: Int? = nil) -> Date {
        let calendar = self.calendar
        
        var comp = DateComponents()
        comp.year   = year   ?? calendar.component(.year,   from: self)
        comp.month  = month  ?? calendar.component(.month,  from: self)
        comp.day    = day    ?? calendar.component(.day,    from: self)
        comp.hour   = hour   ?? calendar.component(.hour,   from: self)
        comp.minute = minute ?? calendar.component(.minute, from: self)
        comp.second = second ?? calendar.component(.second, from: self)
        
        return calendar.date(from: comp)!
    }
}

現在の日時に対して「年月日時分秒」をコンポーネント単位で絶対値を指定できるメソッドです。 省略したコンポーネントは現在持っているコンポーネント値を引き続き使用します。

返されるオブジェクトは自身でなく、新しく生成されるオブジェクトになります。

使用例

let date = Date()
// 2017年10月25日 水曜日 15時53分17秒 日本標準時

date.fixed(hour: 12, minute: 30)
// 2017年10月25日 水曜日 12時30分17秒 日本標準時

date.fixed(year: 2010)
// 2010年10月25日 月曜日 15時53分17秒 日本標準時

指定したコンポーネントの値だけが変更されたことが確認できると思います。

相対値で指定する

前項が絶対値での指定であったのに対して、相対値での指定が可能なメソッドも用意しておくと便利です。

extension Date {
    
    func added(year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil, second: Int? = nil) -> Date {
        let calendar = self.calendar
        
        var comp = DateComponents()
        comp.year   = (year   ?? 0) + calendar.component(.year,   from: self)
        comp.month  = (month  ?? 0) + calendar.component(.month,  from: self)
        comp.day    = (day    ?? 0) + calendar.component(.day,    from: self)
        comp.hour   = (hour   ?? 0) + calendar.component(.hour,   from: self)
        comp.minute = (minute ?? 0) + calendar.component(.minute, from: self)
        comp.second = (second ?? 0) + calendar.component(.second, from: self)
        
        return calendar.date(from: comp)!
    }
}

メソッドのインターフェイスは同じですが、現在のコンポーネント値からどれほど追加(または減少)させるかを指定できるところが違います。 こちらも同様に返されるオブジェクトは自身でなく、新しく生成されるオブジェクトになります。

使用例

let date = Date()
// 2017年10月25日 水曜日 16時03分12秒 日本標準時

date.added(hour: 12, minute: 30)
// 2017年10月26日 木曜日 4時33分12秒 日本標準時

date.added(year: -10)
// 2007年10月25日 木曜日 16時03分12秒 日本標準時

date.fixed(hour: 12, minute: 30) は、「12時30分」を指定するのに対して、 date.added(hour: 12, minute: 30) は、「12時間30分後」を指定していることが確認できると思います。

イニシャライザ

ここまで作ったメソッドをもとにイニシャライザの定義も可能なので、やってみます。

extension Date {
    
    init(year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil, second: Int? = nil) {
        self.init(
            timeIntervalSince1970: Date().fixed(
                year:   year,
                month:  month,
                day:    day,
                hour:   hour,
                minute: minute,
                second: second
            ).timeIntervalSince1970
        )
    }
}

標準で用意されている Date のイニシャライザの種類は少ないので、timeIntervalSince1970 を使って初期化をさせています。

これで下記のようにオブジェクトを生成できます。 現在時刻をもとに作成するので、省略したコンポーネントは現在日時を使用します。

使用例

let date = Date()
// 2017年10月25日 水曜日 16時25分52秒 日本標準時

Date(year: 2014, month: 1, day: 1)
// 2014年1月1日 日曜日 16時25分52秒 日本標準時

ずいぶんと直感的にオブジェクトの初期化ができるようになりました。

コンポーネントの取得

標準の Date 構造体は、先述のとおり各コンポーネントの値を DateComponent を介して取得する必要があります。 しかし、これもまた煩わしいので簡単に取得できるようにしておきたいところです。

extension Date {
    
    var year: Int {
        return calendar.component(.year, from: self)
    }
    
    var month: Int {
        return calendar.component(.month, from: self)
    }
    
    var day: Int {
        return calendar.component(.day, from: self)
    }
    
    var hour: Int {
        return calendar.component(.hour, from: self)
    }
    
    var minute: Int {
        return calendar.component(.minute, from: self)
    }
    
    var second: Int {
        return calendar.component(.second, from: self)
    }
}

すでに calendarプロパティで各種設定をしてあるので、あとは component(_:from:) から値を呼べばいいだけです。

使用例

let date = Date()
// 2017年10月25日 水曜日 16時25分52秒 日本標準時

date.month // 10
date.minute // 25

これもまたシンプルに書くことができるようになりました。

曜日の取得

日付を表示する時、曜日も同時に表示する要件もあります。

標準のコンポーネントに weekday という日曜日から土曜日を1〜7の数値であらわすものが既に搭載されているので、 一番簡単に実装する方法とすれば、下記のようになると思います。

extension Date {
    
    var weekName: String {
        let index = calendar.component(.weekday, from: self) - 1 // index値を 1〜7 から 0〜6 にしている
        return ["日", "月", "火", "水", "木", "金", "土"][index]
    }
}

あくまで上記の実装は簡易的なものです。 このような曜日名の文字列配列は既にiOSに標準で準備されているので、改めて定義し直す必要は本来ありません。 ただ、それらを使うためには少し覚えて置かなければならないことがあります。

標準で用意されている曜日シンボル

DateFormatter クラスには、weekdaySymbols という文字列配列が数種類用意されています(実は Calendar にも用意されている)。

  • weekdaySymbols
  • standaloneWeekdaySymbols
  • veryShortWeekdaySymbols
  • shortWeekdaySymbols
  • shortStandaloneWeekdaySymbols
  • veryShortStandaloneWeekdaySymbols

各ロケールに対してローカライズされた文字列が取得できる大変便利なプロパティです。 ただし、日本語は「日」か「日曜日」という2種類のみ、英語でも「S」か「Sun」か「Sunday」と3種類のみで、 6種類すべて分岐することはなかなかないかもしれません(自分は言語博士じゃないので割愛)。

準備

まずはお試しでロケールをいくつか足します。 とりあえず、日本に加えて英語(US)と韓国語を追加しました。

extension Locale {
    
    static let japan = Locale(identifier: "ja_JP")
    
    static let us = Locale(identifier: "en_US") // ← 追加
    
    static let korea = Locale(identifier: "ko_KR") // ← 追加
}

他のロケールを試したいときは、こちらで identidier を確認してみてください。 https://gist.github.com/jacobbubu/1836273

曜日取得用の拡張

曜日を取得するために下記のような実装をしました。

extension Date {
    
    enum SymbolType {
        case `default`
        case standalone
        case veryShort
        case short
        case shortStandalone
        case veryShortStandalone
        case custom(symbols: [String])
    }
    
    var weekIndex: Int {
        return calendar.component(.weekday, from: self) - 1
    }
    
    func weeks(_ type: SymbolType = .short, locale: Locale? = nil) -> [String] {
        let formatter = DateFormatter()
        formatter.locale = locale ?? calendar.locale
        
        switch type {
        case .`default`:           return formatter.weekdaySymbols
        case .standalone:          return formatter.standaloneWeekdaySymbols
        case .veryShort:           return formatter.veryShortWeekdaySymbols
        case .short:               return formatter.shortWeekdaySymbols
        case .shortStandalone:     return formatter.shortStandaloneWeekdaySymbols
        case .veryShortStandalone: return formatter.veryShortStandaloneWeekdaySymbols
        case let .custom(symbols): return symbols
        }
    }
    
    func week(_ type: SymbolType = .short, locale: Locale? = nil) -> String {
        return weeks(type, locale: locale)[weekIndex]
    }
}

曜日のインデックス、ロケールを考慮した曜日名の配列、そして現日時の曜日名。 これらを enum によって種別指定を可能にしています。

これで少し煩わしかった曜日の取得は下記例のように随分とシンプルにできるようになったのではないかと思います。

使用例

let date = Date(year: 2017, month: 10, day: 25, hour: 0, minute: 0, second: 0)
// 2017年10月25日 水曜日 0時00分00秒 日本標準時

// 曜日インデックス
date.weekIndex // 3

// 曜日名の取得
date.week() // 水
date.week(.default) // 水曜日
date.week(.standalone, locale: .us) // Wednesday
date.week(.short, locale: .korea) // 수

// カスタムな曜日名
date.week(.custom(symbols: ["にち", "げつ", "か", "すい", "もく", "きん", "ど"])) // すい

// 曜日名配列の取得
date.weeks(.veryShort, locale: .us) // ["S", "M", "T", "W", "T", "F", "S"]

月の取得

「月」にも文字列表現があります。日本語だと数字に「〜月」と付けるだけですが、英語圏も含め諸外国では月に名前があることは言うまでもありません。

これもiOSには標準で用意されていて、配列の種類も曜日のときと同じです。 ですので、曜日の取得に似た形になるよう下記のように実装しました。

extension Date {
    
    var monthIndex: Int {
        return calendar.component(.month, from: self) - 1
    }
    
    // SymbolType は 前項の「曜日の取得」で定義したもの
    func monthSymbols(_ type: SymbolType = .short, locale: Locale? = nil) -> [String] {
        let formatter = DateFormatter()
        formatter.locale = locale ?? calendar.locale
        
        switch type {
        case .`default`:           return formatter.monthSymbols
        case .standalone:          return formatter.standaloneMonthSymbols
        case .veryShort:           return formatter.veryShortMonthSymbols
        case .short:               return formatter.shortMonthSymbols
        case .shortStandalone:     return formatter.shortStandaloneMonthSymbols
        case .veryShortStandalone: return formatter.veryShortStandaloneMonthSymbols
        case let .custom(symbols): return symbols
        }
    }
    
    func monthSymbol(_ type: SymbolType = .short, locale: Locale? = nil) -> String {
        return monthSymbols(type, locale: locale)[monthIndex]
    }
}

使用例

let date = Date(year: 2017, month: 10, day: 25, hour: 0, minute: 0, second: 0)
// 2017年10月25日 水曜日 0時00分00秒 日本標準時

// 月インデックス
date.monthIndex // 9

// 月名の取得
date.monthSymbol() // 10月
date.monthSymbol(.veryShort) // 10
date.monthSymbol(.standalone, locale: .us) // October
date.monthSymbol(.short, locale: .korea) // 10월

// カスタムな月名
date.monthSymbol(.custom(symbols: ["睦月","如月","弥生","卯月","皐月","水無月","文月","葉月","長月","神無月","霜月","師走"])) // 神無月

// 月名配列の取得
date.monthSymbols(.veryShort, locale: .us) // ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]