1904年になりました(dayjsでの年入力の話)

この記事ははてなエンジニア Advent Calendar 2023 - Hatena Developer Blogの1/1の記事です。


「2024」と打ち込んだはずなのに……

1904年になってしまいました。今年もよろしくお願いします。id:nakatakiです。

辰年生まれなので年男…と言いたいところですが、どうやら年表示が壊れてしまっているようです。この原因はdayjsの仕様の穴にありました。一緒に原因を探してみましょう。

以下のようなコンポーネントがありました。(重要な部分だけをまとめた仮のものです)

const DateInput: React.FC = () => {
  const [selectedDate, setSelectedDate] = useState("");

  const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const inputDate = event.target.value;
    const parsedDate = dayjs(inputDate, "YYYY-MM-DD");
    if (parsedDate.isValid()) {
      // 本来は色々処理する
      setSelectedDate(parsedDate.format("YYYY-MM-DD"));
    } else {
      // invalidなら今日の日付にしておく
      setSelectedDate(dayjs().format("YYYY-MM-DD"));
    }
  };

  return (
    <input
      type="date"
      max="9999-12-31"
      value={selectedDate}
      onChange={onChange}
    />
  );
};

これは、dayjsという日時処理ライブラリを利用した、年月日を入力するコンポーネントです。似た機能を持つMoment.jsが新規開発を停止したので、乗り換える…という方も多いと思います。 このような表示になります。

ここでは、様々な事情により、dayjsによるバリデーションを経由したデータをもう一度inputに戻しています。

この状態で一度日付を選択し、キーボードで試しに「1234」と打ち込んでみると…

なんだこれ!

「1901年→1902年→1903年→1904年」と画面が遷移してしまっています。何を入力してもこの調子。 だから「2024」を入力しても「1904」になってしまったんですね。

原因

dayjsのコーナーケース

こうなっている原因は、現在のdayjsのコーナーケースにあります。 こちらをご覧ください。(https://jstool.gitlab.io/dayjs/ を使用しています)

この画像が示すように、現在のdayjsは、'0001-01-01'から'0099-12-31'までの文字列を1900年代として解釈します。

JSのDateのコンストラクタで、Yに2桁の数を渡すと1900年台として解釈されるのはそこそこ有名かと思いますが、dayjsの内部でこれを呼び出しているため同様の現象が起きているようでした。

Issueにもなっていますが、現在は仕様として扱われているようです。

Dateとは違って"YYYY-MM-DD" で渡しているんだから1900年代じゃないのは明白じゃないか、とは思うのですが…。

仕組み

仕様がわかればあとは簡単です。先ほどの「1234」の例で、なぜこんな挙動になったか順を追って考えてみます。

  1. キーボードで「1」を押した時点で、inputの中身は'0001-01-01'になっています。
  2. dayjsはこれを1900年1月1日と解釈し、seledtedDateに1900-01-01がセットされます。
  3. inputの中身が'1901-01-01'に置き換わってしまうため、フォーカスが下1桁に戻ります。
  4. 次にキーボードで「2」を押すと、inputの中身は'0002-01-01'になります。(2に戻る)

なるほどこれでは、いつまで経っても2024が入力できませんね。

解決

dayjsにStringを渡していてはきりがありません。一旦Date.parseを使ってエポック秒にしてから渡しましょう。

const parsedDate = dayjs(Date.parse(inputDate));

すると…

正常に動作し、無事に2024年になることができました! これで干支も辰年、晴れて年男に…ってあれ?

1904年も辰年だったのか。僕は2000年生まれだから…マイナス96歳って年男になりますか?

まとめ

  • input type="date"の中身をdayjsで逐一パースしていると、キーボード入力がおかしくなるぞ(Moment.jsはこうはならない)
  • dayjsに'0001'年から'0099'年を扱って欲しい時は、StringではなくEpochで渡すと良さそう