見出し画像

【GAS】Googleスプレッドシート自作関数で シート情報を取得する -2

これは本編のシリーズネタとは別で、旬の話題や Googleスプレッドシート、GoogleWorkspace関連でランダムに気になったことを書いていく 雑談記事です。土日に新しい記事を出していこうかなと思います。

前回の記事
【GAS】Googleスプレッドシート自作関数で シート情報を取得する -1

LAMBDAやXLOOKUPはうれしいけど、Excelからの 輸入じゃなくてオリジナル新関数でこれ欲しかったよ~ と願う シート名やシートIDを取得する関数を GAS で自作しています。

前回は、1つのスプレッドシート内にある 全シート名、全シートIDを取得する関数の作成まで出来ました。



シート名/シートIDを取得する関数をブラッシュアップしよう

1. 式を入れたシート(アクティブなシート)のシート名
2. 式を入れたシート(アクティブなシート)のシートID
3. そのスプレッドシートの全シート名、全シートID
4. オプション(引数を追加)で取得する内容を 制御してみる

今回は 4. オプション(引数を追加)で取得する内容を 制御してみる にチャレンジして、より便利な 自作関数にブラッシュアップしたいと思います。



1つの式で シート名、シートIDを返す自作関数

  • シート名の取得

  • シートIDの取得

前回は 別々の関数として作成していましたが、まずはこれを1つにまとめてみましょう。

今回はもうQA方式でなく、順番にコード検証・機能追加をしていきます。



シート名、シートID 両方返す自作関数

function sheetNames2(){
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheets = ss.getSheets();
  sheets = sheets.map(sheet => [sheet.getName(), sheet.getSheetId()]);
  return sheets;
}

関数名はなんでもいいんですが、とりあえず sheetNames2 としておきましょう。

コード内の const ではなく let sheets と、let を使ってる箇所は 理由はわかりますか?その日の気分じゃないですよ。

次の行で再代入するからなんですが、この辺りの基礎的な補足などプログラミング系の記事はどこまで(どこから)説明したらよいか悩ましいところです。

なるべく わかりやすく、でも 基本ばかりにもならないように、よそ様のページも参考にしつつ書いていきましょう。

前回も登場した map + アロー関数 で、sheets(全シートのシートオブジェクト配列)内 の個々の sheet を

 [sheet.getName(), sheet.getSheetId()]

とすることで、シートオブジェクトを シート名、シートIDという(スプレッドシート的には横並びの)配列に変換しています。これが各シート毎に繰り返されるイメージ。

GASで配列をスプレッドシートに書き込む(setValues する)時は、二次元配列にしたり、書き込み先の 範囲を getRange で配列と同じサイズで取得する必要があったりと、結構気にかけるポイントが多いんですが、自作関数だとその辺りをあまり意識せず return で書き出せちゃうんで楽ですね。

1つの式でシート名、シートIDが返せましたが、ケースによってはどちらかのみ使いたいという時もあります。

まずは、この制御を引数でコントロールしてみましょう。



swtich文で 出力モードを制御してみる

出力の切り替えですが、sheetNames2() に 引数 mode を渡して sheetNames2(mode) として切り替えるようにしてみましょう。

■mode で以下のように切り替える
 1. シート名のみ
 2. シートIDのみ
 0(もしくは空欄). シート名、シートIDの両方(デフォルト値)

コードはこんな感じ

function sheetNames2(mode=0){
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheets = ss.getSheets();

  //モードを確認し出力を制御 
  switch (mode){
    case 1:
    sheets =sheets.map(sheet => [sheet.getName()]);//シート名のみ
    break;

    case 2:
    sheets =sheets.map(sheet => [sheet.getSheetId()]);//シートIDのみ
    break;

    default:
    sheets =sheets.map(sheet =>  [sheet.getName(),sheet.getSheetId()]);//両方
  };

  return sheets;
}

switch文による分岐で caseごとに map処理を記述してみましたが、もっといい方法ないんかな? (処理を 別関数にしてもいいんですが)

記述的にすっきりさせる方法はあるけど、使わないのに 両方取得したり、map 内でswitchすると無駄が多いし・・・。

switch使う時は、ケースごとの break 入れ忘れに注意です。 

VBAと違って型の指定がないのが、良くも悪くも javascriptって感じです。

一応 引数のデフォルト値を 0ってことで、sheetNames2(mode=0) としてるけど、今回の場合は switch の最後の default: 処理で1,2以外は全部拾っちゃいますねw

まぁここは別に問題ないでしょう。

100入れても 0と同じ扱い

