NSCalendar on iOS Advent Calendar 2013

この投稿は iOS Advent Calendar 2013 - Qiita の25日目の記事です。

今年の iOS Advent Calendarの最後の日に、NSCalendarをとりあげてみますね。 iOSとMacOSXで暦を司る、あまり目立たないですが重要なクラスです。

NSDate

さて、NSCalendarの説明をするには、まずNSDateからですよね。

NSDateとは、iOSとMacOSXで(日時を含む)時刻を表すクラスです。 2001/1/1を基準として、それより前の日時は負の値、それ以降の日時は正の値として保存されています。

伝統的なUnix系のOSでは、1970/1/1が時刻データの基準とされてきましたが、MacOSXのリリース年は2001年なので、それにあわせて2001年が基準になっているのかもしれません。

[NSDate date]で、現在の日付を取得することができるので、これをそのままNSLogに出してみましょう。

NSDate* now =[NSDate date];
NSLog(@"now is %@",[now description]);
    
// now is 2013-12-25 08:00:00 +0000

(descriptionで表示される時間は日本時間ではなくUTC。カレンダーはシステムのデフォルトカレンダーが使われています。)

なお、ここからの先のコードには、コードの最後のコメントとしてNSLogの実行結果が表示されています。 コードを実行した時間によって表示が変わりますが、日本時間の2013/12/25 17:00に実施した結果です。

日付と時刻の表示に関係する3つの要素

NSDateに含まれている時間をユーザーに対して表示する時には、3つの要素が関係しています。

一つ目はNSTimeZone。表示する日時の時刻に関係します。 二つ目はNSLocale。表示する言語と表示フォーマットに関係します。 そして、三つ目はNSCalendar。主に表示する日時の年月日に関係します。

この三つの要素をあわせて設定することのできる要素がNSDateFormatterなので、ここではNSDateFormatterを通してこの三要素の働きをみてみましょう。

  NSDateFormatter* myFormat = [[NSDateFormatter alloc] init];
  myFormat.dateStyle = NSDateFormatterFullStyle; // ここは気にしなくてもいいです。
  myFormat.timeStyle = NSDateFormatterFullStyle; // ここは気にしなくてもいいです。

NSTimeZone

まずは、一番わかりやすいNSTimeZoneから。 ご存知のように、同じ時刻でも、イギリスにいたり、ニューヨークにいたり、日本にいたり……など、経度に応じて時刻が変わりますよね。

いろいろなタイムゾーンをNSDateFormatterに設定すると、それぞれの場所の時刻のNSStringを取得することができます。

  NSTimeZone* TokyoTimeZone = [NSTimeZone timeZoneWithName:@"Asia/Tokyo"];
  myFormat.timeZone = TokyoTimeZone;
  strDate = [myFormat stringFromDate:now];
  NSLog(@"strDate is %@",strDate);
  // strDate is Wednesday, 25 December 2013 17:00:00 Japan Standard Time

  NSTimeZone* LosTimeZone = [NSTimeZone timeZoneWithName:@"America/Los_Angeles"];
  myFormat.timeZone = LosTimeZone;
  strDate = [myFormat stringFromDate:now];
  NSLog(@"strDate is %@",strDate);
  // strDate is Wednesday, 25 December 2013 00:00:00 Pacific Standard Time

  NSTimeZone* AmsTimeZone = [NSTimeZone timeZoneWithName:@"Europe/Amsterdam"];
  myFormat.timeZone = AmsTimeZone;
  strDate = [myFormat stringFromDate:now];
  NSLog(@"strDate is %@",strDate);

  // strDate is Wednesday, 25 December 2013 09:00:00 Central European Standard Time

また、場所と時期によってサマータイムが設定されていますが、タイムゾーンを設定すると、サマータイムも自動で反映されます。

なお、使用できるタイムゾーンは下記の関数でリストアップできます。

  NSArray* zones = [NSTimeZone knownTimeZoneNames];
  NSLog(@"zones are %@",zones);

NSLocale

次にNSLocale。 NSLocaleは、言語と地域を組み合わせて、文化圏を設定するための情報です。

NSLocaleによって影響があるのはまず言語。 適当にいろいろなロケールを設定すると、日時が知らない文字で表示されておもしろいですよね。

  NSLocale* frLocale = [NSLocale localeWithLocaleIdentifier:@"fr_FR"]; // フランス語
  myFormat.locale = frLocale;
  strDate = [myFormat stringFromDate:now];
  NSLog(@"strDate is %@",strDate);
  // strDate is mercredi 25 décembre 2013 17:00:00 heure normale du Japon

  NSLocale* iiLocale = [NSLocale localeWithLocaleIdentifier:@"ii_CN"]; // 何語??
  myFormat.locale = iiLocale;
  strDate = [myFormat stringFromDate:now];
  NSLog(@"strDate is %@",strDate);
  // strDate is 2013 ꊰꑋꆪ 25, ꆏꊂꌕ 17:00:00 GMT+09:00

  NSLocale* hyLocale = [NSLocale localeWithLocaleIdentifier:@"hy_AM"]; // ヒンズー語
  myFormat.locale = hyLocale;
  strDate = [myFormat stringFromDate:now];
  NSLog(@"strDate is %@",strDate);
  // strDate is 25 դեկտեմբերի, 2013 թ., չորեքշաբթի, 17:00:00, Ճապոնիայի ստանդարտ ժամանակ

そして、NSLocaleが影響するのは、言語だけではありません。

