TypeScriptで定休日から営業日を自動整形する関数を作ってみた

## はじめに

実務で病院やクリニック、サロンなど、**定休日を指定して、それ以外の営業日を整形して表示したい**という場面がありました。

たとえば、

- 定休日が日曜だけなら「月曜 ~ 土曜」
- 定休日が水曜だけなら「日曜 ~ 火曜、木曜 ~ 土曜」
- 定休日が不定休なら「定休日なし（不定休）」

といった表示が求められるケースです。

そこで今回は、**定休日の配列から営業日を `"◯曜 ~ ◯曜、◯曜"` のように整形して返す TypeScript 関数**を紹介します。

## 🎯 ゴール

以下のような `enum` をベースに：

```ts
enum ClinicClosedDays {
  SUNDAY = "日曜日",
  MONDAY = "月曜日",
  TUESDAY = "火曜日",
  WEDNESDAY = "水曜日",
  THURSDAY = "木曜日",
  FRIDAY = "金曜日",
  SATURDAY = "土曜日",
  IRREGULAR = "不定休"
}

このような関数を実装します：

```ts
getOpenDayRanges([ClinicClosedDays.SUNDAY, ClinicClosedDays.WEDNESDAY, ClinicClosedDays.SATURDAY]);
// → "月曜 ~ 火曜、木曜 ~ 金曜"
```

## 🔧 実装コード
```ts 
function getOpenDayRanges(closedDays: ClinicClosedDays[]): string {
  const allDays = [
    ClinicClosedDays.SUNDAY,
    ClinicClosedDays.MONDAY,
    ClinicClosedDays.TUESDAY,
    ClinicClosedDays.WEDNESDAY,
    ClinicClosedDays.THURSDAY,
    ClinicClosedDays.FRIDAY,
    ClinicClosedDays.SATURDAY,
  ];

  if (closedDays.includes(ClinicClosedDays.IRREGULAR)) {
    return "定休日なし（不定休）";
  }

  const openDays = allDays.filter(day => !closedDays.includes(day));

  if (openDays.length === 0) {
    return "全日休診";
  }

  const dayIndexMap = new Map<ClinicClosedDays, number>([
    [ClinicClosedDays.SUNDAY, 0],
    [ClinicClosedDays.MONDAY, 1],
    [ClinicClosedDays.TUESDAY, 2],
    [ClinicClosedDays.WEDNESDAY, 3],
    [ClinicClosedDays.THURSDAY, 4],
    [ClinicClosedDays.FRIDAY, 5],
    [ClinicClosedDays.SATURDAY, 6],
  ]);

  const sortedOpenDays = openDays.sort((a, b) => (dayIndexMap.get(a) ?? 0) - (dayIndexMap.get(b) ?? 0));

  const ranges: string[] = [];
  let rangeStart = sortedOpenDays[0];
  let prevIndex = dayIndexMap.get(rangeStart)!;

  for (let i = 1; i <= sortedOpenDays.length; i++) {
    const currentDay = sortedOpenDays[i];
    const currentIndex = dayIndexMap.get(currentDay);

    if (currentIndex !== prevIndex + 1) {
      const rangeEnd = sortedOpenDays[i - 1];
      ranges.push(formatRange(rangeStart, rangeEnd));
      rangeStart = currentDay;
    }

    prevIndex = currentIndex ?? -999;
  }

  return ranges.join("、");

  function formatRange(start: ClinicClosedDays | undefined, end: ClinicClosedDays | undefined): string {
    const formatDay = (day: ClinicClosedDays | undefined) =>
      day ? day.replace("曜日", "曜") : "";
    return start === end ? formatDay(start) : `${formatDay(start)} ~ ${formatDay(end)}`;
  }
}

