【Swift】Calendarに日付操作のメソッドを追加してみた

Swiftで日付や時刻を扱う場合は`Date`や`DateComponents`、`Calendar`などを使います。`Calendar`には日付に関する便利メソッドがいくつか実装されています。

例えば以下のコードで日付の始まり（当日の午前0時0分0秒）を取得できます。

```swift
let date = Date()
let today = Calendar.current.startOfDay(for: date)
```

また特定の`Date`が今日か、明日か、昨日か、週末かを判別できます。

```swift
let date = Date()
let isToday = Calendar.current.isDateInToday(date)
let isTomorrow = Calendar.current.isDateInTomorrow(date)
let isYesterday = Calendar.current.isDateInYesterday(date)
let isWeekend = Calendar.current.isDateInWeekend(date)
```

2つの`Date`が同じ日かどうか判別できます。

```swift
let date1 = Date()
let date2 = date1.addingTimeInterval(3600)
let isSameDay = Calendar.current.isDate(date1, inSameDayAs: date2)
```

他にも`amSymbol`、`pmSymbol`, `monthSymbols`, `weekdaySymbols`で「午前」「午後」「1月」「2月」「日曜日」「月曜日」などの文字列を取得したりもできます。

## 日付の始まりと終わり

`startOfDay`で日付の始まりを取得できましたが、例えば`endOfDay`（翌日の午前0時0分0秒）を取得したり、明日、昨日、などの`Date`を取得するようなメソッドを追加してみます。

```swift
extension Calendar {

    //MARK: - Day operations
    
    func endOfDay(for date:Date) -> Date {
        return nextDay(for: date)
    }
    
    func previousDay(for date:Date) -> Date {
        return move(date, byDays: -1)
    }
    
    func nextDay(for date:Date) -> Date {
        return move(date, byDays: 1)
    }

    //MARK: - Move operation

    func move(_ date:Date, byDays days:Int) -> Date {
        return self.date(byAdding: .day, value: days, to: startOfDay(for: date))!
    }
}
```

特に`endOfDay`は、`EventKit`や外部APIなどで日付の範囲を指定する時などに使うことができます。`endOfDay`の実装は`nextDay`と同じですが、コードを見たときに分かりやすいので別々にしています。

```swift
let date = Date()
let start = Calendar.current.startOfDay(for: date)
let end = Calendar.current.endOfDay(for: date)

// 今日1日のイベントを取得
let eventStore = EKEventStore()
let predicate = eventStore.predicateForEvents(withStart: start, end: end, calendars: nil)
let events = eventStore.events(matching: predicate)
```

## 週、月の始まりと終わり

週、月に関するメソッドも追加してみます。

```swift

extension Calendar {

    //MARK: - Week operations
    
    func startOfWeek(for date:Date) -> Date {
        let comps = self.dateComponents([.weekOfYear, .yearForWeekOfYear], from: date)
        return self.date(from: comps)!
    }
    
    func endOfWeek(for date:Date) -> Date {
        return nextWeek(for: date)
    }
    
    func previousWeek(for date:Date) -> Date {
        return move(date, byWeeks: -1)
    }
    
    func nextWeek(for date:Date) -> Date {
        return move(date, byWeeks: 1)
    }
    
    //MARK: - Month operations
    
    func startOfMonth(for date:Date) -> Date {
        let comps = dateComponents([.month, .year], from: date)
        return self.date(from: comps)!
    }
    
    func endOfMonth(for date:Date) -> Date {
        return nextMonth(for: date)
    }
    
    func previousMonth(for date:Date) -> Date {
        return move(date, byMonths: -1)
    }
    
    func nextMonth(for date:Date) -> Date {
        return move(date, byMonths: 1)
    }
    
    //MARK: - Move operations
    
    func move(_ date:Date, byWeeks weeks:Int) -> Date {
        return self.date(byAdding: .weekOfYear, value: weeks, to: startOfWeek(for: date))!
    }
    
    func move(_ date:Date, byMonths months:Int) -> Date {
        return self.date(byAdding: .month, value: months, to: startOfMonth(for: date))!
    }
}
```

以下のコードで昨日、明日、先週、来週、先月、来月の始まりの`Date`を取得できます。

