0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GASで席替え案を自動作成する:前回ペアを避けながら座席表まで出す

0
Posted at

作るもの

やりたいことは、次の3つです。

  1. 名簿 シートにいる生徒をランダムに並べる
  2. 履歴 シートにある「前回隣同士だった組み合わせ」をなるべく避ける
  3. 座席表 シートに、教室の行列に合わせて席を出力する

席替えは、完全に機械任せにしにくい校務です。人間関係、視力、配慮事項、授業中の様子など、最後は担任が見る必要があります。

ただ、毎回ゼロから紙に名前を書いて並べ替える部分は、GASに寄せられます。この記事のコードは「たたき台を作る」ためのものです。できあがった座席表を見て、必要に応じて手で入れ替える前提です。

使うシートは3枚です。

  • 名簿
  • 履歴
  • 座席表

名簿 シートは、1行目を見出しにして、A列に出席番号、B列に氏名を書きます。

出席番号 氏名
1 青木
2 石田
3 上田

履歴 シートは、過去に隣同士だった組み合わせを保存します。自動生成後に、その回の横隣ペアも追記するようにします。

日付 名前1 名前2
2026/09/01 青木 石田

座席表 シートはコード側で作ります。既にある場合は中身を消してから、今回の座席案を書き込みます。

全体の考え方

今回の実装では、座席を「前から順に、左から右へ」埋めます。たとえば6列×5行なら、最大30席です。

処理の流れはこうです。

  1. 名簿を読む
  2. 履歴から過去ペアを読む
  3. 名簿をシャッフルする
  4. 横隣ペアが過去履歴と重なる数を数える
  5. 何度か並べ替えて、重なりが少ない案を採用する
  6. 座席表に出す
  7. 今回の横隣ペアを履歴へ追記する

ここで大事なのは、「条件を増やしすぎない」ことです。

座席表の自動作成は、要望を盛るとすぐ複雑になります。「視力が低い生徒は前」「特定の組み合わせは避ける」「班の男女比をそろえる」「支援が必要な生徒は近く」など、現場の条件は多いです。

この記事では、まず「前回の横隣を避ける」という1条件に絞ります。小さく始めると、あとから条件を足す場所も見えやすくなります。

GASコード全文

スプレッドシートを開き、拡張機能Apps Script から、次のコードを貼り付けます。

このコードは、外部APIを呼びません。APIキーも使いません。スプレッドシート内の名簿と履歴だけを読み書きします。

const CONFIG = {
  STUDENTS_SHEET: '名簿',
  HISTORY_SHEET: '履歴',
  SEAT_SHEET: '座席表',
  ROWS: 5,
  COLS: 6,
  TRY_COUNT: 300,
  HEADER_BG: '#d9ead3',
  WARNING_BG: '#fce5cd'
};

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('席替え')
    .addItem('座席案を作成', 'buildSeatPlan')
    .addToUi();
}

function buildSeatPlan() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const students = readStudents_(ss);

  if (students.length === 0) {
    SpreadsheetApp.getUi().alert('名簿シートに生徒がいません。');
    return;
  }

  const seatCount = CONFIG.ROWS * CONFIG.COLS;
  if (students.length > seatCount) {
    SpreadsheetApp.getUi().alert(
      '座席数が足りません。名簿の人数: ' + students.length +
      ' / 座席数: ' + seatCount
    );
    return;
  }

  const historyPairs = readHistoryPairs_(ss);
  const bestPlan = findBestPlan_(students, historyPairs);

  writeSeatSheet_(ss, bestPlan);
  appendCurrentPairsToHistory_(ss, bestPlan);

  SpreadsheetApp.getUi().alert(
    '座席案を作成しました。前回までの横隣ペア重複数: ' +
    bestPlan.duplicateCount
  );
}

function readStudents_(ss) {
  const sheet = ss.getSheetByName(CONFIG.STUDENTS_SHEET);
  if (!sheet) {
    throw new Error('名簿シートが見つかりません: ' + CONFIG.STUDENTS_SHEET);
  }

  const lastRow = sheet.getLastRow();
  if (lastRow < 2) {
    return [];
  }

  const values = sheet.getRange(2, 1, lastRow - 1, 2).getValues();

  return values
    .filter(row => row[0] !== '' && row[1] !== '')
    .map(row => ({
      number: row[0],
      name: String(row[1]).trim()
    }));
}