とりあえず この式をベースにして、他の条件の引数を追加していきましょう。



対象とするシートの条件を 引数で制御してみる

どんな条件(引数)が必要かを考える

出力対象とするシートを絞り込む際に、どんな条件で絞り込めたら便利かを考えてみました。

・アクティブシート(式を入れたシート)を含めるか否か
・非表示のシートを含めるか否か
・シート〇番目から〇番目といった 範囲設定ができる

この辺りの機能があると良さそうですね。

式が入っているシート(アクティブシート)の除外は、他のシートの値を合計するサマリーシートで必要になりそう。

非表示のシート除外は 前回のような 全シートINDEXを生成する際に、非表示のシートを含めたくない場合もあるでしょう。

あと、シート〇番目から〇番目 といった範囲指定があると、いわゆる EXCELでいうところの シート範囲を指定した 串刺し計算 に使えるかなと。


自作関数の Goalイメージとしては、

=sheetNames3(mode, self, hidden, start, end)
mode
出力モード  0 両方、1 シート名、 2 シートID (省略時は 0)
self このシートを除外するか? trueで除外 (省略時は false)
hidden 非表示シートを除外するか? trueで除外 (省略時は false)
start 開始シート番号 (省略時は1)
end 終了シート番号(省略時は末尾)

hidden true(非表示シート除外)時は、表示されているシート内での番号
self は trueとした場合も シート番号には影響しない

こんな式を作ってみましょう。

※引数は 条件を適用する時を true として、省略時を false扱いとした方が楽です。

引き続きQA方式ではなく、ステップを踏んで解説していきます。


self このシートを除外するか?

mode は既に sheetNames2 の式で実装済みなので、self 条件から組み込んでいきましょう。

const ss = SpreadsheetApp.getActiveSpreadsheet();
const activeSheet = ss.getActiveSheet();

式の入っているシートの取得は、前回もやりましたね。

既に アクティブな スプレッドシートを取得し ss としているので、 ss.getActiveSheet() でアクティブなシート(この式が入っているシート)が取得できます。

let sheets = ss.getSheets();

全シート(sheets)から、アクティブシートを除外すればよいのですが、どちらも シートオブジェクトなのでこのままでは比較ができません

シートオブジェクト って概念は、なかなかわかりにくいかもしれませんが、そのシートを操作したり、そのシートの情報を取得したりする為のシートそのもの() という理解でよいかなと思います。

「いつも隣にITのお仕事」さんでも少しオブジェクトについて触れているので、そちらもぜひご確認を。

今回のケースでは最終的に シート名やシートIDを返すので、「シート名」もしくは「シートID」という ユニークな文字列に変換してから比較することで、アクティブシートを除外すれば良いわけです。

というわけで、まずはこんな感じにしてみました。

function sheetNames3(mode=0,self){
  const ss = SpreadsheetApp.getActiveSpreadsheet();

  //全シートを取得
  let sheets = ss.getSheets()

  //モードを確認し出力を制御
  switch (mode){
    case 1:
    sheets =sheets.map(sheet => [sheet.getName()]);//シート名のみ
    break;

    case 2:
    sheets =sheets.map(sheet => [sheet.getSheetId()]);//シートIDのみ
    break;

    default:
    sheets =sheets.map(sheet =>  [sheet.getName(),sheet.getSheetId()]);//両方
  }; 

  //selfフラグが true の場合は 式が入っているシートを除外
  const activeSheet = mode == 2? ss.getActiveSheet().getSheetId() : ss.getActiveSheet().getName();
  sheets = self? sheets.filter(sheet => sheet[0] != activeSheet) : sheets ;


  return sheets;
}

アクティブシート除外の部分の記述をシンプルにするために、switch文での mapによる変換を 全てのケースで [ ] をつけて 配列で返す形にしました。

後半のself フラグの条件処理 2行は、どちらも 三項演算子で 記述しています。

const activeSheet = mode == 2? ss.getActiveSheet().getSheetId() : ss.getActiveSheet().getName();

まず、この部分で activeSheet という変数に mode が2の時だけ アクティブシートのシートIDを、それ以外の時はアクティブシートの シート名をセットしています。


  sheets = self? sheets.filter(sheet => sheet[0] != activeSheet) : sheets ;

その上で、この部分 self? つまり self がture なら、 filterで sheets(配列)を アクティブシート と一致しない ( != で一致しない)ものだけに絞りこみ、 selfがtureでないなら そのまま sheets を返す、としています。

