📘

総当たりケース作成

2024/06/16に公開

概要

Googleスプレッドシートで見出しとケースパターンを記載すると、総当たりの表を作成する処理。

設定値入力シート 結果出力シート

画像の例は子音、母音、濁点の有無でマトリクスを作った場合の入力例。
「や行」は「い」と「え」の文字が無い、「あ行」は濁点が無い、などの除外条件が思いつきやすく、ケースが3種類あるのでこれをサンプルとした。

入力値の仕様

タイトル

セルにケースのタイトルを記載する。
種類を増やす場合は列方向に伸ばしていく。

ケース

タイトルに対するケースをセル内改行で複数記載する。

除外ケース

総当たりのためにケースを並べた時点でありえない組み合わせが出る時が有る。
その組み合わせをjavascriptオブジェクトの形式で記載する。

総当たりケース作成

総当たりケースをどうやって作る?
ループ処理にするとしても階層が可変。
再帰処理で作るとしたら関数の外で保持する値が増えて汚い。(個人の感想)
総当たりの合計ケース数は各ケース数の掛け算なので算出は簡単。
何行目の時に各列はどの値を出すのかをどう考えるか…
子要素が1周したら親要素が次に進む、という構造と考えた場合…
一番下はループカウントを要素数で割った余りが要素のインデックスになる。
その一つ上はループカウントを子要素の要素数で割った数を自分の要素数で割った余りが要素のインデックス、この考えで行けそうな気がする。

例えば、ケースAは2種類、ケースBは3種類、ケースCは2種類とする。
ループカウンタ、各ケースの表示したい配列インデックス、総件数/子要素の総数を記載すると、

cnt A(2種) cnt/6 B(3種) cnt/2 C(2種)
0 0 0 0 0 0
1 0 0 0 0 1
2 0 0 1 1 0
3 0 0 1 1 1
4 0 0 2 2 0
5 0 0 2 2 1
6 1 1 0 3 0
7 1 1 0 3 1
8 1 1 1 4 0
9 1 1 1 4 1
10 1 1 2 5 0
11 1 1 2 5 1

タイトルの配列、ケースの二次元配列作成

タイトルはただの文字列配列にする。
ケースはセル内改行で分割した配列をもつ配列に変換する。

const titles = inputSheet.getRange(titleRange.getRow(), titleRange.getColumn(), 1, inColLen).getValues()[0];
const columns = inputSheet.getRange(caseRange.getRow(), caseRange.getColumn(), 1, inColLen).getValues()[0].map(elm => elm.split("\n"));

総ケース数を求める

ケースを二次元配列としているので、1階層目をループして2階層目のサイズをかけていく。
二次元配列を要素数の一次元配列に変換して、1で初期化した変数に*=でどんどん乗算していく。

calcRows(columns) {
  const lenArray = columns.map(elm => elm.length);
  let rows = 1;
  lenArray.forEach(elm => rows *= elm);
  Logger.log(`総ケース数:${rows}(${lenArray})`);
  return rows;
}

階層毎の子要素数を求める

こちらは二重ループになる。
自分の子要素のケース数を求めるため、子要素の要素数を求めるループ処理はカウンタの初期値が親ループのカウンタの現在地より1多くなる。
ループカウンタの初期値がカウンタ最大値を超えていても例外にならないので余計な判定は要れないで済む。
カウンタの現在地を意識する処理なので、mapやforEachを使う方式としていない。

_calcDepthRows(columns) {
  const length = columns.length;
  const deptRows = [];
  for (let i = 0; i < length; i++) {
    let rows = 1;
    for (let j = i + 1; j < length; j++) {
      rows *= columns[j].length;
    }
    deptRows.push(rows);
  }
  Logger.log(`深さ別重複行:${JSON.stringify(deptRows)}`);
  return deptRows;
}

総当たりを生成する

1行毎に列方向にループしてケースを取得していく。
1列目は2列目以降の総数でループカウンタを割って整数にした値を1列目の要素数で割った余りを配列のインデックスにする。
そんな考えを二重ループで実現する。

createRoundRobinValues(columns) {
  const crossValues = [];
  const rows = this.calcRows(columns)
  const depthRows = this._calcDepthRows(columns);
  for (let rowIdx = 0; rowIdx < rows; rowIdx++) {
    const rowValues = [];
    for (let colIdx = 0; colIdx < columns.length; colIdx++) {
      const idx = parseInt(rowIdx / depthRows[colIdx]) % columns[colIdx].length;
      rowValues.push(columns[colIdx][idx]);
    }
    crossValues.push(rowValues);
  }
  Logger.log(`総当たりケース:${JSON.stringify(crossValues)}`);
  return crossValues;
}

これで総当たりになった配列が作成される。

除外ケース

これをどうやって定義するべきか。
どんな形であればループ処理とかに乗せやすいか?
とりあえず、AND条件となるものをオブジェクトにして、それを配列でもてば数の変化に対応できる.
オブジェクトのキーは列の位置(0始まり)とすれば、ループ内で配列のどれを取るかにも使える。

除外設定の配列化

セルの中にJSONで書いてもらえればそのまま使えるが、先頭と末尾のような必ず付く部分まで書かせないような方式としたい。
なので、プログラム側で文字列を補完して配列に変換する。

const ignoreCase = JSON.parse("[" + inputSheet.getRange("排除ケース").getValue() + "]");

総当たりケースとの紐付

引数に作成済みの総当たり配列、除外パターンの配列を受け取る。
総当たりの1行毎に、除外パターンの中でヒットするものが有るかを判定する。
除外パターンのオブジェクトの要素はどれか一つでもヒットしなければその除外判定で対象外。
ただし、除外パターンのオブジェクトは複数ある。
この考慮のために、除外パターンの判定ごとに除外するつもりでフラグをtrueで初期化し、その除外パターン内で該当なしならfalseにしてbreakする。
最終的に除外となったものは"×"、除外とならない場合は空白文字とする。
スプレッドシートには二次元配列で貼るのが早いので、要素1個の配列をpushしていく。

  createIgnoreValues(caseRows, ignoreCase) {
    const ignoreRows = [];
    for (let i = 0; i < caseRows.length; i++) {
      const row = caseRows[i];
      let isIgnore = true;
      for (const igCase of ignoreCase) {
        isIgnore = true;
        for (const igIdx of Object.keys(igCase)) {
          const _c = igCase[igIdx];
          const _v = row[parseInt(igIdx)];
          const _i = igCase[igIdx].indexOf(_v);
          if (_i < 0) {
            isIgnore = false;
            break;
          }
        }
        if (isIgnore) {
          break;
        }
      }
      if (isIgnore) {
        ignoreRows.push(["×"]);
      } else {
        ignoreRows.push([""]);
      }
    }
    Logger.log(ignoreRows);
    return ignoreRows;
  }

どうして作ったのか

エクセルで組み合わせ爆発を手書きでやる作業が有ったので。
そんのは人間がやる作業ではない。
でもChatGPTとかは使わせてもらえない。
せっかくだから休みの日に作ってみた。

ソース

GitHubに公開設定で配置済み
https://github.com/ModokiSealsky/GoogleAppsScript/tree/main/roundrobincase

Discussion