Open3

ChatGPTにシャッフルランチのグループ分けのロジックを考えてもらう

Naoto ImamachiNaoto Imamachi

以下の内容でお願いした。

以下の二次配列は、グループごとのメンバーの名前を表しています。
以下のルールに従って、メンバーをシャッフルして、グループを再編成するコードを書いてください。

  • メンバーをシャッフルして、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)']
]
Naoto ImamachiNaoto Imamachi

出来上がったコード

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(', '));
    }
}
Naoto ImamachiNaoto Imamachi

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;
}