※本記事はプロモーションを含みます
目次
Google Apps Script(GAS)でスプレッドシートの集計やデータ照合を行っていると、特定の文字列やIDがリストのどこにあるかを調べたい場面が頻繁に出てきます。なので、シートの行を上から下までfor文で1行ずつ比較して探すようなスクリプトを書いていると、コードの行数が無駄に増えるだけでなく、記述の意図がひと目で分からなくなってしまいがちです。ただし、配列内に特定の値が存在するかどうか、そしてそれが何番目にあるのかをピンポイントで特定したいときは、JavaScriptの標準機能であるindexOfメソッドを利用するのが最も簡潔で明快です。というのは、今回は、GASで配列内の値を検索してインデックスを取得する基本的な書き方から、シートの行番号として利用する際のオフセット計算、さらに検索が失敗しやすい大文字小文字の扱いまで、現場でつまずきやすいポイントを整理していきます。
ループで1行ずつ判定していた処理とindexOfで見つける処理の違い
スプレッドシートのデータを処理する際、特定の見出し名が何列目にあるのか、あるいは指定したユーザーIDが何行目に存在しているのかを特定する処理は避けて通れません。なので、このような検索処理を組むとき、GASに慣れていない段階では、配列の先頭から末尾までをfor文で回し、if文で値が一致した瞬間にループをbreakしてカウンタ変数を取得する、というコードを書いてしまいがちです。ただし、この方法でも目的の位置を取得することは可能ですが、カウンタ用の変数を初期化し、ループの終了条件を設定し、見つからなかった場合のフラグ管理まで行う必要があり、わずか数行の探索のために何重もの記述が必要になってしまいます。
ループを使った探索を続けていると、スクリプト全体の可読性が著しく低下します。というのは、コードレビューの際にも「このループはデータを変換しているのか、集計しているのか、それとも単に場所を探しているだけなのか」を一目で判別することが難しくなるからです。なので、途中でbreakを入れ忘れて無駄に末尾までループを回し続けたり、見つからなかった場合の条件分岐を書き漏らして想定外のインデックスを参照したりといった、凡ミスを誘発する要因にもなり得ます。
一方で、配列の検索にindexOfメソッドを採用すると、探索処理は劇的に整理されます。なので、配列に対してメソッドを呼び出し、探したい値を引数として渡すだけで、その値が最初に現れるインデックスが数値として返ってきます。ただし、ループ制御や一時変数の管理が完全に不要になり、処理の意図が「この配列から値の位置を探す」という1行に集約されます。というのは、GAS 配列 検索の処理において、値が見つからない場合には確実に-1が返ってくるという仕様が保証されているため、存在チェックと位置取得を同時に、かつ安全に完結させることができるようになります。なので、処理の意図を明確にし、余計なバグの温床を排除するためにも、愚直なループからindexOfを使った記述への切り替えが欠かせません。
配列から目的の位置を1行で特定する書き方
配列内に格納された値の位置を調べる処理は、最小限の記述であればわずか数行で完結します。なので、まずは1次元配列に対してindexOfを実行し、見つかった位置をログに出力する基本の流れを確認しましょう。
function findFruitIndex() {
const array = ["りんご", "みかん", "バナナ"];
const index = array.indexOf("みかん");
Logger.log(index);
}
上記のコードを実行すると、ログには「1」が出力されます。というのは、配列の添字(インデックス)は0から始まるため、1番目の要素である「りんご」がインデックス0、2番目の要素である「みかん」がインデックス1となるからです。ただし、このように、indexOfの役割は指定値が見つかればその位置(0以上の整数)を返し、もし配列内に存在しない値を指定した場合には-1を返すというものです。
この仕様を理解しておくと、リスト内にデータが存在するか確認する用途としても非常に強力に使えます。なので、単に「存在するかどうか」を知りたいだけであっても、戻り値が-1と等しいかどうかを判定するだけで済みます。ただし、例えば、許可されたステータスの一覧に指定の値が含まれているかを検証したい場合や、重複登録を防ぐために既存のキー配列の中に新規データが含まれていないかを確認する場合など、日常的な業務スクリプトのあらゆる場所で応用が利きます。というのは、余計な比較関数を自作することなく、標準の機能だけで存在確認と位置特定の両方をこなせる点が大きな強みだからです。
スプレッドシートの行番号と0始まりインデックスのズレを埋める
indexOfを使って配列内の位置を特定したあと、その数値をスプレッドシートの操作に持ち込む際には、必ず「インデックスの開始番号」と「シートの行番号・列番号」の差異を意識しましょう。プログラムの世界における配列は0番目から始まります。ただし、Googleスプレッドシートのセル座標(getRangeの引数やA1表記など)は1行目、1列目というように1から始まります。なので、この1のズレを考慮せずに処理を繋いでしまうと、対象のセルの1行上を上書きしてしまったり、存在しない0行目を参照しようとして実行時エラーを引き起こしたりします。
シートの特定列を配列として読み込み、キー検索する際によく使います。例えば、A列に並んでいる注文IDの中から特定のIDを探し、見つかった行のC列に処理済みのステータスを書き込むようなケースです。ただし、シートからデータを取得する際、通常はgetValues()を使って2次元配列として取得しますが、ヘッダー行(見出し)が含まれているかどうかも考慮に入れる必要があります。
1行目が見出し行であり、2行目以降のデータ領域だけを配列として取り出して検索を行った場合、indexOfが返したインデックス「0」は、シート上の「2行目」に相当します。なので、シートの行番号を算出するためには、配列のインデックスに対して「+1(0始まりから1始まりへの補正)」を行い、さらに「ヘッダー行の行数(この場合は1)」を足す必要があります。というのは、合計すると「index + 2」が行番号になるからです。ただし、見出し行も含めてシート全体の1行目から配列化していた場合は、単に「index + 1」とするだけでシートの行番号と一致します。なので、このオフセット計算のルールを明確に定義しておかないと、テスト環境ではたまたま動いていたスクリプトが、本番データでヘッダー行の扱いが変わった途端に致命的な書き込みズレを起こすことになります。検索したインデックスをシート操作の座標として再利用する際は、どの行を起点にして配列化したのかをコード上で明確に意識しましょう。
大文字小文字や型不一致で-1が返ってくる原因を潰す
indexOfを使った検索処理において、画面上には確実にそのデータが存在しているように見えるにもかかわらず、なぜか-1が返ってきて処理が分岐してしまうというトラブルが頻発します。というのは、この現象の最大の原因は、indexOfが「厳密等価比較(===)」で値を判定しているという仕様にあるからです。
まず代表的な落とし穴が、英字の大文字と小文字の扱いです。indexOfは完全一致で探索を行うため、配列の中に「Apple」という文字列が入っている場合、引数に「apple」や「APPLE」を指定しても一致とはみなされず、容赦なく-1を返します。ただし、ユーザーがフォームやシートに手入力したテキストを検索キーにする場合、全角半角の揺れだけでなく、英字の大文字小文字の揺れが混入する可能性は非常に高いと言えます。なので、この問題に対する確実な回避策は、検索を行う前に対象の配列と検索語の両方をtoLowerCase()やtoUpperCase()を使って同一のケースに統一しておくことです。というのは、配列側をあらかじめ小文字に変換した一時配列を作っておき、検索語も小文字に変換した上でindexOfを実行すれば、入力のブレによる不一致を確実に防ぐことができるからです。
もう一つの見落としがちな原因は、数値と文字列の「型の違い」です。スプレッドシートからgetValues()で取得したセルの値は、セルの表示形式や入力内容によって、JavaScriptの数値型(Number)として読み込まれる場合と、文字列型(String)として読み込まれる場合があります。なので、例えば、シート側の社員番号「12345」が数値型として読み込まれているのに対し、外部APIやトリガーの引数から渡ってきた検索キーが「”12345″」という文字列型だった場合、値の見た目は同じでも型が異なるため、indexOfは一致とみなさず-1を返します。ただし、この手のバグはコンソールログに値を出力しただけでは「どちらも12345と表示されているのに一致しない」という状態になり、原因の特定に多くの時間を浪費させます。というのは、比較対象となる検索キーと配列内の値が、どちらもString型なのかNumber型なのかを明確にし、必要に応じてString()による型変換を噛ませてから検索を行う設計を徹底することが、安全な運用の鍵となるからです。
2次元配列から平坦化して検索対象の列を作る設計
スプレッドシートのシート全体を検索対象にする際、GAS特有の課題として「getValues()の戻り値が2次元配列である」という点があります。シートから取得したデータは、行と列を表す配列の入れ子構造([[A1, B1], [A2, B2]])になっているため、そのままではindexOfを使って直接特定の値を検索することができません。なので、1次元の配列でなければ、indexOf(“検索値”)を実行しても行ごとのサブ配列と文字列を比較することになってしまい、結果は常に-1となってしまいます。
この問題を解消するためには、検索を行いたい列だけを抽出して1次元配列に平坦化するステップを挟みます。たとえば、A列(社員番号)から目的の番号を探したいのであれば、getValues()で取得した行ごとの配列から0番目の要素だけをmapメソッドなどで取り出し、1次元の配列を生成します。ただし、その平坦化された配列に対してindexOfを適用すれば、何行目に目的のデータがあるのかを一瞬で特定することが可能になります。
また、行方向の検索だけでなく、ヘッダー行(1行目)の中から「更新日時」という列が何列目にあるのかを動的に取得したい場合にも、この仕組みは威力を発揮します。ヘッダー行を1次元配列として取得し、indexOf(“更新日時”)を実行すれば、列インデックスが即座に手に入ります。なので、列番号をスクリプト内に「3列目(C列)」のように固定値(ハードコード)で埋め込んでしまうと、将来的にシートの列の並び替えや新規列の追加が行われた瞬間にスクリプト全体が破壊されます。ただし、ヘッダーの文字列をindexOfで動的に検索してインデックスを割り出す設計にしておくことで、シートの構造変化に強い、極めて堅牢な自動化スクリプトを構築することができます。