同じ言語を使っていても、国や地域によって時刻の表示方法は変わってきます。 よく例にあげられるのは、イギリスとアメリカの時刻の差。 同じ英語表記でも、アメリカでは「月、日、年」で、イギリスでは「日、月、年」の順番になります。 また、時刻表示も、アメリカではam/pm表示が優先されますが、イギリスでは24時間表示です。

  NSLocale* usLocale = [NSLocale localeWithLocaleIdentifier:@"en_US"];// アメリカ
  myFormat.locale = usLocale;
  strDate = [myFormat stringFromDate:now];
  NSLog(@"strDate is %@",strDate);
  // strDate is Wednesday, December 25, 2013 at 8:22:41 PM Japan Standard Time

  NSLocale* gbLocale = [NSLocale localeWithLocaleIdentifier:@"en_GB"];// イギリス
  myFormat.locale = gbLocale;
  strDate = [myFormat stringFromDate:now];
  NSLog(@"strDate is %@",strDate);
  //strDate is Wednesday, 25 December 2013 20:22:41 Japan Standard Time

使用できるロケールをリストアップするにはタイムゾーンと同様にこんな感じで。

  NSArray* locales = [NSLocale availableLocaleIdentifiers];
  NSLog(@"locales are %@",locales);

NSCalendar

そして、最後にNSCalendar。 これは、各国で異なる暦への対応するためのクラスです。

日本では一般的には西暦、または和暦を使いますが、沖縄では旧暦も使われているそうですよね。 イスラム圏ではイスラム暦、イスラエルではユダヤ暦が使われてる……のだと思います。(詳しくないんですが。)

今日は西暦では2013/12/25ですが、和暦では平成25年12月25日。 タイ仏歴では仏歴2556年12月25日だし、イスラム暦ではAH1435年2月22日です。

  NSCalendar* budd = [[NSCalendar alloc] initWithCalendarIdentifier:NSBuddhistCalendar];
  myFormat.calendar = budd;
  strDate = [myFormat stringFromDate:now];
  NSLog(@"strDate is %@",strDate);
  // strDate is Wednesday, 25 December 2556 BE 17:00:00 Japan Standard Time

  NSCalendar* islam = [[NSCalendar alloc] initWithCalendarIdentifier:NSIslamicCalendar];
  myFormat.calendar = islam;
  strDate = [myFormat stringFromDate:now];
  NSLog(@"strDate is %@",strDate);
  // strDate is Wednesday, 22 Safar 1435 AH 17:00:00 Japan Standard Time

iOSで使用できるカレンダーはこの11種類です。 (ただし後ろの4種類はiOS4.0以上でのみ使用可能です。)

ユーザーのシステム設定からはこれらのカレンダーがすべて設定できるわけではなく、システムの言語設定によって表示される項目が変わったりすることもあるようです。

  NSString * const NSGregorianCalendar; // グレゴリオ暦
  NSString * const NSBuddhistCalendar; // タイ仏歴
  NSString * const NSChineseCalendar; // 中国暦
  NSString * const NSHebrewCalendar; // ユダヤ暦
  NSString * const NSIslamicCalendar; // イスラム暦
  NSString * const NSIslamicCivilCalendar; // 
  NSString * const NSJapaneseCalendar; // 和暦
  NSString * const NSRepublicOfChinaCalendar // 
  NSString * const NSPersianCalendar // イラン暦
  NSString * const NSIndianCalendar // インド国定暦
  NSString * const NSISO8601Calendar // ISO8601規格

過去の元号

和暦(NSJapaneseCalendar)で表示を行うと、「昭和」や「平成」などの元号が表示されます。 実はこの元号は、最近のものだけではなくかなり古いものまで入っています。

NSJapaneseCalendarに設定されている最も古い元号は「大化」。 あの大化の改新の時の元号ですね。

Wikipediaの元号の項目には、「『日本書紀』によれば、大化の改新(645年)の時に「大化」が用いられたのが最初であるとされる」とあるので、最古の元号から最新の元号まで全部入っているということになります。 正直だれがこんな古い元号を使うんだかいまいちわかりませんが、こういうこだわりって個人的に大好きです。

ちなみに、Gregorian Calendarで645年1月1日に相当するNSDateを和暦で表示すると大化0年12月29日となり、三日ほどずれてしまっているんですよね。 旧暦にしてはずれが少ないので、このずれが何なのか気になります。 ご存知の方是非教えてください。

さて、来年のNSCalendarは……

ここまでの内容はだいたい公式の開発情報なので、Appleの公式ドキュメント(Date and Time Programming Guide)に詳しくのっていますが、ここからは非公式な話題。

実は、iOS7ではNSCalendarクラスに大きな変更が入るはずでした。 最終的な API Diffでは、NSCalendarは何も変更がなかったのですが、NSCalendarのヘッダーをみてみると、iOS6/iOS7で大きな方針変更が予定されていたことがわかります。 おそらく、今まで多少の差異があったiOSとMaOSXのDate/Time系のクラスを統合し、さらにいろいろと便利な関数を追加するはずだった、と予想されます。

その、「いろいろと便利な関数」ですが、実はiOS7には追加されていませんが、Marvericksには追加されています。

たとえば、特定のNSDateが「今日」に入るかどうかをチェックしてくれる、[NSCalendar isDateInToday:]や、同じく昨日バージョンの[NSCalendar isDateInYesterday:]、明日バージョンの[NSCalendar isDateInTomorrow:]。 二つのNSDateが同じ日に含まれるかどうかの[NSCalendar isDate:inSameDayAsDate:]なども地味に便利だし、それ以外にもかなりの数の関数が追加されています。

実際にNSDateを使っているエンジニアからすると、かなり欲しい関数(というより、おそらくほとんどのエンジニアが自分で実装していると思います)なので、次期アップデートには是非iOSにも追加してほしいなと思っています。

それでは、今年の iOS Advent Calendarは終わりです。 来年も、よい iOS 開発をおすごしください。