```swift
let date = Date()
let yesterday = Calendar.current.previousDay(for: date)
let tomorrow = Calendar.current.nextDay(for: date)

let lastWeek = Calendar.current.previousWeek(for: date)
let nextWeek = Calendar.current.nextWeek(for: date)

let lastMonth = Calendar.current.previousMonth(for: date)
let nextMonth = Calendar.current.nextMonth(for: date)
```

また2日前、3週間後、半年前などは以下のように取得できます。

```swift
let date = Date()
let twoDaysAgo = Calendar.current.move(date, byDays: -2)
let threeWeeksFromNow = Calendar.current.move(date, byWeeks: 3)
let sixMonthsAgo = Calendar.current.move(date, byMonths: -6)
```

## 同じ週、月か

２つのDateが同じ週にあるか、同じ月にあるかを判別できるメソッドを追加しました。

```swift
extension Calendar {

    //MARK: - inSame operations
    
    func isDate(_ date1:Date, inSameWeekAs date2:Date) -> Bool {
        return isDate(date1, equalTo: date2, toGranularity: .weekOfYear)
    }
    
    func isDate(_ date1:Date, inSameMonthAs date2:Date) -> Bool {
        return isDate(date1, equalTo: date2, toGranularity: .month)
    }
}
```

## 月の日数・週数

ひと月の日数や週の数を取得するメソッドも追加しました。

```swift
extension Calendar {

    //MARK: - Range operations
    
    func daysInMonth(for date:Date) -> Int {
        return range(of: .day, in: .month, for: date)!.count
    }
    
    func weeksInMonth(for date:Date) -> Int {
        return range(of: .weekOfMonth, in: .month, for: date)!.count
    }
}
```

ひと月の日数や週数は以下のコードで取得できます。

```swift
let date = Date()
let days = Calendar.current.daysInMonth(for: date)
let weeks = Calendar.current.weeksInMonth(for: date)
```

## 2つの`Date`間の日数

2つの`Date`間の日数を取得するメソッドを追加しました。

```swift
extension Calendar {

    //MARK: - Range operations
    
    func days(from date1:Date, to date2:Date) -> Int {
        let comps = dateComponents([.day], from: startOfDay(for: date1), to: startOfDay(for: date2))
        return comps.day!
    }
}
```

使い方はこのようになります。

```swift
let date1 = Date()
let date2 = Calendar.current.move(date1, byDays: 7)
let days = Calendar.current.days(from: date1, to: date2)
```

`Calendar`に実装されているメソッドを使って、まだまだ色々な便利メソッドを追加できそうです。

## `TimeZone`について

世界には時差があるので、日付や時刻を扱う場合は常にそのことを意識する必要があります。そのために`TimeZone`を使います。`Calendar`に`TimeZone`を指定すると、例えば「アメリカ西海岸の1日の始まりと終わり」を以下のように取得できます。

```swift
let date = Date()
if let timeZone = TimeZone(identifier: "America/Los_Angeles") {
    var calendar = Calendar.current
    calendar.timeZone = timeZone
    let start = calendar.startOfDay(for: date)
    let end = calendar.endOfDay(for: date)
}
```

`Calendar`に日付関連のメソッドを追加する大きな理由のひとつに、`TimeZone`対応が挙げられます。今回追加した全てのメソッドはタイムゾーンに対応しています。

## 週の始まりについて

週の始まりは日米では日曜日、欧州では月曜日が一般的です。`Calendar`のメソッドはすでに`firstWeekday`に対応しているため、今回追加したメソッドもこれに対応済みとなります。

例えば`weeksInMonth`は週の始まりが日曜日か月曜日かで結果が変わります。

```swift
var c = DateComponents()
c.year = 2018
c.month = 4
c.day = 1
let date = Calendar.current.date(from: c)!

var calendar = Calendar.current
calendar.firstWeekday = 1 // 日曜日
print(calendar.weeksInMonth(for: date)) // 実行結果：5

calendar.firstWeekday = 2 // 月曜日
print(calendar.weeksInMonth(for: date)) // 実行結果：6
```

また`startOfWeek`や`nextWeek`などのメソッドも`firstWeekday`に基づいて正しい`Date`が返されます。