🌻

Next.jsでの営業時間・日数計算のコード例

2024/09/05に公開
2

1. はじめに

この記事では、以下の内容について学ぶことができます

  • date-fns ライブラリと holiday_jp-js ライブラリの設定方法
  • 営業日計算のための様々なコード例とその詳細な解説

使用するライブラリ

  • date-fns: 日付操作のための便利な関数を提供するライブラリ

https://github.com/date-fns/date-fns

  • holiday_jp-js: 日本の祝日データを提供するライブラリ

https://github.com/holiday-jp/holiday_jp-js

2. 営業日計算のコード例

2.1 ライブラリのインストール

まず、必要なライブラリをインストールします。

npm install date-fns @holiday-jp/holiday_jp

2.2 初期設定

app/lib/business-config.tsファイルを作成し、プロジェクト共通の設定と関数を定義します。

app/lib/business-config.ts
import holidays from "@holiday-jp/holiday_jp";
import {
  addDays,
  isBefore,
  isEqual,
  isWithinInterval,
  set,
  startOfDay,
} from "date-fns";

// 営業時間の設定
const BUSINESS_HOURS = {
  start: { hours: 10, minutes: 0 },
  end: { hours: 20, minutes: 0 },
};

// 営業日(月曜から金曜)の設定
const BUSINESS_DAYS = [1, 2, 3, 4, 5];

/**
 * 指定された日時が営業時間内かどうかを判定します。
 * @param date 判定する日時
 * @returns 営業時間内の場合はtrue、それ以外はfalse
 */
export function isBusinessHour(date: Date): boolean {
  const day = date.getDay();
  if (!BUSINESS_DAYS.includes(day)) return false;
  const startTime = set(date, BUSINESS_HOURS.start);
  const endTime = set(date, BUSINESS_HOURS.end);
  return isWithinInterval(date, { start: startTime, end: endTime });
}

/**
 * 指定された日付が祝日かどうかを判定します。
 * @param date 判定する日付
 * @returns 祝日の場合はtrue、それ以外はfalse
 */
export function isHoliday(date: Date): boolean {
  return holidays.isHoliday(date);
}

/**
 * 指定された日付が営業日かどうかを判定します。
 * 営業日は平日(月曜から金曜)かつ祝日でない日を指します。
 * @param date 判定する日付
 * @returns 営業日の場合はtrue、それ以外はfalse
 */
export function isBusinessDay(date: Date): boolean {
  return BUSINESS_DAYS.includes(date.getDay()) && !isHoliday(date);
}

/**
 * 指定された日付の次の営業日を取得します。
 * 次の営業日が見つかるまで日付を1日ずつ進めます。
 * @param date 基準となる日付
 * @returns 次の営業日
 */
export function getNextBusinessDay(date: Date): Date {
  let nextDay = addDays(date, 1);
  while (!isBusinessDay(nextDay)) {
    nextDay = addDays(nextDay, 1);
  }
  return nextDay;
}

/**
 * 指定された日付から指定された営業日数後の日付を取得します。
 * 土日と祝日はスキップされます。
 * @param date 基準となる日付
 * @param days 追加する営業日数
 * @returns 指定された営業日数後の日付
 */
export function addBusinessDays(date: Date, days: number): Date {
  let result = new Date(date);
  let remainingDays = days;
  const direction = days >= 0 ? 1 : -1;

  while (remainingDays !== 0) {
    result = addDays(result, direction);
    if (isBusinessDay(result)) {
      remainingDays -= direction;
    }
  }

  return result;
}

/**
 * 指定された期間内の営業日数を計算します。
 * 開始日と終了日を含む期間内の営業日をカウントします。
 * @param startDate 期間の開始日
 * @param endDate 期間の終了日
 * @returns 期間内の営業日数
 */
