iOS8からの日付(NSDate)操作・比較

日付操作とか比較とかしようとか思ってググると、NSDateComponentsを使ったやつがよく出てきます。一旦NSDateComponentsを作ってから一つの要素ごとに足し算したりとか色々めんどいなーと思ってリファレンス見てたら、日付操作とか日付比較で便利なメソッドNSCalendrに追加されていることに気づきました! introduced=8.0って書いてあるからiOS8からなのかなと思ったんですが、どうなんでしょう。 とりあえずiOS8からっぽいやつを試してみました。

日付生成

全体を通してですが、NSCalendarは西暦を使用します。currentCalendar()使っちゃうと和暦が設定されてると困るみたいです。

let calendar = NSCalendar(identifier: NSGregorianCalendar)!

時刻を指定してNSDateの生成

例えば2014年12月10日20時26分を生成するには、dateWithEraを使います。

let date2014_12_10_2026 = calendar.dateWithEra(1, year: 2014, month: 12, day: 10, hour: 20, minute: 26, second: 0, nanosecond: 0)!

// => 2014/12/10 20:26:00

dateWithEraのeraってなんだろうと思ったんですが、紀元前(BC)、紀元後(AD)のことだそうです。紀元前は、0、紀元後は1なので、1を指定しています。

ちなみに、和暦の場合はここが、なんか色んな値になるっぽいコメントがありました。

stackoverflowのDaveさんのコメント

+1 this is important because (for example) the Japanese calendar uses the Era to denote the reign of a single emperor. So while the Gregorian calendar only had had 2 eras do far, the Japanese calendar has had dozens.

時刻だけを変更したものを生成

NSDateオブジェクトの時刻だけを変えたいってときは、dateBySettingHourを使います。結構使う頻度高そう。例えばさっきのやつの時刻だけを変えるにはこうする。

let date2014_12_10_0830 = calendar.dateBySettingHour(8, minute: 30, second: 0, ofDate: date2014_12_10_2026, options: nil)!

// => 2014/12/10 08:30:00

日付を加減算したものを生成

dateByAddingUnitで年、月、日、時、分、秒を指定して加減算できます。 例えばさっき生成した日付の10日前はこんな感じ。

let date2014_11_30_0830 = calendar.dateByAddingUnit(.DayCalendarUnit, value: -10, toDate: date2014_12_10_0830, options: nil)!

//=> 2014/11/30 08:30:00

分だけを足し算するにはこう。.MinuteCalendarUnitを指定する。

let date2014_11_30_0845 = calendar.dateByAddingUnit(.MinuteCalendarUnit, value: 15, ofDate: date2014_11_30_0830, options: nil)!

// => 2014/11/30 08:45:00

わざわざNSDateComponentsを作って足し算引き算しなくてすむようになったので便利ですね。

日付要素の取得

componentってメソッドがあるので好きな要素だけをとれます。 例えば、だけが欲しいときは、こうやります。

let hour = calendar.component(.HourCalendarUnit, fromDate: date2014_11_30_0845)

// => 8

各要素をまとめてとりたい時用のAPIもあるんですが、参照渡しで値を取得するってやつで、なんか使い方が直感的じゃない気がする。(とりあえずタプルでとってみたけどタプルじゃなくてもできます。)

// 年月日の各要素をとる場合
var comps = (0, 0, 0, 0)
calendar.getEra(&comps.0, year: &comps.1, month: &comps.2, day: &comps.3, fromDate: date2014_11_30_0845)
// comps => (1, 2014, 11, 30)

// 時分秒の各要素をとる場合
var comps = (0, 0, 0, 0)
calendar.getHour(&comps.0, minute: &comps.1, second: &comps.2, nanosecond: &comps.3, fromDate: date2014_11_30_0845)
// comps => (8, 45, 0, 0)

日付の比較

同じ日付かどうか

isDateが比較用のAPIなんですが、日付が同じかどうかと一口で言っても、同じ年か、同じ月かという個別に判断したい場合もありますよね。そういうときにも、細かく指定できるようになってます。

例えば、2014/12/10 8:30と、2014/11/30 8:30が年が一緒か、月が一緒かチェックしたい場合はこう書きます。これは結構使いやすいんじゃないかなー。

// 年までを比較する場合は、.YearCalendarUnitを指定する
calendar.isDate(date2014_12_10_0830, equalToDate: date2014_11_30_0830, toUnitGranularity: .YearCalendarUnit)
// => true

// 年月までを比較する場合は、.MonthCalendarUnitを指定する
calendar.isDate(date2014_12_10_0830, equalToDate: date2014_11_30_0830, toUnitGranularity: .MonthCalendarUnit)
// => false

そもそも、日にちだけならもっと簡単なAPIisDate(date1: NSDate, inSameDayAsDate date2: NSDate)があります。

calendar.isDate(date2014_12_10_0830, inSameDayAsDate: date2014_12_10_2026)
// => true

