カレンダー予定の一括登録ミスを作成前に発見|GASで行ごとの登録計画をdry-run監査
ツールおじ|GoogleアカウントをAIエージェント化
予定をまとめて登録したい時ほど、開始・終了の逆転、空の件名、同じ行の二重登録が一気に広がります。この記事では、Googleカレンダーへ1件も作成せず、スプレッドシートの候補行を検査して「登録してよい行/止める行」を別シートへ出す無料GASを紹介します。
この無料コードでできること
- 件名、開始日時、終了日時、カレンダーIDの入力漏れを検出する
- 終了日時が開始日時以前の行を止める
- 候補表の中にある完全一致の重複を検出する
- 最大件数を超えた時は処理前に停止する
- 元の候補表とGoogleカレンダーを変更せず、監査結果だけを別シートへ出す
このコードは登録処理を含みません。実行結果を確認した後も、予定の作成・変更・削除は自動では行われません。
準備するシート
スプレッドシートに「イベント候補」シートを作り、1行目へ次の見出しを入れます。
- 状態
- 件名
- 開始日時
- 終了日時
- カレンダーID
状態には「登録候補」または「除外」を入力します。最初はテスト用のカレンダーIDと、少量のダミー予定で確認してください。個人情報や機密情報は入力しないでください。
コピーして使えるdry-runコード
const CALENDAR_PLAN_CONFIG = Object.freeze({
sourceSheet: 'イベント候補',
auditSheet: '登録計画',
targetStatus: '登録候補',
maxRows: 200,
dryRun: true,
});
function previewCalendarImportPlanFree() {
if (CALENDAR_PLAN_CONFIG.dryRun !== true) {
throw new Error('安全のためdryRun=true以外では実行できません。');
}
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const source = spreadsheet.getSheetByName(CALENDAR_PLAN_CONFIG.sourceSheet);
if (!source) {
throw new Error('イベント候補シートが見つかりません。');
}
const values = source.getDataRange().getValues();
if (values.length < 2) {
throw new Error('確認するデータ行がありません。');
}
const headers = values[0].map(value => String(value).trim());
const required = ['状態', '件名', '開始日時', '終了日時', 'カレンダーID'];
const column = Object.fromEntries(required.map(name => [name, headers.indexOf(name)]));
const missingHeaders = required.filter(name => column[name] < 0);
if (missingHeaders.length) {
throw new Error(`見出し不足: ${missingHeaders.join(', ')}`);
}
const candidates = values.slice(1)
.map((row, index) => ({ row, rowNumber: index + 2 }))
.filter(item => String(item.row[column['状態']]).trim() === CALENDAR_PLAN_CONFIG.targetStatus);
if (candidates.length > CALENDAR_PLAN_CONFIG.maxRows) {
throw new Error(`登録候補が上限${CALENDAR_PLAN_CONFIG.maxRows}件を超えています。`);
}
const seen = new Map();
const results = candidates.map(item => {
const title = String(item.row[column['件名']] ?? '').trim();
const start = toValidDate_(item.row[column['開始日時']]);
const end = toValidDate_(item.row[column['終了日時']]);
const calendarId = String(item.row[column['カレンダーID']] ?? '').trim();
const issues = [];
if (!title) issues.push('件名が空です');
if (title.length > 100) issues.push('件名が100文字を超えています');
if (!start) issues.push('開始日時を日付として読めません');
if (!end) issues.push('終了日時を日付として読めません');
if (start && end && end.getTime() <= start.getTime()) {
issues.push('終了日時が開始日時以前です');
}
if (!calendarId) issues.push('カレンダーIDが空です');
const key = [
normalizeText_(title),
start ? start.toISOString() : '',
end ? end.toISOString() : '',
normalizeText_(calendarId),
].join('|');
if (key !== '|||') {
if (seen.has(key)) issues.push(`候補表内で${seen.get(key)}行目と重複しています`);
else seen.set(key, item.rowNumber);
}
return [
item.rowNumber,
issues.length ? 'HOLD' : 'READY',
issues.join(' / ') || '入力検査PASS',
title ? '入力あり' : '空',
start ? Utilities.formatDate(start, Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm') : '',
end ? Utilities.formatDate(end, Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm') : '',
calendarId ? '入力あり' : '空',
];
});
const audit = spreadsheet.getSheetByName(CALENDAR_PLAN_CONFIG.auditSheet)
|| spreadsheet.insertSheet(CALENDAR_PLAN_CONFIG.auditSheet);
audit.clearContents();
const output = [[
'元の行番号', '判定', '理由', '件名', '開始日時', '終了日時', 'カレンダーID'
], ...results];
audit.getRange(1, 1, output.length, output[0].length).setValues(output);
const readyCount = results.filter(row => row[1] === 'READY').length;
const holdCount = results.length - readyCount;
console.log(JSON.stringify({ dryRun: true, checked: results.length, readyCount, holdCount }));
}
function toValidDate_(value) {
const date = value instanceof Date ? value : new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function normalizeText_(value) {
return String(value ?? '').normalize('NFKC').trim().toLowerCase();
}実行手順
- 元ファイルではなく、検証用にコピーしたスプレッドシートを開きます。
- 拡張機能からApps Scriptを開き、コードを貼り付けます。
- プロジェクトのタイムゾーンが予定の運用地域と一致するか確認します。
- `previewCalendarImportPlanFree` を選んで実行します。
- 初回の権限確認後、「登録計画」シートを開きます。
- HOLD行の入力を直し、少量のダミーデータで再実行します。
- 実行ログは件数だけを確認し、予定名やカレンダーIDが出ていないことを確認します。
結果の読み方
READYは入力形式の検査を通過したという意味で、実際に登録できることを保証する判定ではありません。HOLDは、理由欄を直してからもう一度確認する行です。
監査結果では件名とカレンダーIDの中身を表示せず、「入力あり/空」だけを残します。元の行番号で候補表へ戻れるため、監査シートへ個人名や予定内容を複製しません。
失敗しやすい点と制限
- 文字列の日付はシートの地域設定によって解釈が変わるため、セルを日時形式にそろえてください。
- この例は候補表内の完全一致だけを確認します。既存のGoogleカレンダー予定との重複は調べません。
- カレンダーへの閲覧権限・編集権限やApps Scriptの実行上限は確認しません。
- 「登録計画」シートは毎回内容を作り直します。残したい結果は先に別ファイルへコピーしてください。
- `dryRun` を変更しても、このコードに予定作成処理はありません。自動登録・自動削除・自動通知は行いません。
公開前チェックリスト
- 元データではなくコピー環境で試した
- タイムゾーンとシートの地域設定を確認した
- 登録候補は200件以下に絞った
- READYとHOLDの件数が想定どおりだった
- 個人情報や認証情報を候補表・ログへ入れていない
- 実際の登録は別工程として、少数ずつ人が確認する
まとめ
大量登録で怖いのは、コードが動かないことより、誤った候補がまとめて作成されることです。先に行番号つきの登録計画を作れば、開始・終了の逆転や入力漏れ、候補表内の重複を、カレンダーへ触れる前に修正できます。
朝のタスク整理や予定確認までまとめて整えたい方へ

GasおじのTips商品一覧を見る

※本記事は学習・検証用です。実データへ適用する前にコピー環境と少量のダミーデータで確認してください。Google公式・Google公認の商品ではありません。