function readHistoryPairs_(ss) {
  let sheet = ss.getSheetByName(CONFIG.HISTORY_SHEET);
  if (!sheet) {
    sheet = ss.insertSheet(CONFIG.HISTORY_SHEET);
    sheet.getRange(1, 1, 1, 3).setValues([['日付', '名前1', '名前2']]);
    return new Set();
  }

  const lastRow = sheet.getLastRow();
  if (lastRow < 2) {
    return new Set();
  }

  const values = sheet.getRange(2, 1, lastRow - 1, 3).getValues();
  const pairs = new Set();

  values.forEach(row => {
    const name1 = String(row[1]).trim();
    const name2 = String(row[2]).trim();
    if (name1 && name2) {
      pairs.add(pairKey_(name1, name2));
    }
  });

  return pairs;
}

function findBestPlan_(students, historyPairs) {
  let bestSeats = null;
  let bestDuplicateCount = Number.MAX_SAFE_INTEGER;

  for (let i = 0; i < CONFIG.TRY_COUNT; i++) {
    const shuffled = shuffle_(students);
    const seats = toSeatGrid_(shuffled);
    const duplicateCount = countDuplicateNeighborPairs_(seats, historyPairs);

    if (duplicateCount < bestDuplicateCount) {
      bestSeats = seats;
      bestDuplicateCount = duplicateCount;
    }

    if (bestDuplicateCount === 0) {
      break;
    }
  }

  return {
    seats: bestSeats,
    duplicateCount: bestDuplicateCount
  };
}

function shuffle_(students) {
  const copied = students.map(student => ({
    number: student.number,
    name: student.name
  }));

  for (let i = copied.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    const tmp = copied[i];
    copied[i] = copied[j];
    copied[j] = tmp;
  }

  return copied;
}

function toSeatGrid_(students) {
  const seats = [];
  let index = 0;

  for (let r = 0; r < CONFIG.ROWS; r++) {
    const row = [];
    for (let c = 0; c < CONFIG.COLS; c++) {
      if (index < students.length) {
        row.push(students[index]);
      } else {
        row.push(null);
      }
      index++;
    }
    seats.push(row);
  }

  return seats;
}

function countDuplicateNeighborPairs_(seats, historyPairs) {
  let count = 0;

  for (let r = 0; r < seats.length; r++) {
    for (let c = 0; c < seats[r].length - 1; c++) {
      const left = seats[r][c];
      const right = seats[r][c + 1];

      if (!left || !right) {
        continue;
      }

      if (historyPairs.has(pairKey_(left.name, right.name))) {
        count++;
      }
    }
  }

  return count;
}

function writeSeatSheet_(ss, plan) {
  let sheet = ss.getSheetByName(CONFIG.SEAT_SHEET);
  if (!sheet) {
    sheet = ss.insertSheet(CONFIG.SEAT_SHEET);
  }

  sheet.clear();

  sheet.getRange(1, 1).setValue('黒板');
  sheet.getRange(1, 1, 1, CONFIG.COLS)
    .merge()
    .setHorizontalAlignment('center')
    .setBackground(CONFIG.HEADER_BG)
    .setFontWeight('bold');

  const output = plan.seats.map(row => {
    return row.map(student => {
      if (!student) {
        return '';
      }
      return student.number + '\n' + student.name;
    });
  });

  sheet.getRange(2, 1, CONFIG.ROWS, CONFIG.COLS).setValues(output);
  sheet.getRange(2, 1, CONFIG.ROWS, CONFIG.COLS)
    .setWrap(true)
    .setVerticalAlignment('middle')
    .setHorizontalAlignment('center');

  sheet.setRowHeights(2, CONFIG.ROWS, 56);
  sheet.setColumnWidths(1, CONFIG.COLS, 110);

  sheet.getRange(CONFIG.ROWS + 3, 1).setValue(
    '前回までの横隣ペア重複数'
  );
  sheet.getRange(CONFIG.ROWS + 3, 2).setValue(plan.duplicateCount);

  if (plan.duplicateCount > 0) {
    sheet.getRange(CONFIG.ROWS + 3, 1, 1, 2).setBackground(CONFIG.WARNING_BG);
  }
}

