JavaScriptの標準ライブラリであるDateオブジェクトの悪名高さは古今東西で有名であり、今更クソ仕様が一つ見つかった程度でなんだ、といった感想をお持ちの方も多いと思うが私はこれでハマったのでこの記事に記す。
クソ仕様
const d = new Date();
d.setMonth(3);
console.log(d.getMonth())
さて諸君、上記プログラムにおいて3行目で出力される数字はなんであろうか?正解は「これだけからは分からない」である。
より具体的に言うと3の場合と4の場合があり得る。3の場合はいいとして4の場合、これはこのプログラムを実行した日付が例えば5月31日だった場合である。new Date()は現在の日時で初期化されるので、dは現在の日付5月31日が入っている。これに対してd.setMonth(3)を実行すると4月31日になる(別の有名なクソ仕様、「月の数は1ずれる」を忘れてはならない)が、4月は30日までなので繰り上がり5月1日になるのだ。
別の見方
このアホな仕様は別の見方をすることもできる。
const d = new Date(2022, 0, 31);
d.setMonth(8);
console.log(d); // 10月1日
d.setMonth(8);
console.log(d); // 9月1日
全く同じ文d.setMonth(8);を2回実行しているが、各文実行後のdの値は同じか?答えは「同じではない」だ。
前セクションと同様に1回目のsetMonth後では「9月31日」が繰り上がってdは10月1日になる。その後もう一度d.setMonth(8)を行うと9月1日になるので、いかにもただのsetterっぽい名前の同じ関数を同じ引数で呼び出しているにもかかわらず、1回目と2回目の時点で値が異なる。
この仕様の凶悪な点
この仕様が凶悪なのは、これを元にしたバグに気づきにくいことである。この仕様はsetMonthの引数の月が2月, 4月, 6月, 9月, 11月の場合かつ、元の日付が31日(引数が2月の場合は29日〜31日)の場合のみしか発現しない。実際私は数年間この仕様によるバグを含んだままのコードを知らずに使っていた(以下のような感じ)。
function f(d: Date, month: number, day: number) {
d.setMonth(month);
d.setDate(day);
}
感想
私は最初にこの仕様が原因のバグに当たったとき、これはブラウザ側のバグに違いないと思った。月の値がおかしいのでブラウザのデバッガのコンソールでsetMonthをもう一度実行すると値が変わるのだ。どうしてこれが正しい仕様だと想像できようか。
宣伝
この仕様に当たったのは、私の書いているJavaScriptの日付時刻ライブラリ@silane/datetime-jsの開発中である。JavaScript標準のDateオブジェクトはこのようにクソすぎるので、私が良い設計だと思っているPythonの標準ライブラリを模倣するように作っている。しかし内部では日付時刻の計算にDateオブジェクトを使っているのでそこでバグが発生していた。テストは書いていたが発現条件が厳しいので漏れていたということですね。