縦にも横にも関連データが広がるシートを一行に整形する【GoogleAppsScript】

GoogleAppsScript書き始めて二日目。

言語仕様はそのままJavaScriptらしいがC++しかまともにやってないのでなんもわからん。

ただまあ、for文の書き方はC++とほぼ同じだしvar let constの仕様さえ覚えればC++のつもりで書けるみたいなのでなんもわからんが何故かマクロが動いたのでよし。

理想的なデータ構造

個別ID 名前 項目1 項目2 項目n...
1 りんご バラ科 しゃりしゃり etc...
2 ごりら ヒト科 ごりらごりら etc...
... ... ... ... ...

こういうデータだと個別IDで検索をかけるだけでほしいデータをすぐに取り出せて良い。

今回整形するデータ

個別ID 項目
1 りんご うまい
バラ科
しゃりしゃり
赤い
apple
2 ごりら
ごりらごりら
うほうほ
森の賢者
3 ピカソ
パブロ
ディエゴ
ホセ
...

タイトルの通り、縦と横に関連データが広がっていてとても使いにくい。

そもそもこんなデータを作らないでほしい。

手作業で解決するにはコピーして転置貼り付けして……を繰り返すしか無いが、

今回はマクロを組んで解決した。3000行ぐらいあるデータを手作業で整形できるわけがなかった。

1:個別IDが書き込まれている行番号の配列を作る

GAS(GoogleAppsScript)にはスプレッドシート操作に便利なAPIが用意されているのだが、

いちいちそれを呼び出していると実行速度が遅くなりあまり良くない。

そのため必要なデータをシート側の関数を用いて作っておく。

今回のデータはA列に唯一無二のIDが書き込まれており、

あるIDをもつセルの行番号から次のIDを持つセルの行番号-1までの範囲をコピーして転置したい、ということにする。

空いている列に=IF( ISBLANK(A1), ,ROW( ) )と入れてオートフィルで表の下まで入力

するとIDがある行番号が返されるので、フィルタを作成して空白を除外表示する

f:id:amvirosa:20220107184838p:plain

f:id:amvirosa:20220107185007p:plain

このままコピーして別シートに値のみ貼り付けをすると行番号配列が出来上がる。後でGAS側で読み込むのでシート名もわかりやすくリネームしておく

ついでに結果を書き込む新しいシートも用意しておくと失敗したときに大変なことにならなくて良い(3敗)

2:GoogleAppsScriptで書く

行番号配列row_indexs[i]とrow_index[i+1]の要素が元データでコピーしたい範囲をだいたい示している。

function trimDataToRow(){
 const read_file = SpreadsheetApp.openById("スプレッドシート固有ID");
 const row_indexs = read_file.getSheetByName("行番号配列があるシート名").getRange(/*行番号配列の範囲*/).getValues();
 const read_datas =read_file.getSheetByName("クソデータがあるシート名").getRange(/*転置したいデータがある範囲*/).getValues();

 const set_sheet = read_file.getSheetByName("書き込むシート名");

 for(let i=0;i<row_indexs.length-1;i++){
  let rows = { begin: row_indexs[i], end:row_indexs[i+1] };
  let temp_array = [];
  for(let j=rows.begin; j<rows.end-1; j++){
   temp_array.push(String(read_datas[j]));
  }
 set_sheet.appendRow(temp_array);
 }
}

実行するとこのようになる。

f:id:amvirosa:20220107195406p:plain

3:完成!

IDと同じ列はフィルタの空欄除外で抽出できる。

f:id:amvirosa:20220107195919p:plain

GASで作ったデータと組み合わせれば完成。

f:id:amvirosa:20220107200136p:plain

実行速度とかの話

クソデータの行数が4000近くあるデータでも実行時間制限にひっかからずに書き出せる。

ひっかからないのだが、処理が遅い。

f:id:amvirosa:20220107200850p:plainf:id:amvirosa:20220107201013p:plain

上記コードはfor(let i)のループ回数につき一回.appendRow();をしているのでそこがボトルネックになっている。

速度を求めるならループ中で二次元配列を作った上で,ループを抜けた後に.getRange(書き込む範囲).setValues(二次元配列);を一回呼ぶのがよい。

しかしgetRangeにわたす範囲の指定とsetValuesにわたす二次元配列の範囲の指定は完全に一致していないといけないらしく、

クソデータ、もといジャグ配列を渡すのが面倒くさく諦めた。