calendar.isDate(date2014_12_10_2026, inSameDayAsDate: date2014_11_30_0830)
// => false

調べたい日付が、今日なのか、明日なのか、週末なのかも用意されてます。この辺はカレンダーとか作る場合に使うかもしれないですね。

// 例えば今日が12月10日ならtrue
calendar.isDateInToday(date2014_12_10_0830)

// 例えば今日が12月9日ならtrue
calendar.isDateInTomorrow(date2014_12_10_0830)

// 11月30日は日曜日なのでtrue
calendar.isDateInWeekend(date2014_11_30_0845)

日付の前後/大小の比較

これは、beforeとかafterとかって名前がついたAPIがあったらいいなというところでしたが、残念ながらいつものNSComparisonResultが返ってくるやつしかないっぽいです。 でもどこまで比較するかってのを指定できるようになってて、例えばこんなことができます。 2014/12/10 8:302014/12/10 20:26を日にちまでの部分だけで比較するためには、DayCalendarUnitを指定します。日にちまでは同じなので比較する順番を入れ替えても.OrderedSameが返ってきます。

calendar.compareDate(date2014_12_10_0830, toDate: date2014_12_10_2026, toUnitGranularity: .DayCalendarUnit)
// => `2014/12/10 8:30`と`2014/12/10 20:26` を比較しても .OrderedSame

calendar.compareDate(date2014_12_10_2026, toDate: date2014_12_10_0830, toUnitGranularity: .DayCalendarUnit)
// => `2014/12/10 20:26` と`2014/12/10 8:30`で比較しても .OrderedSame

そして、MinuteCalendarUnitを指定すれば分のところまでで比較されるので、どっちが先か後かが返ってきます。

calendar.compareDate(date2014_12_10_0830, toDate: date2014_12_10_2026, toUnitGranularity: .MinuteCalendarUnit)
// => `2014/12/10 8:30` < `2014/12/10 20:26` なので .OrderedAscending

calendar.compareDate(date2014_12_10_2026, toDate: date2014_12_10_0830, toUnitGranularity: .MinuteCalendarUnit)
// => `2014/12/10 20:26` > `2014/12/10 8:30` なので .OrderedDescending

おしゃれなメソッドはないけど、比較したい要素を指定できるので使い勝手はよくなってると思います。

謎のAPI

APIのドキュメントも読んだんですがなんとなく使い道がイメージできなかったやつたちです。 使い方も理解するのに時間がかかった。

その1: nextDateAfterDate

次のマッチする日付を探索します。 例えば、次の13日はいつ?というのを調べます。

calendar.nextDateAfterDate(NSDate(), matchingUnit: .DayCalendarUnit, value: 13, options: NSCalendarOptions.MatchNextTime)

// 今日が12/11だとしたら、12/13がかえってきます。
// 今日が12/14だとしたら、1/13がかえってきます。

うーん何かに使えるのかな。あと、NSCalendarOptionsの詳しい説明がリファレンスに書いてなくて各値が何を表すのかが分からないです。。カレンダーを探索するときに使えそうな雰囲気ではあるんだが。

その2: rangeOfWeekendStartDate

そして、一番謎だったAPIがこれ。 => rangeOfWeekendStartDate(datep: AutoreleasingUnsafeMutablePointer<NSDate?>, interval tip: UnsafeMutablePointer<NSTimeInterval>, containingDate date: NSDate)

使い方と使い道が分からないんだけど、なんとか使い方は理解できました。この参照渡し系のやつってなんとかならんのか。

要は、containingDateが週末かどうかをチェックしてくれるんだけど、参照渡しで、datepintervalを渡しておくと週末の始まりの日付と週末の期間の長さをセットしてくれるというAPI

// 参照渡し用に変数を宣言しておく
var startDate : NSDate? = nil
var interval = NSTimeInterval()
// 週末かどうかを判定したい日付
let containingDate = calendar.dateWithEra(1, year: 2014, month: 12, day: 21, hour: 1, minute: 1, second: 3, nanosecond: 1)!

calendar.rangeOfWeekendStartDate(&startDate, interval: &interval, containingDate: containingDate)
// => 2014/12/21は日曜なのでtrue

startDate
// => この週の週末は土曜からだから2014/12/20がセットされている

interval
// => この週の週末は2日間だからintervalには、`60*60*24*2=172800`がセットされている

うーん、何に使うんだろうか・・・きっとスケジュールアプリとか作ってる人は使い道がぱっと思いつくんだろうな。

まとめ

NSDateの操作は他の言語に比べて直感的な操作ができるものが長らく用意されてなかったけど、 iOS8ぐらいから少しずつ手を入れていくつもりなんでしょうか。 Javaだって8になってようやく使いやすいAPIが出てきたぐらいだし、なんかそんなもんなんでしょうか。 あと、日付生成系はほとんどNSDate?を返すので今回は説明用に!でアンラップしてますが、本当はチェックしてから使用する必要があるあたりはちょっと面倒かもしれません。