[Swift] あると便利だったextension達 Date編(修正版)

2017.12.22

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

はじめに

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

以前に Date の extension についてのブログを書きました。

【Swift】Tips: あると便利だったextension達(Date編)
【Swift】Tips: あると便利だったextension達(Date編 その2)

これを自分で読み直してみて「ああ、こうしておけばよかったなぁ」と思うところが出てきまして、 記事を加筆修正しようかなと思ったのですが、 新たにひとつ記事をつくることにしました。

直したいこと

修正したいところは「Date編その1」の「基本6コンポーネントを指定できるようにする」のところと、「コンポーネントの取得」のところです。

同じ内容を繰り返すのもアレなので簡略しますと

  • 年月日時分秒を指定して Dateオブジェクトを作れるようにしておくと便利ですよー
  • 年月日時分秒を整数で取得できるようにしておくと便利ですよー

という内容でした。

記事の中のソースコードをまとめると下記のような感じです。(長くてすいません)

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
        )
    }
    
    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)!
    }
    
    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)
    }
    
    var calendar: Calendar {
        var calendar = Calendar(identifier: .gregorian)
        calendar.timeZone = .japan
        calendar.locale   = .japan
        return calendar
    }
}

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

で、これを見て気づきました。

年月日時分秒は整数で取得できるけど、読み取り専用プロパティになっているから代入することができない。

date.year = 2010

って書いたら、Dateの中身が2010年にならないかなぁと思うわけです。

やってみよう

元のままにする箇所

Locale、TimeZone、Calendar の実装はそのままでいいかなと思います。

プロパティをなおす

コンポーネントのプロパティを書き換えてやります。

    // Before
    
    var year: Int {
        return calendar.component(.year, from: self)
    }
    
    // After
    
    var year: Int {
        get {
            return calendar.component(.year, from: self)
        }
        set {
        
        }
    }

そして、セットのための処理を加えてやることにしました。 Dateは構造体なので mutating にしてやる必要があります。

    private mutating func setComponentValue(_ value: Int, for component: Calendar.Component) {
        var components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: self)
        components.setValue(value, for: component)
        if let date = calendar.date(from: components) {
            self = date
        }
    }

あとは、Setterに適用させてやるだけです。

    var year: Int {
        get {
            return calendar.component(.year, from: self)
        }
        set {
            setComponentValue(newValue, for: .year)
        }
    }

これで、やりたかったこと date.year = 2010 みたいなことが実現できるようになります。

あとは他のコンポーネントプロパティも同じように変更します。

イニシャライザをなおす

こうして値がセットできるようになった Date では、元ソースにあった fixed() メソッドは不要になるかなぁと思います。

なので、fixed() メソッドを噛ませていたイニシャライザのやり方も変えることにします。

    // Before
    
    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
        )
    }
    
    // After
    
    init(year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil, second: Int? = nil) {
        self.init()
        if let value = year   { self.year   = value }
        if let value = month  { self.month  = value }
        if let value = day    { self.day    = value }
        if let value = hour   { self.hour   = value }
        if let value = minute { self.minute = value }
        if let value = second { self.second = value }
    }

指定したいコンポーネントだけが書き換わるイニシャライザの仕様はそのままに、Setterの仕組みで書くことが出来ました。

動作確認

ブログ執筆日は12月22日ですが、ちゃんと動くかなぁ?

        // 時刻は0時0分0秒にして、日付はそのままで、年を2010にしたい
        var date = Date(year: 2010, hour: 0, minute: 0, second: 0)

        print(date.year, date.month, date.day, date.hour, date.minute, date.second)
        // 出力結果: 2010 12 22 0 0 0

お、期待通りになりました。

コンポーネントの書き換えも上手くいくかな。

        // 時刻は0時0分0秒にして、日付はそのままで、年を2010にしたい
        var date = Date(year: 2010, hour: 0, minute: 0, second: 0)
        
        // 4月にしたい
        date.month = 4

        print(date.year, date.month, date.day, date.hour, date.minute, date.second)
        // 出力結果: 2010 4 22 0 0 0

期待通りです。

ちなみに月に0やマイナスを与えるときは、その年の1月基準になるので

  • date.month = 0 は、 前年の12月
  • date.month = -3 は、 前年の9月

という感じになります。

まとめ

最後に修正後のソースコードを載せておきます。 役に立つかどうかわかりませんが(そもそもこのシリーズはそういうノリです)、参考になさってください。

extension Date {
    
    init(year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil, second: Int? = nil) {
        self.init()
        if let value = year   { self.year   = value }
        if let value = month  { self.month  = value }
        if let value = day    { self.day    = value }
        if let value = hour   { self.hour   = value }
        if let value = minute { self.minute = value }
        if let value = second { self.second = value }
    }
    
    var year: Int {
        get {
            return calendar.component(.year, from: self)
        }
        set {
            setComponentValue(newValue, for: .year)
        }
    }
    
    var month: Int {
        get {
            return calendar.component(.month, from: self)
        }
        set {
            setComponentValue(newValue, for: .month)
        }
    }
    
    var day: Int {
        get {
            return calendar.component(.day, from: self)
        }
        set {
            setComponentValue(newValue, for: .day)
        }
    }
    
    var hour: Int {
        get {
            return calendar.component(.hour, from: self)
        }
        set {
            setComponentValue(newValue, for: .hour)
        }
    }
    
    var minute: Int {
        get {
            return calendar.component(.minute, from: self)
        }
        set {
            setComponentValue(newValue, for: .minute)
        }
    }
    
    var second: Int {
        get {
            return calendar.component(.second, from: self)
        }
        set {
            setComponentValue(newValue, for: .second)
        }
    }
    
    private mutating func setComponentValue(_ value: Int, for component: Calendar.Component) {
        var components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: self)
        components.setValue(value, for: component)
        if let date = calendar.date(from: components) {
            self = date
        }
    }
    
    var calendar: Calendar {
        var calendar = Calendar(identifier: .gregorian)
        calendar.timeZone = .japan
        calendar.locale   = .japan
        return calendar
    }
}

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

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