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

2017.12.20

はじめに

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

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

今回は Date 編のその2です。(その1はこちら)

お便利ファクトリ

日付を扱うサードパーティのライブラリには便利なファクトリが用意されていることが多いです。 今まで作ってきたextensionの機能を使えば、そういったものを作成するのも簡単ですし、あったほうが便利だと思います。

コンポーネントの切り捨て

extension Date {
    
    var oclock: Date {
        return fixed(minute: 0, second: 0)
    }
    
    var zeroclock: Date {
        return fixed(hour: 0, minute: 0, second: 0)
    }
}

前回作った fixed メソッドを使って、「0分0秒」や「0時0分0秒」の状態のDateオブジェクトを新たに作る計算プロパティです。

これの何が嬉しいかというと、下記例のように「〜日まで」や「〜日の時まで」という比較が簡単になるところでしょうか。

let date1 = Date(year: 2017, month: 4, day: 5)
// "2017年4月5日 水曜日 14時18分41秒 日本標準時"

let date2 = Date()
// 2017年10月26日 木曜日 14時18分41秒 日本標準時

Date().zeroclock
// 2017年10月26日 木曜日 0時00分00秒 日本標準時

date1 < Date().zeroclock ? "今日より前" : "今日より後"
// 今日より前

date2 < Date().zeroclock ? "今日より前" : "今日より後"
// 今日より後
let date1 = Date(year: 2017, month: 4, day: 5)
// "2017年4月5日 水曜日 14時18分41秒 日本標準時"

let date2 = Date()
// 2017年10月26日 木曜日 14時18分41秒 日本標準時

Date().fixed(hour: 18).oclock
// 2017年10月26日 木曜日 18時00分00秒 日本標準時

date1 < Date().fixed(hour: 18).oclock ? "今日の18時より前" : "今日の18時より後"
// 今日の18時より前

date2 < Date().fixed(hour: 18).oclock ? "今日の18時より前" : "今日の18時より後"
// 今日の18時より前

現時刻

現時刻のオブジェクトを返す計算プロパティも定義しておくと少し便利です。

extension Date {
    
    static var now: Date {
        return Date()
    }
}

これは Date() を呼び出せばいいだけの話なのですが、 下記のように Date を渡すときに何を渡しているかが一目で分かり、単純化されます。

struct Article {
    
    let content: String
    let publishDateTime: Date
}

let article = Article(content: "なんとかかんとか", publishDateTime: .now)

現日付

日付を比較で使用するために「本日」を返すような定義も便利です。 返ってくるのは「今日の0時0分0秒」となります。

extension Date {
    
    static var today: Date {
        return now.zeroclock
    }
}

さらに、ここから派生させて「昨日」「明日」「明後日」などもこのように定義します。

extension Date {
    
    static func day(after days: Int) -> Date {
        return today.added(day: days)
    }
    
    static var yesterday: Date {
        return day(after: -1)
    }
    
    static var tomorrow: Date {
        return day(after: 1)
    }
    
    static var dayAfterTomorrow: Date {
        return day(after: 2)
    }
}

使用例

let date = Date.now .description(with: .japan)
// "2017年10月26日 木曜日 15時00分27秒 日本標準時"

Date.today
// 2017年10月26日 木曜日 0時00分00秒 日本標準時
Date.yesterday
// 2017年10月25日 木曜日 0時00分00秒 日本標準時
Date.tomorrow
// 2017年10月27日 木曜日 0時00分00秒 日本標準時
Date.day(after: 7)
// 2017年11月2日 木曜日 0時00分00秒 日本標準時

月の最初と最後の日付

extension Date {
    
    var firstDayOfMonth: Date {
        return fixed(day: 1, hour: 0, minute: 0, second: 0)
    }
    
    var lastDayOfMonth: Date {
        return added(month: 1).fixed(day: 0, hour: 0, minute: 0, second: 0)
    }
}

これも前項までの応用編です。月初めと月終わりの日付を返します。

カレンダーやスケジュール帳を扱うようなアプリでは重宝するはずです。

let date: Date = .now
// 2017年10月26日 木曜日 15時24分25秒 日本標準時

"今月(\(date.monthSymbol()))は\(date.lastDayOfMonth.day)日までです"
// 今月(10月)は31日までです

