🐙
【GAS】スプレッドシートで使いやすいClass紹介
長らく手続き型でしかGASを書けなかったのですが、Classを勉強したことで便利なことに気が付きました。
スプレッドシートにコピペするだけで、あらゆるツールが使えるようになるので、よろしければお使いください(もっと便利な機能があればコメントまでお待ちしています)。
/**
* スプレッドシートの操作を簡略化する汎用クラス
* - 基本的な読み書き
* - オブジェクト形式での操作
* - 検索・情報取得
*/
class SheetAccess {
/**
* @param {string} sheetName - 操作したいシート名
* @param {string} [spreadsheetId] - (任意) 別スプレッドシートのID
*/
constructor(sheetName = null, spreadsheetId = null) {
this.ss = spreadsheetId
? SpreadsheetApp.openById(spreadsheetId)
: SpreadsheetApp.getActiveSpreadsheet();
this.sheet = sheetName
? this.ss.getSheetByName(sheetName)
: this.ss.getActiveSheet();
if (!this.sheet) {
const targetName = spreadsheetId ? `ID:${spreadsheetId}内` : "アクティブSS内";
throw new Error(`${targetName}にシート「${sheetName}」が見つかりません。`);
}
}
// ==========================================
// 基本操作 (Read / Write)
// ==========================================
/**
* 単一セルの値を取得(A1形式 / RC形式 両対応)
* @param {string|number} arg1 - アドレス(A1) or 行番号
* @param {number} [arg2] - 列番号
*/
getValue(arg1, arg2 = null) {
if (typeof arg1 === 'number' && typeof arg2 === 'number') {
return this.sheet.getRange(arg1, arg2).getValue();
}
return this.sheet.getRange(arg1).getValue();
}
/**
* 単一セルに値をセット(A1形式 / RC形式 両対応)
* @param {string|number} arg1 - アドレス(A1) or 行番号
* @param {any} arg2 - 値 (A1形式の時) or 列番号 (RC形式の時)
* @param {any} [arg3] - 値 (RC形式の時のみ)
*/
setValue(arg1, arg2, arg3 = null) {
if (typeof arg1 === 'number') {
this.sheet.getRange(arg1, arg2).setValue(arg3);
} else {
this.sheet.getRange(arg1).setValue(arg2);
}
}
/**
* 範囲(Rangeオブジェクト)を取得
* 1. 引数なし → データ全範囲
* 2. 文字列 ("A1:C5")
* 3. 数値 (row, col, [numRows], [numCols])
*/
getRectRange(arg1, arg2, arg3, arg4) {
if (arg1 === undefined) return this.sheet.getDataRange();
if (typeof arg1 !== 'number') return this.sheet.getRange(arg1);
const row = arg1;
const col = arg2;
// 指定がなければ最終行/列まで
let numRows = arg3 || (Math.max(1, this.sheet.getLastRow() - row + 1));
let numCols = arg4 || (Math.max(1, this.sheet.getLastColumn() - col + 1));
return this.sheet.getRange(row, col, numRows, numCols);
}
/**
* 範囲の値(2次元配列)を取得
* @param {string|number} arg1 - アドレス or 行番号
*/
getRectValues(arg1, arg2, arg3, arg4) {
return this.getRectRange(arg1, arg2, arg3, arg4).getValues();
}
/**
* 2次元配列を書き込む
* @param {string|number} arg1 - アドレス(A1) or 行番号
* @param {any} arg2 - 値([][]) or 列番号
* @param {any[][]} [arg3] - 値([][])
*/
setRectValues(arg1, arg2, arg3 = null) {
let row, col, values;
if (typeof arg1 === 'number') {
row = arg1; col = arg2; values = arg3;
} else {
const range = this.sheet.getRange(arg1);
row = range.getRow(); col = range.getColumn(); values = arg2;
}
if (!values || !values.length) return;
this.sheet.getRange(row, col, values.length, values[0].length).setValues(values);
}
// ==========================================
// 情報取得・検索 (Info / Search)
// ==========================================
/**
* 最終行を取得
* @param {string} [column='A'] - 判定基準にする列
*/
getLastRow(column = 'A') {
const colIndex = isNaN(column) ? this.sheet.getRange(`${column}1`).getColumn() : column;
const maxRows = this.sheet.getMaxRows();
if (maxRows === 0) return 1;
return this.sheet.getRange(maxRows, colIndex).getNextDataCell(SpreadsheetApp.Direction.UP).getRow();
}
/**
* 最終列を取得
* @param {number} [row=1] - 判定基準にする行
*/
getLastCol(row = 1) {
const maxCols = this.sheet.getMaxColumns();
if (maxCols === 0) return 1;
return this.sheet.getRange(row, maxCols).getNextDataCell(SpreadsheetApp.Direction.PREVIOUS).getColumn();
}
/**
* 指定列からキーワードを検索し行番号を返す
* ※列が固定の場合に使用
* @param {string} keyword
* @param {string} [column='A'] - 列文字 (例: 'A', 'B')
*/
getRowIndex(keyword, column = 'A') {
const finder = this.sheet.getRange(`${column}:${column}`).createTextFinder(keyword).matchEntireCell(true).findNext();
return finder ? finder.getRow() : null;
}
/**
* ヘッダー名から列を特定し、その列内でキーワードを検索して行番号を返す
* ※列位置が変わる可能性がある場合に使用
* @param {string} keyword - 検索したい値
* @param {string} headerName - ヘッダー名
* @param {number} [headerRow=1] - ヘッダーがある行番号
*/
findRowByHeader(keyword, headerName, headerRow = 1) {
// 1. まずヘッダーがある列番号を探す(既存メソッドを活用)
const colIndex = this.getColumnIndex(headerName, headerRow);
// ヘッダー自体が見つからない場合は null
if (!colIndex) return null;
// 2. 特定した列(colIndex)全体を対象に検索
// 行数が多い場合を考慮し、列全体(MaxRows)を指定
const finder = this.sheet.getRange(1, colIndex, this.sheet.getMaxRows(), 1)
.createTextFinder(keyword)
.matchEntireCell(true)
.findNext();
return finder ? finder.getRow() : null;
}
/**
* 指定行からキーワードを検索し列番号を返す
* @param {string} keyword
* @param {number} [row=1]
*/
getColumnIndex(keyword, row = 1) {
const finder = this.sheet.getRange(`${row}:${row}`).createTextFinder(keyword).matchEntireCell(true).findNext();
return finder ? finder.getColumn() : null;
}
// ==========================================
// オブジェクト操作 (Object Mapper)
// ==========================================
/**
* シートデータをオブジェクト配列に変換
* @param {number} [headerRow=1]
* @param {number} [dataStartRow=null]
*/
getDataAsObjects(headerRow = 1, dataStartRow = null) {
const values = this.sheet.getDataRange().getValues();
const headers = values[headerRow - 1];
const startIdx = dataStartRow ? dataStartRow - 1 : headerRow;
if (!headers) return [];
return values.slice(startIdx).map((rowValues, index) => {
const item = headers.reduce((obj, header, i) => {
obj[header] = rowValues[i];
return obj;
}, {});
item.row = startIdx + index + 1;
return item;
});
}
/**
* オブジェクト配列を指定キー順で書き込み
* @param {Object[]} items
* @param {string[]} [keys=null] - 省略時は row 以外の全キー
* @param {number} [startRow=1]
* @param {number} [startCol=1]
*/
setRectObjects(items, keys = null, startRow = 1, startCol = 1) {
if (!items || !items.length) return;
let targetKeys = keys;
if (!targetKeys || targetKeys.length === 0) {
targetKeys = Object.keys(items[0]).filter(k => k !== 'row');
}
const values = [targetKeys];
items.forEach(item => {
values.push(targetKeys.map(key => (item.hasOwnProperty(key) ? item[key] : '')));
});
this.setRectValues(startRow, startCol, values);
}
/**
* 既存ヘッダーに合わせてオブジェクト配列を追記
* @param {Object[]} dataObjects
* @param {number} [headerRow=1]
*/
insertObjects(dataObjects, headerRow = 1) {
if (!dataObjects.length) return;
const lastCol = this.sheet.getLastColumn();
const headers = this.sheet.getRange(headerRow, 1, 1, lastCol).getValues()[0];
const newRows = dataObjects.map(obj => {
return headers.map(header => (obj.hasOwnProperty(header) ? obj[header] : ''));
});
const lastRow = this.sheet.getLastRow();
this.sheet.getRange(lastRow + 1, 1, newRows.length, newRows[0].length).setValues(newRows);
}
// ==========================================
// 静的メソッド (Factory / Static)
// ==========================================
/**
* シートをリセット(クリアまたは新規作成)してインスタンスを返す
* @param {string} sheetName
* @param {string} [spreadsheetId]
*/
static resetSheet(sheetName, spreadsheetId = null) {
const ss = spreadsheetId ? SpreadsheetApp.openById(spreadsheetId) : SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(sheetName);
if (sheet) {
sheet.clear();
} else {
sheet = ss.insertSheet(sheetName);
}
sheet.activate();
return new SheetAccess(sheetName, spreadsheetId);
}
}
/**
* 日付操作ユーティリティ
* DateUtils.format(...) のように直接使用可能
*/
class DateUtils {
// 定数として曜日配列を定義
static get DAYS_JP() {
return ['日', '月', '火', '水', '木', '金', '土'];
}
/**
* 日付を指定フォーマットに変換
* @param {Date|string} date - 日付オブジェクトまたは日付文字列
* @param {string} [type='date'] - フォーマット種別(key) または 直接パターン(yyyy/MM/ddなど)
* @return {string} フォーマットされた文字列
*/
static format(date, type = 'date') {
if (!date) return "";
// 文字列で来てもDate型に変換
const d = new Date(date);
// 無効な日付なら空文字を返す
if (isNaN(d.getTime())) return '';
// 基本パーツの生成 (Utilities.formatDateはGAS専用メソッド)
// タイムゾーンはJST固定
const ymd = Utilities.formatDate(d, 'JST', 'yyyy/MM/dd');
const ymdJP = Utilities.formatDate(d, 'JST', 'yyyy年MM月dd日');
const hm = Utilities.formatDate(d, 'JST', 'HH:mm');
// 曜日の取得 (this.DAYS_JP でこのクラスのstatic getterにアクセス)
const dayName = this.DAYS_JP[d.getDay()];
// 定義済みパターンのマップ
const formats = {
'date': ymd, // 2023/10/01
'withDay': `${ymd}(${dayName})`, // 2023/10/01(日)
'jp': ymdJP, // 2023年10月01日
'jpFull': `${ymdJP}(${dayName})`, // 2023年10月01日(日)
'time': hm, // 12:00
'full': `${ymd}(${dayName}) ${hm}`, // 2023/10/01(日) 12:00
'dayOnly': `(${dayName})` // (日)
};
// マップにあればそれを返し、なければユーザー指定のパターン(yyyyMMddなど)として処理
return formats[type] || Utilities.formatDate(d, 'JST', type);
}
}
/**
* Google Drive / ファイル操作用ユーティリティ
*/
class DriveAccess {
/**
* 1. 空のスプレッドシートを新規作成し、指定フォルダに配置
* @param {string} fileName - 新しいファイル名
* @param {string} folderId - 格納先のフォルダID
* @return {string} 作成されたSSのID
*/
static createSpreadsheet(fileName, folderId) {
// 1. フォルダ確認
const folder = this._getFolderOrThrow(folderId);
// 2. 作成
const ss = SpreadsheetApp.create(fileName);
const file = DriveApp.getFileById(ss.getId());
// 3. 指定のフォルダに移動
file.moveTo(folder);
return ss.getId();
}
/**
* 2. 既存のスプレッドシートをコピーし、指定フォルダに配置
* @param {string} templateId - 元になるSSのID
* @param {string} newFileName - コピー後のファイル名
* @param {string} folderId - 格納先のフォルダID
* @return {string} 新しいSSのID
*/
static copySpreadsheet(templateId, newFileName, folderId) {
// 1. フォルダと元ファイルの確認
const folder = this._getFolderOrThrow(folderId);
const templateFile = DriveApp.getFileById(templateId);
// 2. 指定フォルダの中にコピーを作成
const newFile = templateFile.makeCopy(newFileName, folder);
return newFile.getId();
}
/**
* 3. 新しいSSを作成し、別SSの特定シートだけをコピーして持ってくる
* ・不要な初期シート「シート1」は削除されます。
* @param {string} sourceSpreadsheetId - コピー元SSのID
* @param {string} sourceSheetName - コピーしたいシート名
* @param {string} newFileName - 新しいファイル名
* @param {string} folderId - 格納先のフォルダID
* @return {string} 新しいSSのID
*/
static createFromSheet(sourceSpreadsheetId, sourceSheetName, newFileName, folderId) {
// --- 準備フェーズ ---
const folder = this._getFolderOrThrow(folderId);
// コピー元の確認
const srcSS = SpreadsheetApp.openById(sourceSpreadsheetId);
const srcSheet = srcSS.getSheetByName(sourceSheetName);
if (!srcSheet) {
throw new Error(`コピー元のシート「${sourceSheetName}」が見つかりません。`);
}
// --- 作成フェーズ ---
// 1. 新規SS作成と移動
const newSS = SpreadsheetApp.create(newFileName);
const newFile = DriveApp.getFileById(newSS.getId());
newFile.moveTo(folder);
// 2. シートのコピー実行
const copiedSheet = srcSheet.copyTo(newSS);
copiedSheet.setName(sourceSheetName); // コピー~ とつくのを防ぐ
// 3. 初期シート(シート1)の削除
const defaultSheet = newSS.getSheets()[0];
// 万が一、コピーしたシートと同じ名前だった場合などの安全策
if (newSS.getSheets().length > 1) {
newSS.deleteSheet(defaultSheet);
}
return newSS.getId();
}
/**
* 内部用ヘルパー:フォルダIDからFolderオブジェクトを取得
* 取得できない場合は明確なエラーを投げる
* @private
*/
static _getFolderOrThrow(folderId) {
try {
if (!folderId) throw new Error();
return DriveApp.getFolderById(folderId);
} catch (e) {
throw new Error(`フォルダが見つかりません。ID: ${folderId} を確認してください。`);
}
}
}
/**
* Google Chatへの通知用クラス
* ※Webhook URLが必要です
*/
class ChatUtils {
/**
* テキストメッセージを送信
* @param {string} url - Webhook URL
* @param {string} text - 送信メッセージ(メンション等は <users/all> 等の形式で記述)
*/
static post(url, text) {
if (!url || !text) return;
const payload = { text: text };
const options = {
'method': 'post',
'contentType': 'application/json',
'payload': JSON.stringify(payload),
'muteHttpExceptions': true
};
try {
UrlFetchApp.fetch(url, options);
} catch (e) {
console.error('Chat通知に失敗しました: ' + e.message);
}
}
}
/**
* メニュー作成用ユーティリティ
*/
class MenuUtils {
/**
* カスタムメニューを追加
* @param {string} menuName - メニュー名
* @param {Object} items - { '表示ラベル': '実行関数名', ... }
* ※区切り線を入れたい場合はキーに 'SEPARATOR_1' などを指定
*/
static create(menuName, items) {
const ui = SpreadsheetApp.getUi();
const menu = ui.createMenu(menuName);
Object.keys(items).forEach(label => {
if (label.includes('SEPARATOR')) {
menu.addSeparator();
} else {
menu.addItem(label, items[label]);
}
});
menu.addToUi();
}
}
/**
* スクリプトプロパティ操作用ショートカット
*/
class PropAccess {
/**
* プロパティの値を取得
* @param {string} key
* @return {string|null}
*/
static get(key) {
return PropertiesService.getScriptProperties().getProperty(key);
}
/**
* プロパティに値をセット(基本はGUIでやると思うので、あまり使わないかもですが念のため)
* @param {string} key
* @param {string} value
*/
static set(key, value) {
PropertiesService.getScriptProperties().setProperty(key, value);
}
}
Discussion