export function getBusinessDaysBetween(startDate: Date, endDate: Date): number {
  let currentDate = startDate;
  let businessDays = 0;
  while (currentDate <= endDate) {
    if (isBusinessDay(currentDate)) {
      businessDays++;
    }
    currentDate = addDays(currentDate, 1);
  }
  return businessDays;
}

/**
 * 次の営業時間の開始時刻を計算します。
 *
 * この関数は以下のように動作します。
 * 1. 現在が営業時間内の場合、翌営業日の開始時刻を返します。
 * 2. 現在が営業時間外の場合
 *    a. 同日が営業日で、まだ営業開始時刻前なら、同日の開始時刻を返します。
 *    b. それ以外の場合は、次の営業日の開始時刻を返します。
 *
 * @param {Date} date - 基準となる日時
 * @returns {Date} 次の営業時間の開始時刻
 */
export function getNextBusinessHourStart(date: Date): Date {
  const currentDay = startOfDay(date);
  const currentDayBusinessStart = set(currentDay, BUSINESS_HOURS.start);
  const currentDayBusinessEnd = set(currentDay, BUSINESS_HOURS.end);

  // 現在の日が営業日かつ、まだ営業開始時刻前の場合
  if (isBusinessDay(currentDay) && isBefore(date, currentDayBusinessStart)) {
    return currentDayBusinessStart;
  }

  // 現在の日が営業日かつ、営業時間内の場合
  if (
    isBusinessDay(currentDay) &&
    (isEqual(date, currentDayBusinessStart) ||
      isWithinInterval(date, {
        start: currentDayBusinessStart,
        end: currentDayBusinessEnd,
      }))
  ) {
    // 翌日を取得
    let nextDay = addDays(currentDay, 1);
    // 翌日が営業日でない場合、次の営業日を探す
    while (!isBusinessDay(nextDay)) {
      nextDay = addDays(nextDay, 1);
    }
    return set(nextDay, BUSINESS_HOURS.start);
  }

  // それ以外の場合(営業時間後や休日)
  let nextBusinessDay = addDays(currentDay, 1);
  while (!isBusinessDay(nextBusinessDay)) {
    nextBusinessDay = addDays(nextBusinessDay, 1);
  }
  return set(nextBusinessDay, BUSINESS_HOURS.start);
}

2.3 様々な営業日計算のコード例

app/business-time/page.tsxファイルを作成し、実際の計算例を実装します。

このコンポーネントでは、useEffectフックを使用して、ページロード時に一度だけ各種計算を実行し、結果を状態(state)に保存します。

app/business-time/page.tsx
"use client";

import holidays from "@holiday-jp/holiday_jp";
import {
  addDays,
  addHours,
  addMonths,
  addWeeks,
  endOfMonth,
  endOfYear,
  format,
  setDay,
  startOfYear,
  subDays,
  subHours,
  subYears,
} from "date-fns";
import { useEffect, useState } from "react";
import {
  addBusinessDays,
  getBusinessDaysBetween,
  getNextBusinessDay,
  getNextBusinessHourStart,
  isBusinessDay,
  isBusinessHour,
} from "../lib/business-config";

