Google Apps Scriptでカスタム関数を作る【GAS】
ExcelやGoogleスプレッドシートには標準で多数の「関数」が装備されており、表計算ソフトの基本中の基本になっています。これらの関数を組み合わせて、色々な処理を実装してる人も多いでしょう。
プログラミングを学習する上で、GASの場合はVBA同様に「いきなりプログラムを組む前に、自作のユーザ定義関数をまず挑戦する」というのが最初のステップです。コードを書く量も少なく、それでいてスプレッドシート上で呼び出すだけで使えるので効果をすぐに体感しやすいという点で、最初の一歩としてオススメの選択肢です。
このエントリーでは、GASを使って自分だけのオリジナルの関数を作ってみます。
今回利用するスプレッドシート等
- カスタム関数 : Google Spreadsheet
- Google スプレッドシートのカスタム関数
作成に当たっては色々と注意事項があります。後述の項目を参照してください。
ユーザ定義関数は通常Excelで用いられる用語で、Googleスプレッドシートの場合は「カスタム関数」と呼ばれます。JavaScriptで処理を構築するわけですが、Excel同様にそのままでは構築したファイル上でしか動作しません。
これらを他のファイルでも動作するようにする為には「ライブラリ化」であったり「アドオン化」をして導入する必要がありますが、これらは中級以上の講座で紹介する予定です。アドオン化することで、自分以外の人でも使え、また社内に配布することが出来るようになるので、そのレベルにまで到達を目指しましょう。
カスタム関数を作ってみよう
概要
豊富な標準関数が揃ってるのに、なぜ自前で関数を作る必要があるのだ?という人がいますが、標準関数で間に合ってる人というのはその範疇で賄える仕事をしてるだけという事でしかありません。また、当人が「当たり前」と思ってる手作業を自作の関数で処理が出来るようになると言っても、本人がその事を知らないが故に、自作の関数なんて要らないというケースもあります。
また、プログラミングではいきなり大きなアプリケーションを構築するのはかなりハードルが高いですが、単一の処理を計算して返すだけの関数であれば入門編にはもってこいの内容。VBAやGASはスプレッドシートという環境でそれが実現できるため、他の言語よりも一段下にハードルを設けることができるため、とっつき易いという特徴があります。
作成シーンと手法
もっとも単純な関数を作る
単純に1つの値を受け取って答えを返す、というカスタム関数の基本となるスタイルです。とは言えその構造そのものは、GASのFunctionを作るものと殆ど変わらない。
1 2 3 4 |
//単純に答えを返す関数 function doubleman(value){ return value * 2; } |
上記の関数は、受け取ったvalueの値を2倍にしてそのままreturnしています。ここまでがワンセット。returnの前までの間に色々な処理や計算を追加してスプシの標準関数で組むには面倒であったり、JavaScriptの豊富なメソッドを使って独自の関数を構築することが可能です。
もちろん、引数は複数渡すことが可能ですし、残余引数と呼ばれる引数の数が不確定な場合の受け取り方もありますが、初級編ではまずは単純で単一なカスタム関数から始めると良いでしょう。
また、こういった簡単な関数を作ろうというケースは「標準関数の場合は、関数の入れ子が多発して何をしているのかわかりにくくなり、関数のメンテナンスが大変」といった場合に使うケースが多いです。特にIFの入れ子で複数条件判定するような場合は有効です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function choice(selectval){ let temp = ""; switch(selectval){ case "キノコ": temp = 1; break; case "野菜": temp = 2; break; case "果物": temp = 3; break; } return temp; } |
上記のコードは、selectvalで受けた内容に応じてswitch文でそれぞれ判定し、最後に値を返します。スプシの関数にもSwitch関数がありますが、単純に複数判定する分には役立ちますが、さらに細かくそこから条件判定を入れたいとなると、入れ子になる全体の見通しが悪くなります。これをカスタム関数で装備すると、何をしているのか?がわかりやすくなるメリットがカスタム関数にはあります。
配列で返す関数を作る
スプレッドシートの標準関数にもある「配列で返す関数」をカスタム関数でも作成することが可能です。Filter関数やQuery関数が代表的ですが、数式を入れて実行すると複数行に渡って答えが返ってくるタイプになります。
受け取る引数は単一でも複数でも問題はないですが、コードの中で最後に返す関数は通常「二次元配列」で返すのが定石です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
//配列で返す function arrayfunc(cnt) { //IDの初期値 let uid = 1; //返却する用の配列 let array = []; //cntの数字を元に数値を生成する for(let i = 0;i<cnt;i++){ //乱数を生成する(0〜999の整数) let random = Math.floor(Math.random() * 1000); //一時配列を作る let temparr = [ uid, random ] //返却用に追加 array.push(temparr); //uidを加算する uid = uid + 1; } //配列を返す return array; } |
図:1から始まるIDとランダムな数値を100回生成する関数
範囲を受け取って処理をする関数
スプシの関数と言えばよく見かけるのが「範囲指定をして関数に渡し答えを得る」というスタイル。範囲を渡すとどうなるのか?
1 2 3 4 5 6 7 8 9 10 11 12 |
//2つの範囲を結合して返す関数 function mixRange(s1range,s2range) { //二次元配列を作成する var dataArray = new Array(); //作成した2次元配列に2つのレンジの配列を追加する Array.prototype.push.apply(dataArray,s1range); Array.prototype.push.apply(dataArray,s2range); //2次元配列を返してあげる return dataArray; } |
上記の関数のコードは「2つの範囲のデータを結合して1つの配列として返す」という関数になります。特徴的なのが
- 範囲であるs1rangeやs2rangeはA2:B5みたいな値が入ってるのではなく、その範囲内のデータがすでに配列になって送られてきます。
- JavaScriptのArray.prototype.push.applyにて、配列同士を縦に結合していきます。
- 最後にreturnで結合した配列を返します。
- SpreadsheetAppを使わずともカスタム関数の場合はこのように範囲内のデータを取得可能です
- 前述でも紹介してる「配列で答えを返す」ようにしてるため、回答は1個ではなく複数の範囲にわたって一括で出力されます。
図:2つの範囲を結合して返す関数
外部サービス連携する関数
カスタム関数では割と簡単に外部サービスと連携して答えを得るものを作成することが可能です。事前に外部サービスで提供してるREST APIのAPIキーが必要になります。
今回はGoogle Gemini 1.5 FlashのAPIを使って、スプシ上の値を元に答えを得るというものを作成してみたいと思います。まずは以下の手順でAPIキーを取得しておきましょう。このAPIは一定量まで無償で利用することが可能です。以下のエントリー内でGemini FlashのAPIキー取得手順がありますので参考にしてみてください。
また、利用上の注意点として以下のようなものがあげられます。
- あまり同時に多数の場所で関数を利用するとエラーになります(一気にリクエストが投げられて拒否されてしまいます)
- カスタム関数よりかは普通にGASのスクリプトとして作ったほうが良いケースも多いです。
- 他にも郵便番号から住所を逆引きする無料のAPIなどもあったりするので、使い所は結構あります。
- 配列で返すような仕組みの場合は、リクエスト過多を防ぐために、Utilities.sleep(500)のようにループの中でウェイトを入れておいたほうが良いです。一回のリクエストが終わったら0.5秒待機するといった関数になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
//APIキー var apikey = "ここに取得したAPIキーを入れる"; //リクエストエンドポイント var endpoint = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=" //Geminiに問い合わせをして回答を得る function geminiman(request) { //リクエスト用URLを構築 let url = endpoint + apikey; //リクエストボディを構築する var body = { "contents": [ { "parts": [ { "text": request //質問文がここに入る } ] } ], "generationConfig": { "temperature": 0.4, //回答のランダム性を制御する "topK": 50, //AIが考慮する選択肢の幅。小さいほど絞られ、高いほど多様性が増します "topP": 0.1, //AIが考慮する回答のうち、確率の高いものをどれだけ優先するか?低いほど選択肢が狭まります。 "maxOutputTokens": 2048, //Tokensの値が大きいほど多くの文字数で答えが返ってきます。 "stopSequences": [] } }; //リクエストオプションを構築する let options = { method: 'post', contentType: 'application/json', payload: JSON.stringify(body) } // リクエストを送信する。(Gemini 1.5 Flashを使用) let response = UrlFetchApp.fetch(url, options); // レスポンスをパースする。 var responseJson = JSON.parse(response.getContentText()); // 生成されたテキストを返す。 let answer = responseJson.candidates[0].content.parts[0].text; //答えを返す return answer; } |
- endpointとは、Gemini APIに質問を投げる先のURLです。REST APIでは必ず用意されています。
- bodyというものを構築します。requestをそのまま質問文として今回は当てはめています。
- また、temperatureやtopP, topKといったパラメータがあり、数値を弄ると回答の精度や自由度が変化します。
- optionsはendpointのURLに対してどういった方式でリクエストを投げるかを規定します。今回はこういうものだとだけ理解してもらえれば。
- URLに対してリクエストを投げる時にはUrlFetchAppというクラスを利用します。URLとoptionsを引数にわたすとresponseに答えが返ってきます。
- 返ってきた回答はまずはJSON.parseという関数でパースという作業をします。これでJSONとして扱えます。
- パースした後にあとは回答の入ってる場所を指定してanswerに格納し、最後にreturnで返します。
図:Geminiにセル上で質問してみる関数
カスタム関数にオプションを追加する
スプレッドシートの関数の中には、単純に値や範囲だけじゃなく、オプション指定するタイプの関数があります。代表的なのが「vlookup関数」。検索値と検索範囲という値以外に、どの列の値を取るか?近似値検索するかどうか?といったオプションが第三引数や第四引数にあります。また、第四引数に至っては省略も出来ます(省略された場合はtrueとして処理される)。
これらオプション値を装備した関数はそのオプション値によって処理される内容が大幅に変わります。今回オプションの値によって以下のような処理をする関数を作ってみました(住所と緯度経度を相互変換する関数を作ります)
- 住所⇔緯度経度に変換するGeocoderクラスを使ってオプション値で処理を変化させます。
- Geocoderはオプション値がtrueの場合は住所から緯度経度に変換して返します。
- Geocoderはオプション値がfalseの場合は緯度経度から住所に変換して返します。
- オプションは省略可として装備します。
GASのGeocoderというクラスは、与えられた住所を緯度経度の値に変換したり、逆をやってくれる便利な関数でAppSheetで作るアプリなどのマップで表示するには住所のままではマップ表示できないので事前に、緯度経度に変換しておく必要があるため、覚えておくと色々と役に立つ優れたクラスです。(また、GoogleのGeocoderは例えば国会議事堂といったようなランドマーク名でも緯度経度に変換してくれます)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
//オプション値によって、ジオコーディング・リバースジオコーディングをする関数 function geochanger(value,option=true) { //計算用の変数をまとめて定義 let result, response, ret; //ジオコーダを呼び出す let geocoder = Maps.newGeocoder(); //空の値の場合には、スルーする if(value == ""){ return ""; } //option値によって処理を分岐 switch(option){ case true: //ジオコーディング実施 response = geocoder.geocode(value); ret = response.results[0]; //緯度経度を格納する result = ret.geometry.location.lat + "," + ret.geometry.location.lng; break; case false: //緯度経度情報を分解する let latlon = value.replace(" ",""); let templatlon = latlon.split(","); let lat = templatlon[0]; let lon = templatlon[1]; //逆ジオコーディング実施(日本語での住所として指定) response = geocoder.setLanguage('ja').reverseGeocode(lat,lon); ret = String(response.results[0].formatted_address); //郵便番号を除去して住所データだけ取得する let tempaddress = ret.split(" ") result = tempaddress[1] + tempaddress[2]; break; } //変換値を返却する return result; } |
- 冒頭のgeochangerという関数の引数にoption=trueとありますが、これは省略された時はtrueとすると定義したデフォルト引数です。引数が空の場合はこれでtrueが入るようになります。
- valueの値が空の場合は即座に何もせずに空値を返して処理を終わらせています。
- option値によってswitch文にて条件分岐。それぞれの条件の場合の処理を以下に記述する。
- trueの場合(緯度経度に変換)は素直に呼び出したgeocoderに対して値を渡すと緯度経度情報として返ってきます。ただし、緯度と経度に分かれてるのでカンマで結合して1つにしています。
- falseの場合(緯度経度から住所へ変換)はちょっと複雑で、緯度経度の値をそれぞれ緯度と経度に分割してからgeocoderに渡す必要があります。
- また、その時setLanguage('ja')を指定しないと英語の住所表記が返ってきてしまいます。
- 渡すメソッドはgeocodeではなくreverseGeocodeになります。
- 返ってきた値はformatted_addressに入っていますが、「日本、郵便番号」というのが冒頭に入ってしまっていますのでこれを除外してresultに格納するために、半角スペースで区切られた0番目を使わず、1番目と2番目を結合しています。
今回はgeocodeの部分は特に理解せずとも良いです。問題は2番目の引数が省略されたら・・・・という点のカバーと、switch文でそれを元に分岐して処理して返すというオプション値で処理を変えるというものをカスタム関数にこれで装備出来ました。
似たような処理だけれど結果が全く違うといったものを1つの関数で実現する場合に必須の手法なので覚えておくと良いでしょう。
図:住所と緯度経度を相互変換する関数
arrayformulaで使える関数を作る
GoogleスプレッドシートにはMicrosoft365のExcelのような「スピル」という機能がありません。故に以下のような範囲 ✕ 範囲みたいな計算がそのままだと出来ません。
1 |
= A2:A6 * B2:B6 |
しかしもともとスピル登場前にGoogleではこの計算はArrayformula関数で出来ていました。上記の式をarrayformulaで書き直すと以下のような数式になります。
1 |
=arrayformula(A2:A6*B2:B6) |
このarrayformulaは範囲を持って計算させる特殊な関数で、例えばvlookupなどでも合せ技でまとめてvlookupをさせることが可能です。本来第一引数は単独の値ですが、arrayformulaを使うことで範囲を指定してまとめてvlookupさせることが可能です。当然答えは配列で返ってくるので一括で返却されます。
1 |
=arrayformula(vlookup(A2:A20,data!$A$2:$D$20,4,false)) |
しかし、このarrayformula関数ですがそのままでは、自分が作成したカスタム関数が動作しません。そこで、今回はJavaScriptのmapメソッドを使って、与えられた配列に対してカスタム関数を全て実行するという仕組みを用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
//arrayformulaで自作関数で展開する function tomato(input){ // 入力値が配列かどうかをチェック if (input.map) { //配列を繰り返すように返す return input.map(tomato); } else { //返却用の変数 let henkyaku = ""; //inputの値に応じて変える switch(input){ case 1: henkyaku = "🍅" break; case 2: henkyaku = "🍌" break; case 3: henkyaku = "🐟" break; default: henkyaku = "🍖" break; } return henkyaku; } } |
- 引数inputに範囲や値が入ってきます。
- 単独の値の場合はIF文のelse以下だけが実行されて返却されますので単独の値でも利用可能です。
- 範囲の場合は、input.mapが作動して、配列の値1つ1つに対して再帰的に自分自身の関数に投げ直しています。
- 投げ直された値は配列のうちの1つの単独値なのでelse以下がそれぞれ実行されて返却されます。
- 返却値はarrayformulaによって、配列として返してくれます。
ここで理解が難しいのがこのmapというメソッドと再帰処理というもの。mapメソッドは冒頭で説明した通り配列の1つ1つの値に対して処理を実行するもの。そして再帰処理とはinput.map(tomato)という部分。引数になぜか自分自身の関数の名前が入っています。つまり自分で自分自身の関数を呼んで処理をさせるというのが再帰処理で、これはなかなか高度な領域ですが、覚えておくと必ずあとで役に立ちます。
再度再帰処理をされたときのinput関数の引数部分はmapで分解された配列の単独の値が順番に入ってくるので、else以下が実行されるという仕掛けです。
arrayformulaは必ず処理の答えは配列で返す必要がありますので、コレでカスタム関数であってもarrayformulaでまとめて処理が可能になります。
図:範囲をまとめてカスタム関数に投げる
注意事項
処理が遅く計算できないことがある
標準で用意されてる関数と比較して、カスタム関数は実行速度が遅いです。よって、カスタム関数をシート上で大量に使うケース(レコード毎にカスタム関数を使って計算 * 数千行など)では、再計算が動作してもすぐに全部の計算が完了しなかったり、最終的にエラーが表示されて計算が中止されることがあります。
その際に表示されるエラーは「Script invoked too many times per second for this Google user account.」。
このエラーが出る原因は、短時間に何度もスクリプトを実行したことによるGASのQuota上限に抵触してしまってる為。関数なのでスプシを開くだけでも再計算が走ってしまうので要注意です。このエラーに関するメッセージはここに掲載されています。この問題に対するガイドとしては、最適化の項目に記述されています。
この問題に対する対処法としては以下の通り
-
1個のリクエストに対して1個の答えを返すのではなく、範囲を受け取って配列で一括で返すカスタム関数として構築する(これで1回の計算で済む)
- カスタム関数はGASだけれども30秒以内に答えを返す必要があるので、30秒を超えない処理に収める。
現在スプレッドシートは年々パワーアップしていて、簡単な関数であれば2014年頃は1,000回呼び出すだけでエラーになっていましたが、現在は10,000回呼び出してもエラーにならずに返してくれるようになっています。この当たりの制限を意識して構築すると良いでしょう。
※但し、一定期間はキャッシュが働く為、他の人が数式などをいじらない限りは再計算されずに計算結果が表示されます。逆を言えば他のユーザに権限がなくても計算結果が表示されてしまうのでセキュリティ面で問題になる可能性があります。
図:20,000行だとエラーになった
GASでは標準関数をコード内で使えない
Excel VBAの場合、VBAのコードの中でWorksheetFunctionという関数を利用することで、標準関数で計算させた結果を利用するということが可能です。一方で、現在のGoogle Apps Scriptではスプレッドシートの標準関数を利用することが出来ません。
よって標準関数の結果を利用したい場合には「事前に作業用シートを用意してカスタム関数で利用する内容を出力しておく」という作業が必要になります。
認証を要求するメソッドの利用
カスタム関数ではどんなGoogleサービスでも呼び出せるわけではありません。これは公式ドキュメントでも記載されています。主にカスタム関数で利用できないものをリストアップしてみました。
- ダイアログやサイドバーなどUIを伴う動作は出来ません
- 他のスプレッドシートを開くようなSpreadsheetApp.openById()等は利用出来ません。
- また特定のセルを指定しての書き出しは出来ません(setValueは使えない)。別シートへの書き出しも出来ません。
- 認証を要求するDriveAppやDirectory APIなどのサービスも使えません。
- 公式ドキュメントにあるように使えるけれど、意味がないというサービスがあります(ロックやキャッシュ等)
ユーザに実行時に承認を要求するものでも、UrlfetchAppであったりLanguageApp、Utilitiesなどは利用することが可能です。よって、外部サービスへのリクエストや翻訳などは使えるので、これらは駆使して関数を構築出来ます。
原則、認証を要求するようなGoogleのサービスは使わないようにしましょう。
図:エラーが出た様子
条件つき書式設定での利用
スプレッドシートの条件付き書式設定ではカスタム数式を利用することで、ANDやTODAYといった標準関数を利用することが可能になっています。例えば、指定したセルの値が100か?200の場合は・・・みたいなケースでは
1 |
=OR($C1=100,$C1=200) |
といった指定が可能です。
しかし、この条件付き書式設定では、自分が用意したカスタム関数は利用することが出来ません。条件を設定してもエラーも出ず書式設定もされない。このような場合は、カスタム関数での判定結果を格納する列を用意しておき計算させ、その列の値を持ってして条件付き書式設定を設定するようにしましょう。
図:直接カスタム関数は使えない
JSDOCについて
標準関数の場合、例えばSUM関数を入力途中にツールチップとして、どんなことをする関数なのか?引数には何を入れるべきなのか?といったものがフワっと表示されます。この関数に関する情報をJSDOCと呼び、独特の書き方があります。これが付いていないと利用するユーザは何をする関数なのか?何を入れたら良いのかが全くわかりません。
これはカスタム関数の時にだけ必要なものではなく、今後プログラミングをしていく上で自前のfunctionを用意した場合、リストに出てきてくれないと色々と困ったことになるので、可能な限り記述しておくべきでしょう。これを記述することで関数候補の一覧にも出てくるようになります。
書き方としては以下の通り
- 対象の関数の直上にコメントの形で記述する
- 2行目は関数の説明文を入れる
- @paramで引数の説明。Numberの部分は入力する値の型や具体的な値を記述しておく。これを引数の数だけ用意する。
- その後に引数の名前を入れて、説明文を繋げる
- @returnで返す答えの説明を入れるが、これは表示されない
- 最後に@customfunctionを追記すること。これがないと説明文が出てきません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/** * 消費税額を計算する関数だよ。 * * @param {計算対象金額} value 税金を掛ける対象の金額を入力 * @param {税率} zei 税額を入れる。10%ならば1.1を入力する * @param {true/false} flg 税額を返すか?合計金額を返すか?のオプションです。trueかfalseを指定。 * @return オプションに応じて答えを返す * @customfunction */ //税額を返す関数 function multiplan(value,zei,flg){ //税額を計算する let goukei = Math.round(value * zei) let zeigaku = goukei - value; //flgの値によって分岐 if(flg == true){ return zeigaku; }else{ return goukei; } } |
図:関数候補として出てきた
図:具体的な使い方も出てきた
手直しにスキルが必要となる
カスタム関数を使ったシートの場合、色々と問題が起きるケースがあります。
- 基本そのファイルでなければ動作しないので、他のファイルで使おうとしてもエラーなる(ライブラリ化やアドオン化が必要)
- アドオン化の場合、そのアドオンが入っていない環境やアカウントだとエラーになる
- 他の人にファイルをExcelなどに変換して渡す場合、エラーとなるので関数を使わない値として出力か?PDF、CSV化して渡す必要がある。
- 標準関数であれば誰でも理解できますが、カスタム関数は作り手でないと手直しが難しい(GASを理解するスキルが必要)
よって、もし全社展開したいといった場合には個人でメンテナンスではなく、情シスを巻き込んで会社としてメンテナンスして運用する体制が必要になるケースがあります。
よってあまり複雑な処理を関数として処理をすると、使い手は便利な反面、引き継ぎに問題が生じたり機能の変更などが難しくなるケースがあるので事前にその準備や対応策を考えておきましょう。
関連資料
関連動画
関連リンク
- Spreadsheetの独自関数をGASで作るよ☆
- スプレッドシートで使えるカスタム関数の作成
- Scriptでスプレッドシートにvlookupやcountifのような関数を埋め込む方法
- 自作関数を使うと「ハッピーな業務」になるのか
- ワークシート用ユーザー定義関数は使い方を誤ると危険である
- ユーザー定義関数(自作の関数)を作る。利点と欠点
- スプレッドシートのカスタム関数でお手軽処理しましょう
- [GAS] カスタム関数を開発するときの3つの注意点
- Googleスプレッドシートで名前付き関数を作ってみる!
- GoogleスプレットシートでGPT関数を使い業務を効率化!
- GASで複数条件対応オリジナルVLOOKUP関数の作成
- Google スプレッドシートでカスタム関数を多くのセルで呼び出しエラーが表示される場合、配列を返す関数(配列数式)に変更する。または、新しいスプレッドシートを利用。
- Googleスプレッドシートの条件付き書式とは?覚えておきたい便利な「カスタム数式」と「複数条件」の設定方法
- GASエディタでJSDocを書いてみる
- JavaScriptにおける丸め誤差と対応
- 【JavaScript】浮動小数点の演算誤差対処法
- 【JavaScript】乱数 (範囲指定)