```

## 処理の流れイメージ
たとえば以下のような入力：

```ts
const closedDays = [SUNDAY, WEDNESDAY, SATURDAY];
```
開いている曜日は：

```ts
[MONDAY, TUESDAY, THURSDAY, FRIDAY]
```
この場合、範囲分けは次のように行われます：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3490027/517de019-6f87-46cb-8a3a-ab20240a7ec5.png)


### 解説付きのコードとテストケース
```ts 
// 定休日を表す enum（日本語表記）
enum ClinicClosedDays {
  SUNDAY = "日曜日",
  MONDAY = "月曜日",
  TUESDAY = "火曜日",
  WEDNESDAY = "水曜日",
  THURSDAY = "木曜日",
  FRIDAY = "金曜日",
  SATURDAY = "土曜日",
  IRREGULAR = "不定休"
}

/**
 * 定休日の配列から営業日を整形し、"月曜 ~ 火曜、木曜 ~ 金曜" のように返す関数
 *
 * @param closedDays - 定休日の配列
 * @returns 営業日を範囲形式で整形した文字列
 */
function getOpenDayRanges(closedDays: ClinicClosedDays[]): string {
  // 全曜日を定義（日曜始まり）
  const allDays = [
    ClinicClosedDays.SUNDAY,
    ClinicClosedDays.MONDAY,
    ClinicClosedDays.TUESDAY,
    ClinicClosedDays.WEDNESDAY,
    ClinicClosedDays.THURSDAY,
    ClinicClosedDays.FRIDAY,
    ClinicClosedDays.SATURDAY,
  ];

  // 不定休が含まれる場合は特別なメッセージを返す
  if (closedDays.includes(ClinicClosedDays.IRREGULAR)) {
    return "定休日なし（不定休）";
  }

  // 定休日を除いた曜日が営業日となる
  const openDays = allDays.filter(day => !closedDays.includes(day));

  // 営業日が0日の場合（毎日休診）
  if (openDays.length === 0) {
    return "全日休診";
  }

  // 各曜日に対応するインデックス（日曜: 0 ～ 土曜: 6）
  const dayIndexMap = new Map<ClinicClosedDays, number>([
    [ClinicClosedDays.SUNDAY, 0],
    [ClinicClosedDays.MONDAY, 1],
    [ClinicClosedDays.TUESDAY, 2],
    [ClinicClosedDays.WEDNESDAY, 3],
    [ClinicClosedDays.THURSDAY, 4],
    [ClinicClosedDays.FRIDAY, 5],
    [ClinicClosedDays.SATURDAY, 6],
  ]);

  // 営業日をインデックス順に並べ替え
  const sortedOpenDays = openDays.sort(
    (a, b) => (dayIndexMap.get(a) ?? 0) - (dayIndexMap.get(b) ?? 0)
  );

  // 営業日の連続範囲を格納する配列
  const ranges: string[] = [];

  // 最初の範囲の開始点
  let rangeStart = sortedOpenDays[0];
  // 前の曜日のインデックスを保持
  let prevIndex = dayIndexMap.get(rangeStart)!;

  // 営業日をループして範囲に分割
  for (let i = 1; i <= sortedOpenDays.length; i++) {
    const currentDay = sortedOpenDays[i];
    const currentIndex = dayIndexMap.get(currentDay);

    // 前の曜日と連続していない場合 → 範囲の区切り
    if (currentIndex !== prevIndex + 1) {
      const rangeEnd = sortedOpenDays[i - 1];
      ranges.push(formatRange(rangeStart, rangeEnd));
      // 次の範囲のスタートを更新
      rangeStart = currentDay;
    }

    // インデックスを更新（undefined の場合はダミー）
    prevIndex = currentIndex ?? -999;
  }

  // 最終的に「月曜 ~ 水曜、金曜」形式で返す
  return ranges.join("、");

  // 単体または範囲を "月曜" または "月曜 ~ 水曜" 形式に整形
  function formatRange(start: ClinicClosedDays | undefined, end: ClinicClosedDays | undefined): string {
    const formatDay = (day: ClinicClosedDays | undefined) =>
      day ? day.replace("曜日", "曜") : "";

    return start === end ? formatDay(start) : `${formatDay(start)} ~ ${formatDay(end)}`;
  }
}

