[iOS] 日付を扱う際に注意しなければならない3つのプロパティ(和暦にすると曜日がずれませんか・・・)

2016.03.02

1 NSDate

NSDateは、基準日(2001/01/01 00:00:00 GMT)からの経過時間を保持しています。 NSDateには、タイムゾーンやローケルの概念がないので、次のようなコードは、いつでも問題なく動作します。

NSDate *date = [NSDate date];
NSLog(@"現在の日時 = %@",date); // 現在の日時 = 2016-03-02 04:58:28 +0000

double seconds = [date timeIntervalSince1970];
NSLog(@"1970/01/01 から %.0f 秒経過", seconds); //1970/01/01 から 1456894708 秒経過

しかし、「これで充分」という訳には、なかなか行きません。

2 日付関連クラス

日付データを保持するのは、あくまでNSDateにお任せできますが、「適切な書式で表示したい」とか「今月とか来年などの概念を扱いたい」などの各種の要求に答えるため、次の2つのクラスが用意されています。

  • NSDateFormatter // 文字列フォーマット関連
  • NSDateComponent // 年・月・日などを個別に扱う

そして、これらの日付関連クラスを扱う際に注意しなければならないプロパティが、次の3つです。

  • NSTimeZone // タイムゾーン(時差・サマータイム)
  • NSLocale // ローケル(書式 24時間表示)
  • NSCalender // 歴法(プロパティとしてNSTimeZoneとNSLocaleも保持している)

(1) NSDateFormatter

NSDateとの関係は、概ね次のようなイメージです。

  • NSDate = NSFormatter + 日付文字列
  • 日付文字列 = NSFormatter + NSDate

そして、NSDateFormatterは、プロパティとして、NSLocaleNSTimeZoneを保持しています。

(2) NSDateComponent

NSDateとの関係は、概ね次のようなイメージです。

  • NSDateComponent = NSCalendar + NSDate
  • NSDate = NSDateComponent + NSColodar

使用時にNSCalender(歴法+NSTimeZone+NSLocale)が必要です。

3 曜日がおかしい

最初に、間違った例を示してみます。

文字列として「2016/03/02」でNSDateを初期化して、曜日まで表示しています。

// "2016/03/02”からNSDateを作成する
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy/MM/dd"];
NSDate *date = [formatter dateFromString:@"2016/03/02"];

NSLog(@"date = %@",date); 

// NSDateComponentsで年・月・日・曜日を取得する
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *components = [calendar components: NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitWeekday fromDate:date];
NSArray *weekList = @[@"日曜",@"月曜",@"火曜",@"水曜",@"木曜",@"金曜",@"土曜"];

NSLog(@"%ld年%ld月%ld日(%@)", components.year,components.month,components.day,weekList[components.weekday-1]);

NSLog(@"%@", [date descriptionWithLocale:[NSLocale currentLocale]]);

出力は、次の通りです。 NSDateが9時間前で初期化され正しく表示されています。

date = 2016-03-01 15:00:00 +0000
2016年3月2日(水曜)
2016年3月2日水曜日 0時00分00秒 日本標準時

しかし、端末の設定で「OSの設定」>「一般」>「言語と地域」>「暦」を「西暦(グレゴリオ歴)から「和暦」に変更して再度実行すると出力は次のようになってしまいます。

date = 4004-03-01 15:00:00 +0000
2016年3月2日(火曜)
平成2016年3月2日火曜日 0時00分00秒 日本標準時

2行目を見ると、2016/03/02は水曜日なのに、火曜日になってしまっています。

原因は、前後の行を見れば一目瞭然です。

NSDateFormatterlocaleプロパティの暦法が「和暦」となっているところに、「2016年」と初期化したので、平成2016年と解釈され、西暦だと4004年で初期化したことになります。 2行目で「2016年3月2日」となっていたのは、実は「平成2016年3月2日」という事だったのですね。

この辺が、問題を見つけにくくする原因のような気がします。

(ちなみに、西暦4004年の3月2日は火曜日らしい・・・)

004

問題を修正するには、次のようにNSDateFormatterlocaleプロパティに対して、これから使用する日付文字列は、西暦表記であることを設定しておくことだけです。

NSDateFormatterlocaleプロパティは、デフォルトで、システムの設定値を使用するため、明確に設定していなかったのが問題だったのです。

NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
// formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; // この行を追加
formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; // この行を追加

[2016.03.03 追記]
en_USではなく、en_US_POSIX使うべきだとご指摘いただきました。詳しくは、コメント欄をご参照ください。

修正後の出力は、次のようになります。

date = 2016-03-01 15:00:00 +0000
28年3月2日(水曜)
平成28年3月2日水曜日 0時00分00秒 日本標準時

4 意識するべき3つのプロパティ

先の処理で、注意するべきところは、次の点でした。

  • NSDateFormatterのLocaleプロパティ
  • NSDateFormatterのTimeZoneプロパティ
  • NSCalendarの暦法
  • NSCalendarのLocaleプロパティ
  • NSCalendarTimeZoneプロパティ

そして、問題だったのは、そのうちの1点です。

  • NSDateFormatterのLocaleプロパティ

もし、意識していなかったとしたら、その他のプロパティについては、「たまたま、意図したものになっていた」だけです。

OSの設定に依存しない定型的な表示などでは、常に「タイムゾーン」・「ローケル」・「暦法」に注意が必要です。

以下、目的のオブジェクトを得るための例です。

(1) タイムゾーン (NSTimeZone)

  • [NSTimeZone defaultTimeZone]
  • [NSTimeZone systemTimeZone]
  • [NSTimeZone localTimeZone]
  • [NSTimeZone Class Reference]

NSTimeZone Class Reference

(2) ロケール (NSLocale)

  • [NSLocale currentLocale] // 現在の設定
  • [NSLocale systemLocale] // 端末デフォルト設定
  • [NSLocale autoUpdatingCurrentLocale] // 現在の設定
  • [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]//指定(英語表記)

NSLocale Class Reference

(3) カレンダー (NSCalendar)

  • [NSCalendar currentCalendar] // 現在の設定
  • [NSCalendar autoupdatingCurrentCalendar]
  • [[NSCalendar alloc]initWithCalendarIdentifier:NSGregorianCalendar] // 西暦
  • [[NSCalendar alloc]initWithCalendarIdentifier:NSCalendarIdentifierGregorian];

NSCalendar Class Reference

5 最後に

日付を定型的に扱う場合、currentLocalecurrentCalendarは、どう考えても勝負ですね。