function appendCurrentPairsToHistory_(ss, plan) {
  let sheet = ss.getSheetByName(CONFIG.HISTORY_SHEET);
  if (!sheet) {
    sheet = ss.insertSheet(CONFIG.HISTORY_SHEET);
    sheet.getRange(1, 1, 1, 3).setValues([['日付', '名前1', '名前2']]);
  }

  const today = new Date();
  const rows = [];

  for (let r = 0; r < plan.seats.length; r++) {
    for (let c = 0; c < plan.seats[r].length - 1; c++) {
      const left = plan.seats[r][c];
      const right = plan.seats[r][c + 1];

      if (!left || !right) {
        continue;
      }

      rows.push([today, left.name, right.name]);
    }
  }

  if (rows.length === 0) {
    return;
  }

  const startRow = sheet.getLastRow() + 1;
  sheet.getRange(startRow, 1, rows.length, 3).setValues(rows);
}

function pairKey_(name1, name2) {
  return [String(name1).trim(), String(name2).trim()]
    .sort()
    .join('::');
}

使い方

まず、スプレッドシートに 名簿 シートを作ります。A列に出席番号、B列に氏名を入れておきます。1行目は見出しです。

次に、Apps Scriptへコードを貼り付けて保存します。スプレッドシートを再読み込みすると、上部メニューに 席替え が出ます。

席替え座席案を作成 を押すと、座席表 シートが作られます。既に 座席表 シートがある場合は、中身を消して今回の案に更新します。

同時に、今回の横隣ペアが 履歴 シートへ追記されます。次回以降は、その履歴を見て、同じ横隣ペアが少なくなる案を探します。

CONFIG の値を変えると、教室の形に合わせられます。

ROWS: 5,
COLS: 6,
TRY_COUNT: 300

たとえば、縦6列、横5列の教室なら、ROWS: 6COLS: 5 にします。

TRY_COUNT は、何回ランダムに並べ替えて試すかです。数を増やすと、前回ペアの重複が少ない案を見つけやすくなります。ただし、人数や条件によっては0にならないこともあります。

ここは「最適解を数学的に保証する」より、「担任が確認しやすいたたき台を素早く作る」ための設定です。

現場で使うときの調整ポイント

席替えは、コードだけで完結させない方が扱いやすいです。実際に使うなら、次のように少しずつ足していくのが現実的です。

まず、前方固定席が必要な生徒がいる場合です。

今回のコードでは全員をランダムに並べています。視力、聞こえ方、支援上の理由などで前にしたい生徒がいる場合は、名簿 シートに 固定行 固定列 のような列を追加し、そこだけ先に座席へ置く形にできます。

次に、避けたい組み合わせがある場合です。

履歴 は過去の横隣ペアですが、同じ形式で 禁止ペア シートを作れば、「この2人は近くにしない」という条件も追加できます。ただし、条件を増やすほど、座席案は作りにくくなります。最初から全部盛りにせず、実際に困っている条件だけ足すのがおすすめです。

また、班活動を重視するなら、横隣だけでなく前後左右を見た方がよい場合があります。今回のコードは横隣だけを履歴化しています。前後も見るなら、countDuplicateNeighborPairs_appendCurrentPairsToHistory_ に、上下のペアを見る処理を追加します。

たとえば「同じ班になったことがあるか」を履歴にしたい場合は、席の隣接ではなく、2列×2行や3列×2行を1グループとして記録する設計に変えます。

小さく作ると引き継ぎやすい

GAS校務自動化では、最初から大きなシステムにしない方が続きます。

今回の席替えコードも、やっていることは単純です。

  • 名簿を読む
  • シャッフルする
  • 過去ペアと照合する
  • 座席表へ書く
  • 今回ペアを履歴に残す

このくらいの粒度なら、翌年度の自分や、同じ学年の先生にも説明しやすくなります。

特に CONFIG に設定を集めておくと、教室の行列数を変えるときにコード全体を探さずに済みます。

const CONFIG = {
  STUDENTS_SHEET: '名簿',
  HISTORY_SHEET: '履歴',
  SEAT_SHEET: '座席表',
  ROWS: 5,
  COLS: 6,
  TRY_COUNT: 300
};

「どこを変えればよいか」が見えているだけで、校務ツールはだいぶ扱いやすくなります。

最後に、席替えのような人間関係に関わる校務は、GASに判断を丸投げするより、GASで候補を出して人が見る形が向いています。名簿、フォーム、メール、座席表のような日々の定型作業を小さくGAS化する考え方は、既刊のKindle『現役理科教員のGAS校務自動化レシピ』でも扱っています。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?