"\(date.fixed(month: 2).monthSymbol())は\(date.fixed(month: 2).lastDayOfMonth.day)日までです"
// 2月は28日までです

日付のみの比較

Date構造体はComparableとEquatableに準拠しているので、演算子を使って比較することができます。 2つのDateを比べてより未来はどちらか、より過去はどちらかを取得するのは簡単です。

let date1 = Date.now
// 2017年10月26日 木曜日 15時42分00秒 日本標準時

let date2 = Date.today
// 2017年10月26日 木曜日 0時00分00秒 日本標準時

date1 > date2 ? "date1のほうが未来" : "date1のほうが過去"
// date1のほうが未来

しかしながら、日付だけで比較がしたいときにはこの書き方は有効ではありません。 上記例のdate1とdate2が同じ日付かどうかを比較するときに == で比較すると必ず false になります(0時0分0秒に実行しない限りは)

ですので、今まで作成してきたものを利用すると

let date1 = Date.now
let date2 = Date.today

date1 == date2 ? "同じ日だよ" : "違う日だよ"
// 違う日だよ ←期待通りではない

date1.zeroclock == date2.zeroclock ? "同じ日だよ" : "違う日だよ"
// 同じ日だよ ←期待通り

このように一旦同じ時間に置き換えて比較すれば、期待通りの比較ができます。

これをさらにラッピングすることで、もうひとつ便利になります。

extension Date {
    
    func isSameDay(_ otherDay: Date) -> Bool {
        return self.zeroclock == otherDay.zeroclock
    }
    
    func isSameDay(after days: Int) -> Bool {
        return isSameDay(Date.day(after: days))
    }
    
    var isToday: Bool {
        return isSameDay(Date.today)
    }
    
    var isYesterday: Bool {
        return isSameDay(Date.yesterday)
    }
    
    var isTomorrow: Bool {
        return isSameDay(Date.tomorrow)
    }
    
    var isDayAfterTomorrow: Bool {
        return isSameDay(Date.dayAfterTomorrow)
    }
}

使用例

let today: Date = .today
today.isToday // true

よい例が思いつかず、「今日は今日なのか?」と哲学的な問いになっていますが、 日付の一覧を表示して、本日だけ背景色を変えたいなどの要件のときにこういうプロパティがあると随分助かります。

曜日の比較

カレンダーなどを作成すると、土日は背景や文字の色を変更することは多々あります。 これも直感的なブール型プロパティを用意しておけば余計なソースは省くことができます。

extension Date {
    
    static let weekIndexOfSunday   = 0
    static let weekIndexOfSaturday = 6
    
    var isSunday: Bool {
        return weekIndex == Date.weekIndexOfSunday
    }
    
    var isSaturday: Bool {
        return weekIndex == Date.weekIndexOfSaturday
    }
    
    var isWeekend: Bool {
        return isSunday || isSaturday
    }
    
    var isUsualDay: Bool {
        return !isWeekend
    }
}

使用例

// 「今月20日間、週末は飲み物が安くなる」の例
(0..<20).forEach {
    let date = Date.now.firstDayOfMonth.added(day: $0)
    let price = date.isWeekend ? 150 : 400
    print("\(date.day)(\(date.week())) は ドリンクが \(price) 円")
}

/* 結果
1(日) は ドリンクが 150 円
2(月) は ドリンクが 400 円
3(火) は ドリンクが 400 円
4(水) は ドリンクが 400 円
5(木) は ドリンクが 400 円
6(金) は ドリンクが 400 円
7(土) は ドリンクが 150 円
8(日) は ドリンクが 150 円
9(月) は ドリンクが 400 円
10(火) は ドリンクが 400 円
11(水) は ドリンクが 400 円
12(木) は ドリンクが 400 円
13(金) は ドリンクが 400 円
14(土) は ドリンクが 150 円
15(日) は ドリンクが 150 円
16(月) は ドリンクが 400 円
17(火) は ドリンクが 400 円
18(水) は ドリンクが 400 円
19(木) は ドリンクが 400 円
20(金) は ドリンクが 400 円
*/

これは一例ですので、必要に応じて isFriday (金曜日かどうか) のようなものを足してください。