【豆記事】日付の配列から休・祝日を判定して真偽値の配列を返す

# 1. 概要
休みの日か平日かどうかを判定するような機能がプロジェクトを作っていく中で必要になったので、忘備録も兼ねて作ったものを紹介しようと思います。

ただ、今回の仕組みは外部APIありきなので、このAPIの仕様が変わったりサービスが終了したらうまく機能しなくなります。業務用で堅牢さが求められるような場面においてはそぐわないかもしれないことをご承知おきください。

# 2. 祝日の取得
祝日の判定は少々面倒だと思います。年によって若干異なる可能性がありますし、日曜日にかぶったら月曜日が振替休日になってしまうので、この処理をするコードも必要です。
ということで、この部分の処理だけでも結構大変なのでこれは外部APIに頼ることにしました。今回使っていくのは[holidays-jp](https://holidays-jp.github.io/)というサイトが提供してくださっているAPIです。

早速コードの中身を見ていきましょう。

```typescript
function isObj(value:unknown):value is Object{
  return value != null && typeof value === "object";
}

export async function getHolidaysOf(year: string):Promise<string[]>{
  const uri = `https://holidays-jp.github.io/api/v1/${year}/date.json`

  try{
    const res = await fetch(uri);
    const holidays = await res.json();
    if(!isObj(holidays)) throw new Error("fetch error");

    return Object.keys(holidays);

  }catch(err){
    console.log(err);
    throw err
  }  
}
```
`https://holidays-jp.github.io/api/v1/${year}/date.json`というuriを指定してjsonを取得します。dateとdatetimeの二つの表示形式があるのですが、この二つはキーの違いになります。値はどちらも祝日の名称を示しており、dateはハイフン形式のキーになっています。
今回は何日が祝日なのかだけに興味があるので、キーだけを返すようにしています。
外部APIを使えばこれだけで祝日の配列を取得できるのでとても便利ですね。

これだけのコードですが関数にした理由は一応あります。
祝日の一覧の取得コードはgetHolidaysOfという関数でまとめ、具体的な処理は次に紹介するカスタムフックからは切り出してあります。このため、このAPIサービスの提供が終了したとしても引数と返り値に注意しつつこの関数内のコードを変更すれば、他のコードに予期せぬ影響を及ぼすことなく改修することができます。

# 3. 休・祝日を判定する
次に、休日と祝日をまとめて判定するコードを紹介します。
```tsx
import { getHolidaysOf } from "@/libs/holidays";
import { useEffect, useState } from "react";

type Holidays = {
  [key: string]: string[];
}

export function useHolidayFlags(dates:string[]){
  const [holidayFlags, setHolidayFlags] = useState<Array<boolean>>(Array(dates.length).fill(false));
  const [holidays, setHolidays] = useState<Holidays>({});
  const [err, setErr] = useState("");

  const yearSet = new Set(dates.map(date => date.trim().split("/")[0]));

  useEffect(() => {
    yearSet.reduce((p,year) => p.then(
      () => getHolidaysOf(year)
         .then(holidays => setHolidays(prev => ({...prev, [year]: holidays})) )
         .catch(err => err instanceof Error ? setErr(err.message) : setErr("something went wrong!"))
      ), Promise.resolve()
   );

    },[]);

  useEffect(() => {
    const hyphenDates = dates.map(date => date.replaceAll("/","-"));
    const updatedFlags = hyphenDates.map(hyphenDate => {
      const date = new Date(hyphenDate);
      const day = date.getDay();
      const isWeekend = day === 0 || day == 6;
  
      const year = date.getFullYear().toString();
      const isHoliday = holidays[year]?.includes(hyphenDate);

      return isWeekend || isHoliday;
      
    });
    setHolidayFlags(updatedFlags);

  }, [holidays]);

  return{
    holidayFlags,
    err
  }
}
```
datesの文字列配列においては、年が異なることもあるかもしれません。全ての年度の祝日を集めるために、以下のコードで重複のない年の集合を作っています。
`const yearSet = new Set(dates.map(date => date.trim().split("/")[0]))`
trimは左右の空文字を取り除くためのメソッドです。空文字が入っていると、リストの0番目の値が空文字になってしまいエラーになってしまいます。

<br />

次に、yearSetを~~forEachで回すことによって祝日を順番に取得しています~~。
reduceで逐次的に処理するようにしています。
年をキーにして、その年の祝日の配列を値とするオブジェクトを作っています。
変数をキーとして埋め込むためにはブランケットを使わないといけないのが意外に忘れがちな大事なところです。
```typescript
yearSet.reduce((p,year) => p.then(
      () => getHolidaysOf(year)
         .then(holidays => setHolidays(prev => ({...prev, [year]: holidays})) )
         .catch(err => err instanceof Error ? setErr(err.message) : setErr("something went wrong!"))
      ), Promise.resolve()
   );

```
:::note
APIの逐次的な処理
forEachの処理ではAPIをほぼ同時に実行するために負荷が高くなるという指摘を受け、当該コードを修正させていただきました。
:::


<br />

そして最後に、其々の日付が休・祝日かどうかを判定するコードを紹介して終わります。
```typescript
const hyphenDates = dates.map(date => date.replaceAll("/","-"));
const updatedFlags = hyphenDates.map(hyphenDate => {
    const date = new Date(hyphenDate);
    const day = date.getDay();
    const isWeekend = day === 0 || day == 6;
  
    const year = date.getFullYear().toString();
    const isHoliday = holidays[year]?.includes(hyphenDate);

      return isWeekend || isHoliday;
      
    });
setHolidayFlags(updatedFlags);
```
曜日は0始まりで0が日曜なので、dayが0と6のときに休日ということになります。
また、`holidays[year]?.includes(hyphenDate)`の部分はholidaysオブジェクトにその日付の年でアクセスし、その中の祝日のリストに含まれているかを判定しています。

これにより、休・祝日をtrueとして、平日をfalseとする配列を返すことができました。

# 4. おわりに
休日かどうかを判定するものが組み込みであれば便利なのになぁと思います。せめて`date.isWeekends()`とか`date.isWeekdays()`とかあればちょっとは楽になるんですけどね。