Programming Journal

学習したことの整理用です。

zodでうるう年判定(有効日付)のバリデーション

概要

日付を登録するフォームで、有効日付(うるう年など)かどうかを判定したい。 年月日が分かれている選択フォームの場合、日付のバリデーションで迷ったのでメモ

環境

react 18.2.0
date-fns 2.29.3
react-hook-form 7.43.9
zod 3.21.4

簡易なフォームをつくる

年・月・日が分かれているフォーム

フォーム

import "./styles.css";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { isValid } from "date-fns";

type FormData = {
  registerYear: string;
  registerMonth: string;
  registerDay: string;
};

const schema = z
  .object({
    registerYear: z.string().nonempty(),
    registerMonth: z.string().nonempty(),
    registerDay: z.string().nonempty()
  });

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm<FormData>({
    resolver: zodResolver(schema)
  });

  const onSubmit = handleSubmit((data) => console.log(data));

  return (
    <form onSubmit={onSubmit}>
      <div id="birthday">登録日</div>
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          marginBottom: "10px"
        }}
      >
        <div
          style={{
            marginBottom: "20px"
          }}
        >
          <input
            id="yearInput"
            type="number"
            min="2023"
            max="2040"
            aria-labelledby="birthday yearLabel"
            {...register("registerYear")}
          />
          <label id="yearLabel" htmlFor="yearInput">
            年
          </label>
          <input
            id="monthInput"
            type="number"
            min="1"
            max="12"
            aria-labelledby="birthday monthLabel"
            {...register("registerMonth")}
          />
          <label id="monthLabel" htmlFor="monthInput">
            月
          </label>
          <input
            id="dayInput"
            type="number"
            min="1"
            max="31"
            aria-labelledby="birthday dayLabel"
            {...register("registerDay")}
          />
          <label id="dayLabel" htmlFor="dayInput">
            日
          </label>
        </div>
        {errors.registerYear && <p>{errors.registerYear.message}</p>}
        <button
          id="savebutton"
          aria-labelledby="birthday savebutton"
          type="submit"
          style={{ width: "100px" }}
        >
          保存する
        </button>
      </div>
    </form>
  );
}

うるう年の判定をしたい

date-fnsisValidという一見、有効日付の判定を行えそうなメソッドがあるものの、うるう年、31日が存在しない日付の判定ができない。32日などどんな月でも有効でない日付はfalseになる。

ここでは、有効日付を確認するために、Dateオブジェクトを生成して確認する。

例:指定された日付が存在しない場合、JavaScriptのDateは以下のようになる

console.log(new Date("2023-09-30")); // Sat Sep 30 2023 09:00:00 GMT+0900 (Japan Standard Time)
console.log(new Date("2023-09-31")); // Sun Oct 01 2023 09:00:00 GMT+0900 (Japan Standard Time)

"2023-09-30"は有効な日付なのでそのまま解釈する
"2023-09-31"は存在しない日付だが、自動的に日付を"2023-10-01"に修正する

有効な日付でない場合、Dateオブジェクトが自動的に有効な日付に変換するので、フォームで選択した値でDateオブジェクトを生成して、選択した値と一致するかどうかで有効日付か否かを判定する。

// 追加
const getSelectedDate = ({
  registerYear,
  registerMonth,
  registerDay
}: FormData) => {
  return new Date(
    Number(registerYear),
    Number(registerMonth) - 1, // Dateオブジェクトは月の値が0から始まるので、選択された値から1を引く
    Number(registerDay)
  );
};

const schema = z
  .object({
    registerYear: z.string().nonempty(),
    registerMonth: z.string().nonempty(),
    registerDay: z.string().nonempty()
  })
  .refine(
    (data) => {
      const selectedDate = getSelectedDate(data);
     
      return (
        isValid(selectedDate) && // 無効な日付かチェック
 // 選択した年月日がDateオブジェクトによる無効値の自動修正によって変更されていないことを確認
        selectedDate.getFullYear() === Number(data.registerYear) &&
        selectedDate.getMonth() + 1 === Number(data.registerMonth) &&
        selectedDate.getDate() === Number(data.registerDay)
      );
    },
    { message: "存在しない日付です", path: ["registerYear"] }
  )

参考にした記事

JavaScript day.js, date-fns で実在する日付かどうか判定したい - かもメモ 【JavaScript】 うるう年判定の一番簡単な方法