GAS×配列操作|map/filter/reduceを使いこなす【実践コード付き】
この記事で学べること
– GASで配列操作を効率化するメソッドの使い方
– map、filter、reduceの違いと使い分け
– スプレッドシートデータを一括処理するテクニック
– forループから脱却してコードをスッキリさせる方法
GASの配列操作を整理する
GAS(Google Apps Script)でスプレッドシートを扱う限り、配列は避けて通れない。
// スプレッドシートのデータは2次元配列で取得される
const data = sheet.getDataRange().getValues();
// → [["名前", "売上", "部署"], ["田中", 100000, "営業"], ["佐藤", 80000, "開発"], ...]
このデータを加工するとき、forループを何重にも重ねるコードをよく見かける。
// よくある書き方(forループ地獄)
const results = [];
for (let i = 0; i < data.length; i++) {
if (data[i][2] === "営業") {
results.push([data[i][0], data[i][1] * 1.1]);
}
}
動くには動くが、可読性が低く、バグの温床になりやすい。
map、filter、reduceといった配列メソッドを使えば、同じ処理を1〜2行で書ける。以下、GASでよく使う配列メソッドを実践例とともに見ていく。
配列メソッド一覧表
GASで使える主要な配列メソッドを一覧にまとめた。
| メソッド | 用途 | 戻り値 | 元配列の変更 |
|---|---|---|---|
| map | 各要素を変換 | 新しい配列 | なし |
| filter | 条件に合う要素を抽出 | 新しい配列 | なし |
| reduce | 配列を単一の値に集約 | 任意の値 | なし |
| find | 条件に合う最初の要素を取得 | 要素 or undefined | なし |
| findIndex | 条件に合う最初の要素のインデックス | インデックス or -1 | なし |
| forEach | 各要素に処理を実行 | undefined | なし |
| some | 条件に合う要素が1つでもあるか | true/false | なし |
| every | 全要素が条件に合うか | true/false | なし |
| includes | 特定の値が含まれるか | true/false | なし |
| flat | 多次元配列を平坦化 | 新しい配列 | なし |
この中でもmap、filter、reduceの3つが使用頻度が高い。順に解説する。
map:データを一括変換する
基本構文
const 新しい配列 = 配列.map(要素 => 変換処理);
map は配列の各要素に同じ処理を適用し、新しい配列を返す。
基本例:数値を2倍にする
function mapBasicExample() {
const numbers = [1, 2, 3, 4, 5];
// 各要素を2倍にする
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
}
実践例①:売上データに消費税を加算
function addTaxToSales() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("売上");
const data = sheet.getDataRange().getValues();
const header = data.shift(); // ヘッダー行を除去
// 各行の売上(2列目)に消費税10%を加算
const withTax = data.map(row => {
return [row[0], row[1], Math.floor(row[1] * 1.1)]; // [名前, 元売上, 税込売上]
});
// 結果を書き込み
const outputSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("税込売上");
outputSheet.getRange(1, 1, 1, 3).setValues([["名前", "売上", "税込売上"]]);
outputSheet.getRange(2, 1, withTax.length, 3).setValues(withTax);
}
実践例②:日付フォーマットを一括変換
function formatDates() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
const header = data.shift();
// 日付(1列目)を "YYYY/MM/DD" 形式に変換
const formatted = data.map(row => {
const date = new Date(row[0]);
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
return [`${yyyy}/${mm}/${dd}`, row[1], row[2]];
});
console.log(formatted);
}
filter:条件に合うデータだけ抽出
基本構文
const 新しい配列 = 配列.filter(要素 => 条件);
filter は条件を満たす要素だけを集めた新しい配列を返す。
基本例:偶数だけ抽出
function filterBasicExample() {
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 偶数だけ抽出
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]
}
実践例③:特定部署のデータを抽出
function filterByDepartment() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("社員");
const data = sheet.getDataRange().getValues();
const header = data.shift();
// 営業部だけ抽出(3列目が部署)
const salesTeam = data.filter(row => row[2] === "営業部");
// 結果を別シートに出力
const outputSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("営業部一覧");
outputSheet.clear();
outputSheet.getRange(1, 1, 1, header.length).setValues([header]);
if (salesTeam.length > 0) {
outputSheet.getRange(2, 1, salesTeam.length, salesTeam[0].length).setValues(salesTeam);
}
console.log(`営業部: ${salesTeam.length}名`);
}
実践例④:売上○万円以上のデータを抽出
function filterHighPerformers() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
const header = data.shift();
// 売上100万円以上を抽出(2列目が売上)
const threshold = 1000000;
const highPerformers = data.filter(row => row[1] >= threshold);
console.log(`売上${threshold / 10000}万円以上: ${highPerformers.length}名`);
return highPerformers;
}
filterとmapの組み合わせ
function filterAndMap() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
const header = data.shift();
// 営業部だけ抽出し、名前と売上のみ取得
const result = data
.filter(row => row[2] === "営業部") // 営業部だけ
.map(row => [row[0], row[1]]); // 名前と売上のみ
console.log(result);
}
reduce:配列を集計・変換する
基本構文
const 結果 = 配列.reduce((累積値, 現在の要素) => 処理, 初期値);
reduce は配列を1つの値に集約する。合計、平均、オブジェクトへの変換など用途が広い。
基本例:合計を計算
function reduceBasicExample() {
const numbers = [10, 20, 30, 40, 50];
// 合計を計算
const sum = numbers.reduce((total, num) => total + num, 0);
console.log(sum); // 150
}
実践例⑤:売上の合計・平均を計算
function calculateSalesSummary() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
data.shift(); // ヘッダー除去
// 売上(2列目)の合計
const totalSales = data.reduce((sum, row) => sum + row[1], 0);
// 平均
const averageSales = totalSales / data.length;
console.log(`合計: ${totalSales.toLocaleString()}円`);
console.log(`平均: ${Math.floor(averageSales).toLocaleString()}円`);
console.log(`件数: ${data.length}件`);
}
実践例⑥:部署別に集計
function groupByDepartment() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
data.shift();
// 部署別に売上を集計
const grouped = data.reduce((acc, row) => {
const department = row[2]; // 部署
const sales = row[1]; // 売上
if (!acc[department]) {
acc[department] = { count: 0, total: 0 };
}
acc[department].count++;
acc[department].total += sales;
return acc;
}, {});
// 結果を整形して出力
console.log("=== 部署別集計 ===");
for (const [dept, stats] of Object.entries(grouped)) {
console.log(`${dept}: ${stats.count}名, 売上合計 ${stats.total.toLocaleString()}円`);
}
return grouped;
}
実践例⑦:2次元配列をオブジェクト配列に変換
function convertToObjects() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
const header = data.shift(); // ["名前", "売上", "部署"]
// ヘッダーをキーにしたオブジェクト配列に変換
const objects = data.reduce((arr, row) => {
const obj = {};
header.forEach((key, index) => {
obj[key] = row[index];
});
arr.push(obj);
return arr;
}, []);
console.log(objects);
// [{ 名前: "田中", 売上: 100000, 部署: "営業部" }, ...]
return objects;
}
find と findIndex:特定の要素を検索
findの使い方
find は条件に合う最初の要素を返す。見つからなければ undefined になる。
function findExample() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
data.shift();
// 名前が "田中" の行を取得
const tanaka = data.find(row => row[0] === "田中");
if (tanaka) {
console.log(`田中さんの売上: ${tanaka[1].toLocaleString()}円`);
} else {
console.log("田中さんは見つかりませんでした");
}
}
findIndexの使い方
findIndex は条件に合う要素のインデックスを返す。見つからなければ -1。
function findIndexExample() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
// "売上100万以上" の最初の行を探す(ヘッダー込みのため+1不要)
const index = data.findIndex(row => row[1] >= 1000000);
if (index !== -1) {
console.log(`${index + 1}行目に売上100万円以上のデータがあります`);
// シート上でその行を選択
sheet.getRange(index + 1, 1).activate();
}
}
実践例⑧:スプレッドシートデータの一括処理
複数のメソッドを組み合わせた実践的な例を見てみよう。
/**
* 営業部の売上トップ3を抽出し、ボーナス額を計算
*/
function getTopSalesWithBonus() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("社員データ");
const data = sheet.getDataRange().getValues();
const header = data.shift();
const result = data
// 1. 営業部だけ抽出
.filter(row => row[2] === "営業部")
// 2. 売上の降順でソート
.sort((a, b) => b[1] - a[1])
// 3. トップ3を取得
.slice(0, 3)
// 4. ボーナス(売上の10%)を計算して追加
.map((row, index) => {
const bonus = Math.floor(row[1] * 0.1);
return [index + 1, row[0], row[1], bonus]; // [順位, 名前, 売上, ボーナス]
});
// 結果を出力
const outputSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("営業トップ3");
outputSheet.clear();
outputSheet.getRange(1, 1, 1, 4).setValues([["順位", "名前", "売上", "ボーナス"]]);
outputSheet.getRange(2, 1, result.length, 4).setValues(result);
console.log("営業部トップ3を出力しました");
}
実践例⑨:条件に合うデータの抽出と集計
/**
* 今月の売上データから、目標達成者を抽出して報告書を作成
*/
function createMonthlyReport() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("月次売上");
const data = sheet.getDataRange().getValues();
const header = data.shift();
const targetAmount = 500000; // 目標額
// 目標達成者を抽出
const achievers = data.filter(row => row[1] >= targetAmount);
// 達成者の合計売上
const totalAchievers = achievers.reduce((sum, row) => sum + row[1], 0);
// 全体の合計売上
const totalAll = data.reduce((sum, row) => sum + row[1], 0);
// 達成率
const achieveRate = (achievers.length / data.length * 100).toFixed(1);
// レポート出力
console.log("=== 月次レポート ===");
console.log(`全体: ${data.length}名, 売上合計: ${totalAll.toLocaleString()}円`);
console.log(`目標達成: ${achievers.length}名 (${achieveRate}%)`);
console.log(`達成者売上合計: ${totalAchievers.toLocaleString()}円`);
console.log("");
console.log("【達成者一覧】");
achievers.forEach(row => {
console.log(` ${row[0]}: ${row[1].toLocaleString()}円`);
});
}
forループとの比較
可読性の比較
同じ処理をforループと配列メソッドで書くとどう変わるか。
課題:営業部の売上合計を計算する。
// ❌ forループ版(冗長)
function sumSalesFor() {
const data = sheet.getDataRange().getValues();
data.shift();
let total = 0;
for (let i = 0; i < data.length; i++) {
if (data[i][2] === "営業部") {
total += data[i][1];
}
}
return total;
}
// ✅ 配列メソッド版(シンプル)
function sumSalesModern() {
const data = sheet.getDataRange().getValues();
data.shift();
return data
.filter(row => row[2] === "営業部")
.reduce((sum, row) => sum + row[1], 0);
}
パフォーマンス比較
1万行のデータで処理速度を比較した場合:
| 処理内容 | forループ | 配列メソッド |
|---|---|---|
| 単純な変換 | ◎ やや速い | ○ 実用上問題なし |
| 条件抽出 | ◎ やや速い | ○ 実用上問題なし |
| 複合処理 | △ コード複雑化 | ◎ メンテナンス性◎ |
数万行程度のデータなら、パフォーマンス差は体感できないレベルにとどまる。可読性とメンテナンス性を優先して配列メソッドを選ぶのが妥当だろう。
ただし、10万行を超える大量データの場合、forループの方が高速なケースもある。
// 大量データ処理時の最適化例
function processLargeData() {
const data = sheet.getDataRange().getValues();
const results = [];
// 大量データはforループで直接処理
for (let i = 1; i < data.length; i++) {
if (data[i][2] === "営業部") {
results.push([data[i][0], data[i][1] * 1.1]);
}
}
return results;
}
よくあるエラーと対処法
1. undefined のエラー
// ❌ エラー: Cannot read property 'filter' of undefined
const result = data.filter(row => row[0] === "田中");
// ✅ 対処: 配列が存在するか確認
const result = (data || []).filter(row => row[0] === "田中");
2. 元の配列を変更してしまう
// ❌ shift()は元配列を変更する
const data = sheet.getDataRange().getValues();
data.shift(); // dataが変更される!
// ✅ slice()でコピーを作成
const data = sheet.getDataRange().getValues();
const header = data[0];
const rows = data.slice(1); // 元配列は変更されない
3. map内でreturnを忘れる
// ❌ 結果が [undefined, undefined, ...] になる
const result = data.map(row => {
row[1] * 2; // returnがない!
});
// ✅ returnを明記(または省略形)
const result = data.map(row => row[1] * 2);
// または
const result = data.map(row => {
return row[1] * 2;
});
まとめ
配列メソッドの使い分け
| やりたいこと | 使うメソッド |
|---|---|
| 各要素を変換したい | map |
| 条件に合う要素だけ欲しい | filter |
| 合計・集計したい | reduce |
| 条件に合う最初の要素が欲しい | find |
| 条件に合う要素があるか確認 | some / every |
押さえておくべき3点
- forループの代わりにmap/filter/reduceを使うと、コードがシンプルになりバグも減る
.filter().map().reduce()のようにメソッドチェーンで繋げると処理の流れが明確になるshift()、pop()、sort()は元配列を変更する破壊的メソッドなので、必要に応じてslice()でコピーを作ってから使う
配列メソッドに慣れると、GASのコード量と保守コストが目に見えて減る。
コメント