// テスト用ラッパー関数：定休日を渡して結果を表示
function test(closed: ClinicClosedDays[], label: string) {
  const result = getOpenDayRanges(closed);
  const formattedClosed = closed.map(d => d.replace("曜日", "曜")).join("・");
  console.log(`🧪 ${label}`);
  console.log(`定休日: ${formattedClosed || "なし"}\n→ 営業日: ${result}\n`);
}

// 豊富なケースを用意
test([], "全て営業（定休日なし）");
test([ClinicClosedDays.SUNDAY], "日曜休み");
test([ClinicClosedDays.WEDNESDAY], "水曜休み");
test([ClinicClosedDays.SUNDAY, ClinicClosedDays.SATURDAY], "土日休み");
test([ClinicClosedDays.MONDAY, ClinicClosedDays.TUESDAY, ClinicClosedDays.THURSDAY, ClinicClosedDays.FRIDAY], "月・火・木・金休み");
test([ClinicClosedDays.SUNDAY, ClinicClosedDays.WEDNESDAY, ClinicClosedDays.SATURDAY], "日・水・土休み");
test([
  ClinicClosedDays.SUNDAY,
  ClinicClosedDays.MONDAY,
  ClinicClosedDays.TUESDAY,
  ClinicClosedDays.WEDNESDAY,
  ClinicClosedDays.THURSDAY,
  ClinicClosedDays.FRIDAY,
  ClinicClosedDays.SATURDAY
], "全休診");
test([ClinicClosedDays.IRREGULAR], "不定休");
test([ClinicClosedDays.TUESDAY, ClinicClosedDays.THURSDAY], "火・木休み");
test([ClinicClosedDays.SUNDAY, ClinicClosedDays.MONDAY], "日・月休み");
test([ClinicClosedDays.FRIDAY], "金曜休み");
```

```
🧪 全て営業（定休日なし）
定休日: なし
→ 営業日: 日曜 ~ 土曜

🧪 日曜休み
定休日: 日曜
→ 営業日: 月曜 ~ 土曜

🧪 水曜休み
定休日: 水曜
→ 営業日: 日曜 ~ 火曜、木曜 ~ 土曜

🧪 土日休み
定休日: 日曜・土曜
→ 営業日: 月曜 ~ 金曜

🧪 月・火・木・金休み
定休日: 月曜・火曜・木曜・金曜
→ 営業日: 日曜、水曜、土曜

🧪 日・水・土休み
定休日: 日曜・水曜・土曜
→ 営業日: 月曜 ~ 火曜、木曜 ~ 金曜

🧪 全休診
定休日: 日曜・月曜・火曜・水曜・木曜・金曜・土曜
→ 営業日: 全日休診

🧪 不定休
定休日: 不定休
→ 営業日: 定休日なし（不定休）

🧪 火・木休み
定休日: 火曜・木曜
→ 営業日: 日曜 ~ 月曜、水曜、金曜 ~ 土曜

🧪 日・月休み
定休日: 日曜・月曜
→ 営業日: 火曜 ~ 土曜

🧪 金曜休み
定休日: 金曜
→ 営業日: 日曜 ~ 木曜、土曜
```

### 処理一部を解説（日本語解説）
1. 営業日を「連続する範囲」に分割
例：
- 月・火・水・金・土 → 月 ~ 水、金 ~ 土
- 日・水・金 → 日曜、水曜、金曜

処理手順：
1. 最初の営業日を rangeStart とする
2. ループで次の営業日と前の営業日を比較
3.「連続してない」と判断されたら、そこまでの範囲を切り出して ranges に追加
4.次の営業日を新しい rangeStart にする
5.最後の要素まで確認したら join("、") して返す


### 終わりに
株式会社シンシアでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら

https://www.wantedly.com/companies/xincere-inc/stories

弊社には年間100人程度の実務未経験の方に応募いただき、技術面接を実施しております。
この記事が少しでも学びになったという方は、ぜひ wantedly のストーリーもご覧いただけるととても嬉しいです！