🕰️

iOS 15 と 16 では new Date() でパースできる文字列に違いがある!

2023/09/19に公開

ラブグラフの横江です。

ラブグラフにはラブグラファー(カメラマン)さんに不具合を教えてもらうチャンネルがあるのですが、
そこで、 オーストラリアからだと日付表示がズレてしまう という相談がありました。

オーストラリアからだと日付がズレるワケ

そこの処理には、 Day.js および TimeZoneプラグイン を用いた
以下のような実装がされていました。

const dateStr = params['date']; // "2020-01-23" のような文字列が渡される
const date = dayjs(dateStr).tz('Asia/Tokyo').startOf('day');

dayjs(dateStr) の実行結果は日本でおこなうと
2020/01/23 00:00:00 GMT+0900

シドニーでおこなうと
2020/01/23 00:00:00 GMT+1100
となります。(※10月〜4月はサマータイムの関係で2時間の時差)

tz('Asia/Tokyo') では、この時間を保ったまま日本時刻に合うように変換されるので、
シドニーからだと
2020/01/22 22:00:00 GMT+0900
と変換されてしまうのです。 これが、1日ズレが起こる仕組みです。

日本より時差が進んでいる国というのはあまりないので、
ラブグラファーさんが海外に行くことはよくあっても今までこの問題は明るみに出ていなかったようです。

修正方法

さて、実はこれ、変な修正方法があります。

渡される文字列をいったん new Date() の中に入れる方法です。
JavaScript の挙動として、

new Date('2020/01/23');
=> Thu Jan 23 2020 00:00:00 GMT+1100 (Australian Eastern Daylight Time)

new Date('2020-01-23');
=> Thu Jan 23 2020 11:00:00 GMT+1100 (Australian Eastern Daylight Time)

のように、ハイフンつなぎで入れるとグリニッジ標準時の0時を基準として入れてくれる動きがあるので、

const dateObj = new Date(params['date']); // "2020-01-23" のような文字列が渡される
const date = dayjs(dateObj).tz('Asia/Tokyo').startOf('day');

とすることで解消することも出来ます。

ただ、これはあまりにもバギーです。
どこかのタイミングで渡される文字列が 2020/01/23 の形式に変更されてしまったとしたら、また同じバグが起こります。

というわけで、 https://day.js.org/docs/en/parse/string を参考に、

dayjs('2018-04-13 19:18:17.040+02:00');

のようにタイムゾーン付きの文字列を渡すことで解消しようとしました。

iOS 15 でだけ機能しない!

const dateStr = params['date']; // "2020-01-23" のような文字列が渡される
const date = dayjs(`${dateStr} 00:00:00+09:00`).tz('Asia/Tokyo').startOf('day');

このように直すことで、Chrome でも Safari でも動き、問題は解消されたかに見えました。

しかし、別のラブグラファーさんから連絡が!

「画面が上手く動かなくなりました!」

使っている iOS のバージョンをうかがうと、最新の16系でなく15系だそうで、
実際に iOS Simulator で動かしてみると、たしかに正常に機能しません。いったい……?

console.log(dayjs('2020-01-01 12:00:00+06:00'));

これのコードの結果はいわずもがな。

しかし、 iOS 15.x で同じコードを実行するとこうなります。

Invalid Date 😱

dayjs 固有の問題ではない

実はこれ、dayjs だから起こる問題ではありません。
dayjs が文字列からオブジェクトにパースするのは https://github.com/iamkun/dayjs/blob/v1.11.9/src/index.js#L60-L80 の部分なのですが、
どれにも該当しないので最後の return new Date(date) が実行されています。

ここの挙動が iOS 15 と iOS 16 で変わっているようです。
確認してみましょう。

// iOS 15.5
new Date('2020-01-01 12:00:00+06:00');
>> Invalid Date

new Date('2020-01-01T12:00:00+06:00');
>> Wed Jan 01 2020 15:00:00 GMT+0900 (JST)

// iOS 16.4
new Date('2020-01-01 12:00:00+06:00');
>> Wed Jan 01 2020 15:00:00 GMT+0900 (日本標準時)

new Date('2020-01-01T12:00:00+06:00');
>> Wed Jan 01 2020 15:00:00 GMT+0900 (日本標準時)

iOS 16 では、 T の代わりにスペースが許されるようになっていることがわかります。

a space instead of the 'T' is allowed?

dayjs のドキュメントでは

Parse the given string in ISO 8601 format (a space instead of the 'T' is allowed) and return a Day.js object instance.

Tの代わりにスペースでもいいよ、と書かれていました。
(iOS 15 では許されていませんでしたが!)

はたして T の代わりにスペースが認められるのでしょうか?

https://github.com/toml-lang/toml/issues/424

TOMLの仕様を巡っての論争では、ISO 8601 では T を省略することが認められているが、
(※省略が認められているのであって、スペースに置き換えていいとは言っていない)
RFC 3339 では Appendix A を根拠に T をスペースにすることを認めないはずだ、という主張が起きつつ、
でもスペースも認めたほうが見やすいよね、と最終的にプルリクがマージされています。

また、Linux系の date コマンドでは以下の出力結果が見られます。
(※ macOS(BSD系)の date コマンドでは、以下のコマンドを実行できません)

date --iso-8601=seconds
2023-09-11T17:18:52+0000

date --rfc-3339=seconds
2023-09-11 17:18:57+00:00

date --version
date (GNU coreutils) 8.22
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by David MacKenzie.

こちらは RFC 3339 は T をスペースにすることを認めている解釈のようです。

ECMAScript では T の代わりにスペースは認めない

じゃあ肝心の ECMAScript ではどう言っているのかというと、

https://tc39.es/ecma262/#sec-date-time-string-format
こちらを見る限り、 T の代わりにスペースを使っていいよ、とは記載されていません。

また、Mozilla のドキュメントでも

Date コンストラクター(および Date.parse と同等)で日付文字列を解釈する際には、常に入力が ISO 8601 形式 (YYYY-MM-DDTHH:mm:ss.sssZ) であることを確認してください。他の形式で解釈した場合には、その挙動は実装によって定義されていて、すべてのブラウザーで動くとは限りません。 RFC 2822 書式の文字列の対応は慣習的に行われているだけです。

と書かれています。

ECMAScript に従うのなら、

new Date('2020-01-01 12:00:00+06:00');

の書き方は異端であり、これを解釈してくれるのはブラウザ側の優しさに基づいていると言えるでしょう。

じゃあ dayjs のドキュメント間違ってるじゃん!!!!

おしまい🌟

ラブグラフのエンジニアブログ

Discussion