Open3
ChatGPTにシャッフルランチのグループ分けのロジックを考えてもらう
以下の内容でお願いした。
以下の二次配列は、グループごとのメンバーの名前を表しています。
以下のルールに従って、メンバーをシャッフルして、グループを再編成するコードを書いてください。
- メンバーをシャッフルして、1グループ6人以下で、グループの数が最小になるようにしてください。
- 例えば、合計で32名いた場合、Math.floor(32/6)+1=5グループを作ります。
- この場合、3グループは6人、2グループは5人になります。
- 元のグループと同じだったメンバーとは同じグループに入れないようにしてください。
- また、'Paul' と 'John' と 'Roger' は同じグループに入れないようにしてください。3人とは限らず、人数は可変としてください。
- 上記のメンバー以外でグループリーダーを決めます。
- このとき、'Mick','Keith','Pete(3)','Paul(2)','Brian','Pete(5)','Stuart','Ian''Kenney','Stuart(2)','John(5)','Kenney(2)'は、グループリーダーにしないでください。
- グループリーダーの名前の先頭には
⭐️
をつけてください。 - コードを実行するたびに、毎回ランダムなグループができるようにしてください。
const membersByGroup = [
['Paul', 'Mick', 'George', 'Ringo', 'Pete', 'Stuart'],
['John', 'Keith', 'Ronnie', 'Charlie', 'Ian'],
['Roger', 'Pete(3)', 'John(2)', 'Keith(2)', 'Kenney'],
['John(3)', 'Paul(2)', 'George(2)', 'Ringo(2)', 'Pete(4)', 'Stuart(2)'],
['Freddie', 'Brian', 'Roger(2)', 'John(4)', 'John(5)'],
['Roger(3)', 'Pete(5)', 'John(6)', 'Keith(3)', 'Kenney(2)']
]
出来上がったコード
const membersByGroup = [
['Paul', 'Mick', 'George', 'Ringo', 'Pete', 'Stuart'],
['John', 'Keith', 'Ronnie', 'Charlie', 'Ian'],
['Roger', 'Pete(3)', 'John(2)', 'Keith(2)', 'Kenney'],
['John(3)', 'Paul(2)', 'George(2)', 'Ringo(2)', 'Pete(4)', 'Stuart(2)'],
['Freddie', 'Brian', 'Roger(2)', 'John(4)', 'John(5)'],
['Roger(3)', 'Pete(5)', 'John(6)', 'Keith(3)', 'Kenney(2)']
];
const specialMembers = ['Paul', 'John', 'Roger']; // 同じグループに入れないメンバー
const nonLeaders = [...specialMembers, 'Mick','Keith','Pete(3)','Paul(2)','Brian','Pete(5)','Stuart','Ian','Kenney','Stuart(2)','John(5)','Kenney(2)']; // リーダーにしないメンバー
// 全メンバーを収集し、元のグループを記録
const members = [];
for (let groupIndex = 0; groupIndex < membersByGroup.length; groupIndex++) {
const group = membersByGroup[groupIndex];
for (let member of group) {
members.push({
name: member,
originalGroup: groupIndex
});
}
}
// グループ数を計算
const N = members.length;
const maxGroupSize = 6;
const defaultGroupSize = Math.floor(N/maxGroupSize) // 全てのグループを 最初は5人 と仮定して初期化
const G = Math.ceil(N / maxGroupSize);
// グループサイズを決定
let groupSizes = [];
let totalMembersAssigned = 0;
for (let i = 0; i < G; i++) {
groupSizes.push(defaultGroupSize);
totalMembersAssigned += defaultGroupSize;
}
let index = groupSizes.length - 1;
while (totalMembersAssigned < N) {
if (groupSizes[index] < maxGroupSize) {
groupSizes[index]++;
totalMembersAssigned++;
}
index = (index - 1 + groupSizes.length) % groupSizes.length;
}
// シャッフル関数
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// メンバーをグループに割り当てる関数
function assignMembers() {
let groups = [];
for (let i = 0; i < G; i++) {
groups.push({
members: [],
originalGroups: new Set(),
specialMembers: new Set()
});
}
shuffle(members);
for (let member of members) {
let assigned = false;
let groupIndices = [...Array(G).keys()];
shuffle(groupIndices);
for (let groupIndex of groupIndices) {
let group = groups[groupIndex];
if (group.members.length >= groupSizes[groupIndex]) continue;
if (group.originalGroups.has(member.originalGroup)) continue;
if (specialMembers.includes(member.name) && group.specialMembers.size > 0) continue;
group.members.push(member.name);
group.originalGroups.add(member.originalGroup);
if (specialMembers.includes(member.name)) group.specialMembers.add(member.name);
assigned = true;
break;
}
if (!assigned) return null;
}
return groups;
}
// 割り当てを最大100回試行
let groups = null;
for (let attempt = 0; attempt < 100; attempt++) {
groups = assignMembers();
if (groups !== null) break;
}
if (groups === null) {
console.log('メンバーの割り当てに失敗しました。');
} else {
// グループリーダーを選択
for (let group of groups) {
let potentialLeaders = group.members.filter(name => !nonLeaders.includes(name));
if (potentialLeaders.length == 0) potentialLeaders = group.members;
shuffle(potentialLeaders);
let leader = potentialLeaders[0];
group.members = group.members.map(name => (name === leader ? '⭐️' + name : name));
}
// 結果を表示
for (let i = 0; i < groups.length; i++) {
console.log(`グループ ${i + 1}:`, groups[i].members.join(', '));
}
}
Google スプレッドシートの GAS を書くとこんな感じ。
以下のシートが必要。
- 前回のシャッフルランチ(これはフォーマットどおりである必要がある)
- 今回のシャッフルランチ
- シャッフルランチのグループ履歴
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('シャッフルランチ') // メニュー名
.addItem('シャッフルランチのグループを作成する', 'myFunction') // メニュー項目名と実行する関数名
.addToUi();
}
function myFunction() {
const prevMembersByGroupSheet = "前回のシャッフルランチ"
const sheetName = "今回のシャッフルランチ"
const historySheetName = "シャッフルランチのグループ履歴"
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
const lastRow = sheet.getLastRow(); // データが入力されている最後の行を取得
const range = sheet.getRange(2, 2, lastRow - 1, 6); // B列からG列、2行目から最後の行まで
range.clearContent(); // 範囲内の内容をクリア
const membersByGroup = getMembersByGroup(prevMembersByGroupSheet)
const specialMembers = getSpecialMembers(prevMembersByGroupSheet)
const nonLeaders = getNonLeaders(prevMembersByGroupSheet)
// 全メンバーを収集し、元のグループを記録
const members = [];
for (let groupIndex = 0; groupIndex < membersByGroup.length; groupIndex++) {
const group = membersByGroup[groupIndex];
for (let member of group) {
members.push({
name: member,
originalGroup: groupIndex
});
}
}
// グループ数を計算
const N = members.length;
const maxGroupSize = 6;
const defaultGroupSize = 5 // 全てのグループを 最初は5人 と仮定して初期化
const G = Math.ceil(N / maxGroupSize);
// グループサイズを決定
let groupSizes = [];
let totalMembersAssigned = 0;
for (let i = 0; i < G; i++) {
groupSizes.push(defaultGroupSize);
totalMembersAssigned += defaultGroupSize;
}
let index = groupSizes.length - 1;
while (totalMembersAssigned < N) {
if (groupSizes[index] < maxGroupSize) {
groupSizes[index]++;
totalMembersAssigned++;
}
index = (index - 1 + groupSizes.length) % groupSizes.length;
}
// シャッフル関数
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// メンバーをグループに割り当てる関数
function assignMembers() {
let groups = [];
for (let i = 0; i < G; i++) {
groups.push({
members: [],
originalGroups: new Set(),
specialMembers: new Set()
});
}
shuffle(members);
for (let member of members) {
let assigned = false;
let groupIndices = [...Array(G).keys()];
shuffle(groupIndices);
for (let groupIndex of groupIndices) {
let group = groups[groupIndex];
if (group.members.length >= groupSizes[groupIndex]) continue;
if (group.originalGroups.has(member.originalGroup)) continue;
if (specialMembers.includes(member.name) && group.specialMembers.size > 0) continue;
group.members.push(member.name);
group.originalGroups.add(member.originalGroup);
if (specialMembers.includes(member.name)) group.specialMembers.add(member.name);
assigned = true;
break;
}
if (!assigned) return null;
}
return groups;
}
// 割り当てを最大100回試行
let groups = null;
for (let attempt = 0; attempt < 100; attempt++) {
groups = assignMembers();
if (groups !== null) break;
}
if (groups === null) {
console.log('メンバーの割り当てに失敗しました。');
} else {
// グループリーダーを選択
for (let group of groups) {
let potentialLeaders = group.members.filter(name => !nonLeaders.includes(name));
if (potentialLeaders.length == 0) potentialLeaders = group.members;
shuffle(potentialLeaders);
let leader = potentialLeaders[0];
group.members = group.members.map(name => (name === leader ? '⭐️' + name : name));
}
// 結果を表示
for (let i = 0; i < groups.length; i++) {
const row = groups[i].members.sort((a, b) => {
if (a.includes("⭐️")) {
return -1
} else {
return 1
}
}).map(item => item.replace("⭐️", ""))
console.log(`グループ ${i + 1}:`, row.join(', '));
const targetRange = sheet.getRange(2 + i, 2, 1, row.length); // B列の2行目から1行ずつ範囲を設定
targetRange.setValues([row]); // 1行分のデータをセット
}
}
// 履歴にあるデータと一緒の場合、履歴は残さない(同じグループメンバーで再実行したとみなす)
let prevHistory;
try {
prevHistory = getMembersByGroup(historySheetName)
if (arraysAreEqual(prevHistory, membersByGroup)) return
} catch {
// 無条件で履歴を残す
}
// 履歴を残す
const history = membersByGroup.map((members, index) => [`Group ${index+1}`, ...members])
const historySheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(historySheetName);
for (let i = 0; i <= history.length; i++) {
historySheet.insertRowBefore(1)
}
for (let i = 0; i < history.length; i++) {
const row = history[i]
const targetRange = historySheet.getRange(2 + i, 1, 1, row.length); // B列の2行目から1行ずつ範囲を設定
targetRange.setValues([row]); // 1行分のデータをセット
}
}
function getMembersByGroup(prevMembersByGroupSheet) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(prevMembersByGroupSheet);
const lastRow = sheet.getLastRow(); // データが入力されている最後の行を取得
const range = sheet.getRange(2, 2, lastRow - 1, 6); // B列からG列、2行目から最後の行まで
const rawData = range.getValues()
let data = []
for(let i = 0; i < rawData.length; i++){
const row = rawData[i]
if(row[0] === "") break
data.push(row.filter(member => member !== "")) // 2次配列としてデータを取得(空欄の余計な行は除外)
}
Logger.log(data); // デバッグ用にログに出力
return data;
}
function getSpecialMembers(prevMembersByGroupSheet) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(prevMembersByGroupSheet);
const lastRow = sheet.getLastRow(); // データが入力されている最後の行を取得
const range = sheet.getRange(2, 9, lastRow - 1, 1); // I列(9列目)の2行目から最後の行まで
const data = range.getValues().flat().filter(member => member !== ""); // 2次元配列を1次元配列に変換
Logger.log(data); // デバッグ用にログに出力
return data;
}
function getNonLeaders(prevMembersByGroupSheet) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(prevMembersByGroupSheet);
const lastRow = sheet.getLastRow(); // データが入力されている最後の行を取得
const range = sheet.getRange(2, 10, lastRow - 1, 1); // J列(10列目)の2行目から最後の行まで
const data = range.getValues().flat().filter(member => member !== ""); // 2次元配列を1次元配列に変換
Logger.log(data); // デバッグ用にログに出力
return data;
}
function arraysAreEqual(array1, array2) {
// 配列の行数が異なる場合は false
if (array1.length !== array2.length) return false;
for (let i = 0; i < array1.length; i++) {
// 各行の列数が異なる場合は false
if (array1[i].length !== array2[i].length) return false;
for (let j = 0; j < array1[i].length; j++) {
// 要素が異なる場合は false
if (array1[i][j] !== array2[i][j]) return false;
}
}
// すべての要素が一致する場合は true
return true;
}