時間割自動生成

概要
スプレッドシートとGASを利用した自動時間割生成ツール
機能
完全自動ツールと一部手動ツール
[自動時間割](https://docs.google.com/spreadsheets/d/1eZxVyWO-xKyKatj4gjYP5LWzbkFglMUczgDMZYnSf1k/copy
"スプレッドシートのコピーを生成します")
```GAS:時間割生成
function generateTimetable() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const settingsSheet = ss.getSheetByName("Settings");
  const timetableSheet = ss.getSheetByName("Timetable");

  // 教科名と回数制限を取得
  const subjects = settingsSheet.getRange(2, 1, settingsSheet.getLastRow() - 1, 1).getValues().flat();
  const limits = settingsSheet.getRange(2, 2, settingsSheet.getLastRow() - 1, 1).getValues().flat();

  const timetable = Array.from({ length: 6 }, () => Array(5).fill(""));
  const count = {};
  const specialSubject = "図工"; // 図工の教科名
  subjects.forEach((subject, i) => count[subject] = limits[i]);

  // クラブの内容をSettingsシートのA13から取得
  const clubActivity = settingsSheet.getRange("A13").getValue();

  // 先に水曜日6時間目をクラブ活動として配置
  timetable[5][2] = clubActivity;

  // ランダム配置用ヘルパー関数
  function getRandomIndex(array) {
    return Math.floor(Math.random() * array.length);
  }

  // 図工をランダムに配置（どの曜日でも可、奇数時間から2時間連続で配置）
  for (let i = 0; i < count[specialSubject] / 2; i++) {
    const availableDays = [0, 1, 2, 3, 4];  // 月曜から金曜まで
    const randomDay = availableDays[getRandomIndex(availableDays)]; // ランダムに曜日を選択

    const availablePeriods = [];
    // 奇数時間（1時間目、3時間目、5時間目）のみ
    for (let period = 0; period < 6; period += 2) { 
      if (period < 5 && timetable[period][randomDay] === "" && timetable[period + 1][randomDay] === "") {
        availablePeriods.push(period);
      }
    }

    if (availablePeriods.length > 0) {
      const randomPeriod = availablePeriods[getRandomIndex(availablePeriods)];
      timetable[randomPeriod][randomDay] = specialSubject;
      timetable[randomPeriod + 1][randomDay] = specialSubject;
      count[specialSubject] -= 2;
    }
  }

  // 他の教科を配置
  for (let day = 0; day < 5; day++) {
    const usedSubjects = new Set(); // その曜日に既に配置された教科を記録

    for (let period = 0; period < 6; period++) {
      // 既に配置済みの場合、または水曜日6時間目の場合はスキップ
      if (timetable[period][day] !== "" || (period === 5 && day === 2)) continue;

      const availableSubjects = subjects.filter(subject => 
        count[subject] > 0 && 
        !usedSubjects.has(subject) // その曜日に既に配置されていない
      );

      // 配置可能な教科がない場合はスキップ
      if (availableSubjects.length === 0) continue;

      const randomIndex = getRandomIndex(availableSubjects);
      const selectedSubject = availableSubjects[randomIndex];

      timetable[period][day] = selectedSubject;
      count[selectedSubject]--;
      usedSubjects.add(selectedSubject); // その曜日の教科リストに追加
    }
  }

  // 未配置の教科を強制的に配置
  for (let period = 0; period < 6; period++) {
    for (let day = 0; day < 5; day++) {
      // クラブが配置された水曜日6時間目はスキップ
      if (timetable[period][day] === "" && !(period === 5 && day === 2)) {
        const remainingSubjects = subjects.filter(subject => count[subject] > 0);

        if (remainingSubjects.length > 0) {
          const randomIndex = getRandomIndex(remainingSubjects);
          const selectedSubject = remainingSubjects[randomIndex];

          timetable[period][day] = selectedSubject;
          count[selectedSubject]--;
        }
      }
    }
  }

  // 時間割をスプレッドシートに出力
  timetableSheet.getRange(2, 2, 6, 5).setValues(timetable);
}

```
```GAS:張り付け
function copyTimetableToExistingSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const timetableSheet = ss.getSheetByName("Timetable");
  const existingSheetName = "時間割";

  // "時間割"シートを取得
  let existingSheet = ss.getSheetByName(existingSheetName);

  if (!existingSheet) {
    // "時間割"シートが存在しない場合、新しいシートを作成
    existingSheet = ss.insertSheet(existingSheetName);
  }

  // Timetableシートの範囲を取得
  const range = timetableSheet.getDataRange();
  const values = range.getValues();

  // 既存シートのデータをクリア
  existingSheet.clear();

  // データを既存シートにコピー
  existingSheet.getRange(1, 1, values.length, values[0].length).setValues(values);

  // 完了メッセージをログに出力
  Logger.log("時間割が既存のシートにコピーされました。");
}


```
```GAS:追加メニュー
function onOpen() {
  const ui = SpreadsheetApp.getUi(); // UIを取得
  ui.createMenu('カスタムメニュー') // メニュー名を設定
    .addItem('時間割を生成', 'generateTimetable') // メニュー項目と実行する関数
    .addItem('既存シートに時間割をコピー', 'copyTimetableToExistingSheet') // メニュー項目と実行する関数
    .addItem('簡単時間割', 'updateTimetableVertical') // メニュー項目と実行する関数
    .addToUi(); // メニューをUIに追加
}

```
```GAS:手入力時間割
function updateTimetableVertical() {
  const subjectSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('手入力設定');
  const timetableSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('手入力時間割');

  // 1. 手入力設定シートのB2:B12の元の値を保存
  const originalValues = subjectSheet.getRange("B2:B12").getValues();

  // 2. 手入力時間割シートのJ2:J12を手入力設定シートのB2:B12に貼り付け
  const manualInputRange = timetableSheet.getRange("J2:J12").getValues();
  subjectSheet.getRange("B2:B12").setValues(manualInputRange);

  // 3. 科目データを取得
  const subjects = subjectSheet.getRange("A2:B12").getValues();
  let randomSubjects = subjects.flatMap(([subject, count]) =>
    Array.from({ length: count }, () => subject)
  ).sort(() => Math.random() - 0.5);

  for (let col = 2; col <= 6; col++) { // 曜日ごとに処理
    const usedSubjects = new Set(); // この曜日に使用された教科を記録
    for (let row = 2; row <= 7; row++) { // 時間帯
      const cell = timetableSheet.getRange(row, col);
      const cellValue = cell.getValue();

      if (!cellValue) { // セルが空の場合のみ処理
        let assigned = false;

        // 割り当て候補を探す
        for (let i = 0; i < randomSubjects.length; i++) {
          const subject = randomSubjects[i];
          if (!usedSubjects.has(subject)) { // 同じ曜日に既に割り当てられていない教科を確認
            cell.setValue(subject);
            usedSubjects.add(subject); // この曜日で使用済みとして記録
            randomSubjects.splice(i, 1); // 割り当てた教科をリストから削除
            assigned = true;
            break; // 割り当て完了したら次のセルへ
          }
        }

        // 割り当てられなかった場合、ランダムに割り当て
        if (!assigned && randomSubjects.length > 0) {
          const subject = randomSubjects.shift(); // 残った教科をランダムに取得
          cell.setValue(subject);
        }
      }
    }
  }

  // 4. 手入力設定シートのB2:B12を元の値に戻す
  subjectSheet.getRange("B2:B12").setValues(originalValues);
}

```
その他関数
```def:コマ数分並べる(手入力設定シート C2:C13)
=iferror(TEXTJOIN(",", TRUE, TRANSPOSE(SPLIT(REPT(A2 & ",", B2), ","))),"")
```
```def:すべての教科を繋げる(手入力設定シート E2)
=TEXTJOIN(",", TRUE, C2:C12)
```