【小ネタ】カレンダーの月を自作の進む・戻るボタンで制御したい（ft. Nextjs）

# 1. 概要
ちょっとした小ネタなので短めに記事をまとめます。

この記事では、カレンダーの月を矢印のボタンによって制御できるようにするコードを紹介します。
htmlのinputタグにtype="date"属性を与えたカレンダーは、日付を変更するためにいちいちカレンダーを開かなければ月を変更するボタンが現れません。
日付に関心を持たず、ユーザーが設定する月の情報だけに関心がある場合、ユーザー目線でも月を変更するために二度手間になるのは好ましくないでしょう。
これに対し、もし自作の戻る・進むボタンをカレンダーの隣に配置できれば、カレンダーを開くことなく月を変えられるので手間が一つ減ります。
ちょっとした工夫ですが、今回実装できたので忘備録も兼ねて共有しようと思います。

# 2. ボタンによる制御
具体的なコードは以下の通りです。
なお、calendarDateのフォーマットはハイフン繋がりになっています。これはinputタグにdateを値として渡すときにその形式でないと受け付けてくれないためです。

```typescript
"use client";

import { Box, HStack,  Input } from "@chakra-ui/react";
import { ChangeEvent, useState } from "react";
import { IoIosArrowForward, IoIosArrowBack } from "react-icons/io";

export default function Calendar({ initialDate }: { initialDate:string }){
  const [calendarDate, setCalendarDate] = useState<string>(initialDate);
  
  function goBack(){
    const date = new Date(calendarDate);
    const lastMonthDays = new Date(date.getFullYear(), date.getMonth(), 0).getDate();

    if(date.getDate() > lastMonthDays){
      date.setDate(lastMonthDays);
    }

    date.setMonth(date.getMonth() - 1);
    setCalendarDate(dateToStr(date));
  }

  function goForth(){
    const date = new Date(calendarDate);
    const nextMonthDays = new Date(date.getFullYear(), date.getMonth()+2, 0).getDate();
  
    if(date.getDate() > nextMonthDays){
      date.setDate(nextMonthDays);
    }

    date.setMonth(date.getMonth() + 1);
    setCalendarDate(dateToStr(date));
  }

  function handleChange(e: ChangeEvent<HTMLInputElement>){
    const date = e.target.value;
    setCalendarDate(date);
  }

  return(
    <HStack p={2}>
      <Box _hover={{ cursor: "pointer" }} >
        <IoIosArrowBack color="cyan.800" onClick={goBack} />
      </Box>
      <Input type="date" size="xs" onChange={handleChange} value={calendarDate}  />
      <Box _hover={{ cursor: "pointer" }}>
        <IoIosArrowForward color="cyan.800" onClick={goForth} />
      </Box>
    </HStack>
  );
}

export function dateToStr(date:Date){
  return date.toLocaleString('lt', {dateStyle:'short'});
}

```

## 2-1．前の月に戻る
前の月に戻るコードは以下の通りです。

```typescript
  function goBack(){
    const date = new Date(calendarDate);
    const lastMonthDays = new Date(date.getFullYear(), date.getMonth(), 0).getDate();

    if(date.getDate() > lastMonthDays){
      date.setDate(lastMonthDays);
    }

    date.setMonth(date.getMonth() - 1);
    setCalendarDate(dateToStr(date));
  }
```
dateオブジェクトは、setMonthメソッドによって月を変更することができます。もし月が0より小さい値になったらメソッドがよしなに前年の12月（monthの値としては11）に設定してくれます。しかし厄介なのが月末の日数が異なる場合です。もし日にちがその月の日数よりも多い場合、dateオブジェクトは翌月に設定しなおしてくれますが、これは今回望んでいる挙動ではありません。
このため、`const lastMonthDays = new Date(date.getFullYear(), date.getMonth(), 0).getDate();`として、先月末の日にち、即ち先月の日数を取得しています。日にちの部分に0を設定すると先月末の日付になります。これもちょっとした小ネタですね。
これによってlastMonthDaysをもとめ、これよりも現在の日付が大きい場合は先月末の日付になおして月を1引いています。

dateオブジェクトは、月が0から始まり11で終わるという直感的じゃない挙動があるため、数値を直接new Date()インスタンスに渡すときは注意が必要です。
おすすめとしては、数値を直接渡すのではなくて、dateオブジェクトのgetMonth()メソッドを用いて渡す方がいいと思います。1から12までの実際の月の数え方と0-11のdateオブジェクトの仕様の違いに悩まなくていいためです。

## 2-2. 次の月に行く
次の月に行く具体的なコードは以下の通りです。

```typescript
 function goForth(){
    const date = new Date(calendarDate);
    const nextMonthDays = new Date(date.getFullYear(), date.getMonth()+2, 0).getDate();
  
    if(date.getDate() > nextMonthDays){
      date.setDate(nextMonthDays);
    }

    date.setMonth(date.getMonth() + 1);
    setCalendarDate(dateToStr(date));
  }
```
前の月に行くコードをいわば逆にしているだけなのでそこまで難しくないと思います。
一つ注意なのは、date.getMonth()+2にしているところでしょうか。Dateインスタンスに日付として0を渡すと先月末の日付になるので、+2してあげることで翌月末の日数が取得できます。
月を加算する場合でも、減算する場合の時のようにdateオブジェクトはよしなにやってくれます。11を超えた場合は翌年の0から始めてくれるわけですね。

## 2-3. dateから文字列の変換
ハイフン繋がりの文字列の形にするための直接的なメソッドは提供されていませんが、~~toISOString()の返り値を操作することで目的のフォーマットの日付が手に入ります。~~
具体的には以下のおとりです。

:::note
onjhthrw368さんのご指摘により、一部コードを修正しました。
toISOString()はUTC時刻を返すため、時間帯によっては日付が跨ってしまう可能性があります。
文字列に変数として年と月を埋め込むか、以下のようにするとよいようです。

```typescript
export function dateToStr(date:Date){
  return date.toLocaleString('lt', {dateStyle:'short'});
}
```

:::

以上が月を変更する処理に関する内容でした。

# 3. おわりに
JavaScriptのDateの挙動はちょっとなれないところも多いですが、慣れたら使い勝手は良さそうだと思いました。
ここらへんで触れた中身は後でも使いそうなので、自分でもこの記事にまとめたことを忘れずに置こうと思います。