作るもの
やりたいことは、次の3つです。
-
名簿シートにいる生徒をランダムに並べる -
履歴シートにある「前回隣同士だった組み合わせ」をなるべく避ける -
座席表シートに、教室の行列に合わせて席を出力する
席替えは、完全に機械任せにしにくい校務です。人間関係、視力、配慮事項、授業中の様子など、最後は担任が見る必要があります。
ただ、毎回ゼロから紙に名前を書いて並べ替える部分は、GASに寄せられます。この記事のコードは「たたき台を作る」ためのものです。できあがった座席表を見て、必要に応じて手で入れ替える前提です。
使うシートは3枚です。
名簿履歴座席表
名簿 シートは、1行目を見出しにして、A列に出席番号、B列に氏名を書きます。
| 出席番号 | 氏名 |
|---|---|
| 1 | 青木 |
| 2 | 石田 |
| 3 | 上田 |
履歴 シートは、過去に隣同士だった組み合わせを保存します。自動生成後に、その回の横隣ペアも追記するようにします。
| 日付 | 名前1 | 名前2 |
|---|---|---|
| 2026/09/01 | 青木 | 石田 |
座席表 シートはコード側で作ります。既にある場合は中身を消してから、今回の座席案を書き込みます。
全体の考え方
今回の実装では、座席を「前から順に、左から右へ」埋めます。たとえば6列×5行なら、最大30席です。
処理の流れはこうです。
- 名簿を読む
- 履歴から過去ペアを読む
- 名簿をシャッフルする
- 横隣ペアが過去履歴と重なる数を数える
- 何度か並べ替えて、重なりが少ない案を採用する
- 座席表に出す
- 今回の横隣ペアを履歴へ追記する
ここで大事なのは、「条件を増やしすぎない」ことです。
座席表の自動作成は、要望を盛るとすぐ複雑になります。「視力が低い生徒は前」「特定の組み合わせは避ける」「班の男女比をそろえる」「支援が必要な生徒は近く」など、現場の条件は多いです。
この記事では、まず「前回の横隣を避ける」という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: 6、COLS: 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校務自動化レシピ』でも扱っています。