export default function Example() {
  const [calculations, setCalculations] = useState<any>({});

  useEffect(() => {
    const now = new Date();

    // 現在日から1営業日後を計算
    const nextBusinessDay = getNextBusinessDay(now);

    // 現在から10営業日後の日付を計算
    const tenBusinessDaysLater = addBusinessDays(now, 10);

    // 現在から1営業日前の日付を計算
    const oneBusinessDayBefore = subDays(now, 1);

    // 現在から2営業時間前の時刻を計算
    const twoBusinessHoursBefore = subHours(now, 2);

    // 現在から1ヶ月後までの営業日数を計算
    const oneMonthLater = addMonths(now, 1);
    const businessDaysBetween = getBusinessDaysBetween(now, oneMonthLater);

    // 去年の営業日数を計算
    const lastYearStart = startOfYear(subYears(now, 1));
    const lastYearEnd = endOfYear(subYears(now, 1));
    const businessDaysLastYear = getBusinessDaysBetween(
      lastYearStart,
      lastYearEnd
    );

    // 特定の日付(例:2024年4月1日)から3営業日前の日付を計算
    const specificDate = new Date(2024, 3, 1);
    const threeBusinessDaysBefore = addBusinessDays(specificDate, -3);

    // 次の営業日の開始時刻を計算
    const nextBusinessDayStart = getNextBusinessHourStart(now);

    // 2週間後の金曜日の終業時刻を計算
    const twoWeeksFriday = setDay(addWeeks(now, 2), 5);
    const twoWeeksFridayEnd = new Date(twoWeeksFriday.setHours(20, 0, 0, 0));

    // 今月の最後の営業日を計算
    const lastDayOfMonth = endOfMonth(now);
    let lastBusinessDayOfMonth = lastDayOfMonth;
    while (!isBusinessDay(lastBusinessDayOfMonth)) {
      lastBusinessDayOfMonth = subDays(lastBusinessDayOfMonth, 1);
    }

    // 直前の営業日の終了時刻を計算
    let previousBusinessDay = subDays(now, 1);
    while (!isBusinessDay(previousBusinessDay)) {
      previousBusinessDay = subDays(previousBusinessDay, 1);
    }
    const previousBusinessDayEnd = new Date(
      previousBusinessDay.setHours(20, 0, 0, 0)
    );

    // 次の営業時間の開始時刻を計算(現在が営業時間外の場合)
    const nextBusinessHourStart = getNextBusinessHourStart(now);

    // 現在から100営業時間後の日時を計算
    let hundredBusinessHoursLater = now;
    for (let i = 0; i < 100; i++) {
      hundredBusinessHoursLater = addHours(hundredBusinessHoursLater, 1);
      while (!isBusinessHour(hundredBusinessHoursLater)) {
        hundredBusinessHoursLater = addHours(hundredBusinessHoursLater, 1);
      }
    }

    // 現在時刻から24営業時間後が週末や祝日を跨ぐ場合の計算
    let twentyFourBusinessHoursLater = now;
    for (let i = 0; i < 24; i++) {
      twentyFourBusinessHoursLater = addHours(twentyFourBusinessHoursLater, 1);
      while (!isBusinessHour(twentyFourBusinessHoursLater)) {
        twentyFourBusinessHoursLater = addHours(
          twentyFourBusinessHoursLater,
          1
        );
      }
    }

    // 現在の日時が営業時間内かどうかを判定
    const isDuringBusinessHours = isBusinessHour(now);

    // 特定の期間内の営業日のみの日付配列を生成
    const startDate = now;
    const endDate = addMonths(now, 1);
    const businessDaysArray = [];
    let currentDate = startDate;
    while (currentDate <= endDate) {
      if (isBusinessDay(currentDate)) {
        businessDaysArray.push(new Date(currentDate));
      }
      currentDate = addDays(currentDate, 1);
    }

    // 次の祝日を取得
    const nextHoliday = holidays.between(now, addMonths(now, 12))[0];

    setCalculations({
      nextBusinessDay,
      tenBusinessDaysLater,
      oneBusinessDayBefore,
      twoBusinessHoursBefore,
      businessDaysBetween,
      businessDaysLastYear,
      threeBusinessDaysBefore,
      nextBusinessDayStart,
      twoWeeksFridayEnd,
      lastBusinessDayOfMonth,
      previousBusinessDayEnd,
      nextBusinessHourStart,
      hundredBusinessHoursLater,
      twentyFourBusinessHoursLater,
      isDuringBusinessHours,
      businessDaysArray,
      nextHoliday,
    });
  }, []);

  return (
    <div className="mx-auto max-w-xl rounded-lg bg-white p-6 shadow-lg">
      <h1 className="mb-6 text-3xl font-bold text-gray-800">営業日計算の例</h1>
      <ul className="space-y-4">
        {[
          {
            label: "次の営業日",
            value: calculations.nextBusinessDay,
            format: "yyyy-MM-dd",
          },
          {
            label: "10営業日後",
            value: calculations.tenBusinessDaysLater,
            format: "yyyy-MM-dd",
          },
          {
            label: "1営業日前",
            value: calculations.oneBusinessDayBefore,
            format: "yyyy-MM-dd",
          },
          {
            label: "2営業時間前",
            value: calculations.twoBusinessHoursBefore,
            format: "yyyy-MM-dd HH:mm:ss",
          },
          {
            label: "1ヶ月後までの営業日数",
            value: calculations.businessDaysBetween,
          },
          { label: "去年の営業日数", value: calculations.businessDaysLastYear },
          {
            label: "2024年4月1日の3営業日前",
            value: calculations.threeBusinessDaysBefore,
            format: "yyyy-MM-dd",
          },
          {
            label: "次の営業日の開始時刻",
            value: calculations.nextBusinessDayStart,
            format: "yyyy-MM-dd HH:mm:ss",
          },
          {
            label: "2週間後の金曜日の終業時刻",
            value: calculations.twoWeeksFridayEnd,
            format: "yyyy-MM-dd HH:mm:ss",
          },
          {
            label: "今月の最後の営業日",
            value: calculations.lastBusinessDayOfMonth,
            format: "yyyy-MM-dd",
          },
          {
            label: "直前の営業日の終了時刻",
            value: calculations.previousBusinessDayEnd,
            format: "yyyy-MM-dd HH:mm:ss",
          },
          {
            label: "次の営業時間の開始時刻",
            value: calculations.nextBusinessHourStart,
            format: "yyyy-MM-dd HH:mm:ss",
          },
          {
            label: "100営業時間後",
            value: calculations.hundredBusinessHoursLater,
            format: "yyyy-MM-dd HH:mm:ss",
          },
          {
            label: "24営業時間後",
            value: calculations.twentyFourBusinessHoursLater,
            format: "yyyy-MM-dd HH:mm:ss",
          },
          {
            label: "現在営業時間内か",
            value: calculations.isDuringBusinessHours ? "はい" : "いいえ",
          },
          {
            label: "次の1ヶ月の営業日数",
            value: calculations.businessDaysArray?.length,
          },
          {
            label: "次の祝日",
            value: calculations.nextHoliday,
            format: "holiday",
          },
        ].map((item, index) => (
          <li
            key={index}
            className="flex flex-col border-b border-gray-200 pb-2 sm:flex-row sm:justify-between"
          >
            <span className="font-medium text-gray-700">{item.label}:</span>
            <span className="text-gray-600">
              {item.format && item.value
                ? item.format === "holiday"
                  ? `${format(item.value.date, "yyyy-MM-dd")} (${item.value.name})`
                  : format(item.value, item.format)
                : item.value}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

2.4 コード例の実行画面(2024/09/05 19:51:59に実行)

ローカル環境だと、http://localhost:8080/business-time にアクセスすると以下のような実行画面が表示されます。

3. コード例の解説

3-1. 次の営業時間・営業日の計算

const nextBusinessHour = addHours(now, 1);
const nextBusinessDay = getNextBusinessDay(now);
  • addHours関数はdate-fnsライブラリの関数で、指定された時間数を現在の日時に加算します。
    • ここでは1時間後を計算しています。
  • getNextBusinessDay関数は自作の関数で、現在の日付から次の営業日を取得します。
    • この関数は週末や祝日をスキップして次の営業日を返します。

3-2. 指定した営業日数後の日付計算

const tenBusinessDaysLater = addBusinessDays(now, 10);
  • addBusinessDays関数は、カスタム関数で、指定した営業日数を現在の日付に加算します。
  • この例では、現在の日付から10営業日後の日付を計算しています。
    • 週末や祝日は営業日としてカウントされません。

3-3. 期間内の営業日数の計算

const oneMonthLater = addMonths(now, 1);
const businessDaysBetween = getBusinessDaysBetween(now, oneMonthLater);
  • addMonths関数はdate-fnsライブラリの関数で、指定した月数を現在の日付に加算します。
  • getBusinessDaysBetween関数は、カスタム関数で、二つの日付の間の営業日数を計算します。
  • この例では、現在から1ヶ月後までの期間内の営業日数を計算しています。

3-4. 特定の日付の営業日判定

const lastDayOfMonth = endOfMonth(now);
let lastBusinessDayOfMonth = lastDayOfMonth;
while (!isBusinessDay(lastBusinessDayOfMonth)) {
 lastBusinessDayOfMonth = subDays(lastBusinessDayOfMonth, 1);
}
  • endOfMonth関数はdate-fnsライブラリの関数で、指定した月の最後の日を返します。
  • isBusinessDay関数は、カスタム関数で、指定した日が営業日かどうかを判定します。
  • このループは、月末から1日ずつ前に戻りながら、最初に見つかった営業日を特定します。

3-5. 営業時間内かどうかの判定

const isDuringBusinessHours = isBusinessHour(now);
  • isBusinessHour関数は、カスタム関数で、指定した時刻が営業時間内かどうかを判定します。
  • この例では、現在の時刻が営業時間内かどうかを判定しています。

3-6. 祝日の取得

const nextHoliday = holidays.between(now, addMonths(now, 12))[0];
  • holidays.betweenは、holiday_jp-jsライブラリのメソッドで、指定した期間内の祝日のリストを返します。
  • この例では、現在から1年後までの期間内の最初の祝日を取得しています。

3-7. 複雑な営業時間計算

let hundredBusinessHoursLater = now;
for (let i = 0; i < 100; i++) {
 hundredBusinessHoursLater = addHours(hundredBusinessHoursLater, 1);
 while (!isBusinessHour(hundredBusinessHoursLater)) {
   hundredBusinessHoursLater = addHours(hundredBusinessHoursLater, 1);
 }
}
  • この例では、100営業時間後の日時を計算しています。
  • 外側のループは100回繰り返し、各イテレーションで1時間を加算します。
  • 内側のwhileループは、加算後の時間が営業時間外の場合、営業時間内になるまで時間を進めます。
  • この方法により、夜間や週末、祝日なども適切にスキップされます。

3-8. 日付配列の生成

const businessDaysArray = [];
let currentDate = startDate;
while (currentDate <= endDate) {
 if (isBusinessDay(currentDate)) {
   businessDaysArray.push(new Date(currentDate));
 }
 currentDate = addDays(currentDate, 1);
}
  • この処理は、指定された開始日(startDate)から終了日(endDate)までの期間内の全ての日を順に確認します。
  • isBusinessDay関数を使って各日が営業日かどうかを判定し、営業日の場合のみ配列に追加します。
  • 結果として、指定期間内の全ての営業日の配列を取得できます。

4. まとめ

この記事では、Next.jsアプリケーションでdate-fnsholiday_jp-jsライブラリを使用して、日本の祝日と休日を考慮した営業日計算を実装する方法を解説しました。

この記事のポイントは以下の通りです。

  1. date-fnsholiday_jp-jsのプロジェクト共通の設定と関数定義
  2. カスタム関数による営業日・時間の判定と計算
  3. date-fnsライブラリの様々な関数を使用した営業日・営業時間の計算
  4. Next.jsのApp Router構造に適合したコンポーネントの作成

これらの実装により、営業日に基づいた日付計算や営業時間の判定が可能になります。
この記事が、Next.jsプロジェクトでの営業日計算実装の参考になれば幸いです。

最後までお読みいただき、ありがとうございました。
この記事が役立つと感じた場合は、ぜひ「いいね!」をお願いします。!

Discussion

nap5nap5

記事記載のisBusinessDay関数を使用して営業日カレンダーにチャレンジしてみました