はじめに
サマータイムが話題になっているのでそれにあやかります
実は過去に日本でもサマータイムが実施されていた時期があり、それが元でiOSアプリに不具合が発生したことがあります。
今回はその原因と対応策を紹介します。
注意
PGはObj-Cで書いています。
できる限りObj-Cを知らなくてもわかるように書いていますが、もしわかりづらい箇所がありましたらすみません
不具合内容
iOS 10以降でJST(日本標準時)の「1951/05/06」が文字列から日付型に変換できません。
「1951/05/05」や「1951/05/07」は変換できます。
PG
Obj-Cは文字列→日付型への変換方法が多少複雑です。
NSDateFormatter
という日付フォーマットのオブジェクトを生成し、そのプロパティをいろいろ(タイムゾーンやロケール、フォーマットなど)設定して dateFromString:
メソッドで変換します。
// 日付フォーマットを生成する
NSDateFormatter *dateFormatter = [NSDateFormatter new];
dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"JST"]; // 日本標準時
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"ja_JP"]; // 「YYYY-MM-DD」形式
dateFormatter.dateFormat = @"yyyy/MM/dd"; // 文字列のフォーマット
// 文字列→日付型へ変換する
NSDate *date0505 = [dateFormatter dateFromString:@"1951/05/05"];
NSDate *date0506 = [dateFormatter dateFromString:@"1951/05/06"];
NSDate *date0507 = [dateFormatter dateFromString:@"1951/05/07"];
// ログを出力する
NSLog(@"%@, %@, %@", date0505, date0506, date0507);
出力結果
結果はUTC(協定世界時, +0000と書く)で出力されます。
変換時に時間を省略すると「00:00:00」となり、JST(≒日本)はUTC(≒イギリス)より9時間進んでいる(+0900と書く)ため、通常なら「yyyy-MM-dd 15:00:00 +0000」となります。
1951-05-04 15:00:00 +0000, 1951-05-05 15:00:00 +0000, 1951-05-06 14:00:00 +0000
1951-05-04 15:00:00 +0000, (null), 1951-05-06 14:00:00 +0000
!?
正直最初にこれを見たときは意味がわかりませんでした。
ツッコミどころが多過ぎます。
iOS 9以前と10以降で「1951/05/06」の変換結果が異なるのも意味がわからないですし、「1951/05/07」はなぜか時差が9時間でなく10時間になっていますし。
このときは日本にサマータイムがあったことなど知らなかったので、「1951/05/06 何の日」でググったりしましたが何も引っかからず、原因を特定するのに時間がかかりました。
原因
もう想像がつくと思いますが、「1951/05/06」がサマータイムの開始日のためでした。
日本におけるサマータイムについてはWikipediaに詳しく書いてあります。
https://ja.wikipedia.org/wiki/夏時間#日本における夏時間
Wikipediaによると日本でサマータイムが実施されたのは1948年から1951年の4シーズンのみということなので、実際にサマータイムの開始日にあたる他の3日も変換できないか検証してみます。
PG
NSDateFormatter *dateFormatter = [NSDateFormatter new];
dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"JST"];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"ja_JP"];
dateFormatter.dateFormat = @"yyyy/MM/dd";
// サマータイムの開始日を文字列→日付型へ変換する
NSDate *date1948 = [dateFormatter dateFromString:@"1948/05/02"];
NSDate *date1949 = [dateFormatter dateFromString:@"1949/04/03"];
NSDate *date1950 = [dateFormatter dateFromString:@"1950/05/07"];
NSLog(@"%@, %@, %@", date1948, date1949, date1950);
出力結果
(null), (null), (null)
変換できませんでした
やはりサマータイムの開始日に問題があるようです。
変換できない理由ですが、おそらく「1951/05/06 00:00:00 +0900」という日時が存在しないためだと思います。
それを証明するため、「1951/05/05 23:59:59」の1秒後が「1951/05/06 01:00:00」になるかどうか検証します。
PG
NSDateFormatter *dateFormatter = [NSDateFormatter new];
dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"JST"];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"ja_JP"];
dateFormatter.dateFormat = @"yyyy/MM/dd HH:mm:ss";
NSDate *date = [dateFormatter dateFromString:@"1951/05/05 23:59:59"];
NSDate *afterDate = [date dateByAddingTimeInterval:1.f]; // `date` の1秒後
// 日付型を同じ日付フォーマット(タイムゾーンがJST)を使って文字列に戻すとJSTで出力される
NSLog(@"%@, %@", [dateFormatter stringFromDate:date], [dateFormatter stringFromDate:afterDate]);
出力結果
1951/05/05 23:59:59, 1951/05/06 01:00:00
BINGO!!
やはり「1951/05/06 00:00:00 +0900」はこの世に存在しない日付のようです。
ここまで日本のサマータイムを忠実に再現してくれていることを嬉しく思うことにしましょう。
つまり、 iOS 9以前で変換できていたのがバグ です。
それがiOS 10で修正されたということです。
対応
①タイムゾーンをJST→UTCにする
タイムゾーンをJSTでなくUTCに指定すれば問題なく変換できるようになります。
PG
NSDateFormatter *dateFormatter = [NSDateFormatter new];
- dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"JST"];
+ dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; // 協定世界時
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"ja_JP"];
dateFormatter.dateFormat = @"yyyy/MM/dd";
NSDate *date0505 = [dateFormatter dateFromString:@"1951/05/05"];
NSDate *date0506 = [dateFormatter dateFromString:@"1951/05/06"];
NSDate *date0507 = [dateFormatter dateFromString:@"1951/05/07"];
NSLog(@"%@, %@, %@", date0505, date0506, date0507);
出力結果
1951-05-05 00:00:00 +0000, 1951-05-06 00:00:00 +0000, 1951-05-07 00:00:00 +0000
UTCで変換したため、文字列そのままの日時になっています。
この対応方法だと最初のPGと結果が異なるので注意です。
また、UTCにもサマータイムがあるので、JSTと同様に変換できない日時があるかもしれません。
今回はそこまでは検証していません。
②「yyyy/MM/dd 01:00:00」で変換する
力技ですが、文字列で時間まで指定してから変換します。
PG
NSDateFormatter *dateFormatter = [NSDateFormatter new];
dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"JST"];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"ja_JP"];
- dateFormatter.dateFormat = @"yyyy/MM/dd";
- NSDate *date0505 = [dateFormatter dateFromString:@"1951/05/05"];
- NSDate *date0506 = [dateFormatter dateFromString:@"1951/05/06"];
- NSDate *date0507 = [dateFormatter dateFromString:@"1951/05/07"];
+ dateFormatter.dateFormat = @"yyyy/MM/dd HH:mm:ss";
+ NSDate *date0505 = [dateFormatter dateFromString:@"1951/05/05 00:00:00"];
+ NSDate *date0506 = [dateFormatter dateFromString:@"1951/05/06 01:00:00"];
+ NSDate *date0507 = [dateFormatter dateFromString:@"1951/05/07 00:00:00"];
NSLog(@"%@, %@, %@", date0505, date0506, date0507);
出力結果
1951-05-04 15:00:00 +0000, 1951-05-05 15:00:00 +0000, 1951-05-06 14:00:00 +0000
最初のPGのiOS 9以前と同様の結果になりました。
この年代を扱うときは生年月日など時間まで必要としないことが多いので、このような力技でも問題ないことが多いです。
おわりに
ここまでわかれば2020年にサマータイムが実施されても怖くありませんね!
その前に年号対応がありますが