filterメソッドは アロー関数を組み合わせた場合は、後ろの式で true となる要素だけに絞り込まれます。感覚的には シート関数のFILTERと同じようなイメージ。

mode によって 以下のような中身・比較になっていると思ってください。
※配列のindex番号(添え字)は 0 開始です。

mode が1だったら
sheets ・・・[[シート名1],[シート名2],[シート名3]…] という二次元配列
sheet ・・・ sheetsの要素を1つずつ取り出したもの (1つ目なら [シート名1])※この時点ではまだ配列
sheet[0] ・・・ シート名1 ※配列 [シート名1] の1つ目の要素(中身) 
activeSheet ・・・ アクティブシート名
sheet[0] != activeSheet で 各シートのシート名と アクティブシート名を比較して != (一致しない)ものに絞り込む

以下、重複する説明は省略

mode が2だったら
sheets ・・・[[シートID1],[シートID2],[シートID3]…]
sheet ・・・  上記同様 (1つ目なら [シートID1] 
sheet[0] ・・・ シートID1
activeSheet ・・・ アクティブシートID
sheet[0] != activeSheet で 各シートのシートIDと アクティブシートIDを比較して != (一致しない)ものに絞り込む


mode0 デフォルトなら
sheets ・・・[[シート名1,シートID1],[シート名2,シートID2],[シート名3,シートID3]…]
sheet ・・・  上記同様 (1つ目なら [シート名1,シートID1]
sheet[0] ・・・ シート名1 ※配列 [シート名1,シートID1] の1つ目の要素(つまり 先頭の要素 シート名1の方)
activeSheet ・・・ アクティブシート名
sheet[0] != activeSheet で 各シートのシート名と アクティブシート名を比較して != (一致しない)ものに絞り込む

これを保存してシート上で試してみると、第2引数の self に trueを入れたときに アクティブなシート(この場合は1枚目の「おシート」)が除外されているのがわかります。

mode 0、mode2 の時も 同様

アクティブシートの抽出制御ができました。



hidden 非表示シートを除外するか?

次に非表示シートの除外部分をコードに加えていきましょう。

シートが非表示かどうかを判別するには、isSheetHidden というメソッドがあるので、これを使えば良さそう。

シートオブジェクトに対して使うメソッドなので、シート名やシートIDに変換する前に記述した方がよいですね。

いっそのこと sheets に格納するタイミングで filter処理しちゃいましょう。

function sheetNames3(mode=0,self,hidden){
  const ss = SpreadsheetApp.getActiveSpreadsheet();

  //全シートを取得(非表示 trueの時は絞り込み)
  let sheets = hidden? ss.getSheets().filter(sheet => !sheet.isSheetHidden()) : ss.getSheets();

  //モードを確認し出力を制御
  switch (mode){
    case 1:
    sheets =sheets.map(sheet => [sheet.getName()]);//シート名のみ
    break;

    case 2:
    sheets =sheets.map(sheet => [sheet.getSheetId()]);//シートIDのみ
    break;

    default:
    sheets =sheets.map(sheet =>  [sheet.getName(),sheet.getSheetId()]);//両方
  }; 

  //selfフラグが true の場合は 式が入っているシートを除外
  const activeSheet = mode == 2? ss.getActiveSheet().getSheetId() : ss.getActiveSheet().getName();
  sheets = self? sheets.filter(sheet => sheet[0] != activeSheet) : sheets ;


  return sheets;
}

変更したのは

//全シートを取得(非表示 trueの時は絞り込み)
  let sheets = hidden? ss.getSheets().filter(sheet => !sheet.isSheetHidden()) : ss.getSheets();

ここだけですね。同じく三項演算子による処理です。

hidden? で 引数 hidden がtrueなら、

ss.getSheets().filter(sheet => !sheet.isSheetHidden())

filter でシートを 表示されているシートだけに絞り込む(非表示シートを除外する)。という処理をしています。

isSheetHidden は、シートが非表示だと true を返すので、 !をつけて反転 させています。 !は論理演算子ってやつで シート関数だと NOT みたいなものですね。

true ⇔ false を切り替えるのに使ってます。

実際に 第3引数 hiddenを true にすると 非表示シートが除外されているのがわかります。

あシート、えシートが非表示シート

非表示シートも制御ができました。



start /end  で抽出範囲を指定する

最後に開始シート番号、終了シート番号の引数で、抽出するシート範囲を制御できるようにしましょう。

function sheetNames3(mode=0,self,hidden,start,end){
  const ss = SpreadsheetApp.getActiveSpreadsheet();

  //全シートを取得(非表示 trueの時は絞り込み)
  let sheets = hidden? ss.getSheets().filter(sheet => !sheet.isSheetHidden()) : ss.getSheets();

  //モードを確認し出力を制御
  switch (mode){
    case 1:
    sheets =sheets.map(sheet => [sheet.getName()]);//シート名のみ
    break;

    case 2:
    sheets =sheets.map(sheet => [sheet.getSheetId()]);//シートIDのみ
    break;

    default:
    sheets =sheets.map(sheet =>  [sheet.getName(),sheet.getSheetId()]);//両方
  };

  //開始値、終了値を定義し sliceで切り取り
  start = start || 1;
  end = end || sheets.length ;
  sheets = sheets.slice(start-1,end); 

  //selfフラグが true の場合は 式が入っているシートを除外
  const activeSheet = mode == 2? ss.getActiveSheet().getSheetId() : ss.getActiveSheet().getName();
  sheets = self? sheets.filter(sheet => sheet[0] != activeSheet) : sheets ;

  return sheets;
}

シート番号は

  • 「非表示シートを除外」とした場合は、非表示シートを含めずカウント

  • 「このシートを除外」としても カウントには アクティブシートを含める

こんなルールにしました。

非表示シートは 番号に含めない方がわかりやすいですが、アクティブシートは番号から除外すると逆にわかりにくくなるからです。

というわけで、範囲部分に切り取る処理は、self フラグ処理の手前、switchを抜けたあとに入れています。

  //開始値、終了値を定義し sliceで切り取り
  start = start || 1;
  end = end || sheets.length ;
  sheets = sheets.slice(start-1,end); 

この部分ですね。

ここはせっかくなんで記述で 論理和 (||) の短絡評価を使った記述にしています。

自作関数として利用する際、引数の start , end を省略した場合は、書き方によって以下の違いがあります。

start = start || 1;
end = end || sheets.length ;


未記入
=sheetNames3(1,true,true, ,  )
※カンマで区切られた枠はあるけど中身がない状態
→ GAS上ではstart,end は 空文字扱い
未設定 =sheetNames3()
※そもそもカンマで区切られた 枠がない状態
→ GASではstart,end は underfind

|| を使った記述部分で、左側 start や end に数値が入っていれば その数値がそのまま使われ、  start や end が ''(空文字)、null、underfind の時は false扱いとなり、右側が start, end に代入されるという処理をしています。

ショートハンド(コード記述の簡略)として使われる書き方です。

start,end の省略時(空白、underfind) は、

start = start || 1;
end = end || sheets.length ;


start 開始シート ・・・1 (先頭のシート)
end 終了シート ・・・ sheets.length シート配列の要素の数 = シートの枚数(最後のシートのシート番号)

としています。
※シート枚数は hidden がtrueの場合は 表示されいているシート枚数

sheets = sheets.slice(start-1,end);

最後の 配列の切り取りは slice を使用。ここで 配列上の 1番目は index 0 なので、staart -1 としています。

一方 endは -1する必要はありません。 日本人的感覚だと start が1、endが5なら、シートの1枚目から5枚目までという意味合いで end を最後の要素として含めたくなりますが、 slice の end はその手前までが切り取り範囲で、end 自体は含まれないのです。

よって、endが 5 だった場合は 手前の index 4までが対象となりますが、0スタート換算なので ちょうど 1~5枚目が 対象となるわけです。

どちらも未入力の場合は、start が 1、endが シート枚数(12) になるので、

sheets.slice(0,12)シートINDEX 0~11まで、つまり全てのシートを返すことになり、範囲選択なしと同じになります。

全ての引数による制御をコーディングできました。
とりあえず完成です。



自作関数の入力時に説明を表示させる

自作関数は完成しましたが、引数が多いし制御ルールも複雑なので、式を入力する際の説明が欲しいところ。

実はこれ GAS上の記述で出来るんです。

リファレンスの Custom Functions の Autocomplete(オートコンプリート)ってところに記載があります。

こちらの例にならって、コードに追記してみましょう。


オートコンプリート部分を追記する

説明を追記した、今回のシート名/シートID 自作関数の 最終形となります。

/**
 * シート名,シートIDを返す GAS関数。 ※非表示シート除外の際はシート番号に含めない
 * @param {2} mode 出力モード 0 両方、1 シート名のみ、 2 シートIDのみ (省略時は 0)
 * @param {true} self このシートを除外するか? (trueで除外、省略時は false)
 * @param {true} hidden 非表示シートを除外するか? (trueで除外、省略時は false)
 * @param {1} start 開始シート番号 (省略時は1)
 * @param {2} end 終了シート番号(省略時は末尾)
 * @return {シート名} シート名/IDの並びの配列
 * @customfunction
 */
function sheetNames3(mode=0,self,hidden,start,end){
  const ss = SpreadsheetApp.getActiveSpreadsheet();

  //全シートを取得(非表示 trueの時は絞り込み)
  let sheets = hidden? ss.getSheets().filter(sheet => !sheet.isSheetHidden()) : ss.getSheets();

  //モードを確認し出力を制御
  switch (mode){
    case 1:
    sheets =sheets.map(sheet => [sheet.getName()]);//シート名のみ
    break;

    case 2:
    sheets =sheets.map(sheet => [sheet.getSheetId()]);//シートIDのみ
    break;

    default:
    sheets =sheets.map(sheet =>  [sheet.getName(),sheet.getSheetId()]);//両方
  };

  //開始値、終了値を定義し sliceで切り取り
  start = start || 1;
  end = end || sheets.length ;
  sheets = sheets.slice(start-1,end); 

  //selfフラグが true の場合は 式が入っているシートを除外
  const activeSheet = mode == 2? ss.getActiveSheet().getSheetId() : ss.getActiveSheet().getName();
  sheets = self? sheets.filter(sheet => sheet[0] != activeSheet) : sheets ;

  return sheets;
}

シート側で自作関数入力時に表示される説明 と コード側で記述するドキュメンテーションの 対比は以下のようになっています。

このコードで保存し、シート側で 自作関数 sheetName3 を入力してみると

入力の途中で関数候補に登場し、さらにヘルプに設定した説明が表示されました。

これは便利ですね。

細かい設定部分や意味合いについては 例によって 「いつも隣にITのお仕事」さん が参考になります。

自作関数における @return の部分は、どこにも表示されないんで 意味があるかわかりませんが・・・。(mirが理解してないだけ?)



完成した 自作関数の動作チェック

実際に動かしてみましょう。わかりやすいように、全て引数はセル参照にしています。

mode: 出力モード切替

self: 自分のシートを 含める/含めない

hidden: 非表示シートを 含める/含めない

start / end: シート範囲設定

イメージ通りの動きになりました。
完成です!



リンク付き シート INDEX(目次) にどう使うか?

前回作成した 自作関数と HYPERLINK関数 を組み合わせた シートINDEX(全シート目次)は、今回ブラッシュアップした関数だと どう作ればよいでしょうか?

前回だと シートINDEXは シート名を返す関数、シートIDを返す関数が 分かれていたので2つの関数を使って作成しましたが・・・。せっかく1つに集約したので この sheetNames3 を 1回使うだけで 実現したいですね!

ここで登場するのがLAMBDAです。


新 リンク付き シート INDEX(目次) 式

=LAMBDA(sheets,ARRAYFORMULA(HYPERLINK("#gid="&INDEX(sheets,,2),INDEX(sheets,,1))))(sheetNames3(0,true))

LAMBDAで  sheetNames3(0,true) から返される シート名、シートIDの 2列になっている配列を sheetsと置いてしまうのが良いでしょう。

自シート(目次シート)を含めないように 第2引数の self を trueとしています。非表示シートも除外したい場合は 第3引数にも true を入れておきましょう。

シート名、シートID はそれぞれ sheets の1列目、2列目になるので、ここは INDEXで取得すれば、あとは前回と同じ HYPERLINK関数と ARRAYFORMULAで OKです。

自作関数は 負荷が大きいので、なるべく利用数は絞っておきたいですね。




スマホ(アプリ版)での動作を確認する


おまけです。スマホアプリ版でも稼働することを確認しておきましょう。

上記の通り残念ながら アプリ版だと 最後に記述した 説明(オートコンプリート)部分は表示されませんが、正しく引数を入れれば 機能していることがわかります。

処理が少し重い気がしますが、GAS自作関数が スマホでも使えるのは便利ですね。



前回の自作関数をアップデートして、引数で出力を制御できる自作関数が完成しました。これでシート情報(シート名、シートID)の取得はOKですね。

次回は 自作関数シリーズの最後、完成した sheetNames3 を 既存のシート関数と組み合わせて、具体的に何ができるか? を検証していきます。

シート指定の串刺し計算や、全シート検索、さらに 複数シートの範囲結合など、GASが続いたので久々にシート関数 LAMBDA関数(REDUCE) や ArrayformulaQUERY関数 などフル活用で 色々検証してみましょう。



■このシリーズの次の記事


いいなと思ったら応援しよう!