カレンダー予定の一括登録ミスを作成前に発見|GASで行ごとの登録計画をdry-run監査

カレンダー予定の一括登録ミスを作成前に発見|GASで行ごとの登録計画をdry-run監査

ツールおじ|GoogleアカウントをAIエージェント化

ツールおじ|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();
}

実行手順

  1. 元ファイルではなく、検証用にコピーしたスプレッドシートを開きます。
  2. 拡張機能からApps Scriptを開き、コードを貼り付けます。
  3. プロジェクトのタイムゾーンが予定の運用地域と一致するか確認します。
  4. `previewCalendarImportPlanFree` を選んで実行します。
  5. 初回の権限確認後、「登録計画」シートを開きます。
  6. HOLD行の入力を直し、少量のダミーデータで再実行します。
  7. 実行ログは件数だけを確認し、予定名やカレンダーIDが出ていないことを確認します。

結果の読み方

READYは入力形式の検査を通過したという意味で、実際に登録できることを保証する判定ではありません。HOLDは、理由欄を直してからもう一度確認する行です。

監査結果では件名とカレンダーIDの中身を表示せず、「入力あり/空」だけを残します。元の行番号で候補表へ戻れるため、監査シートへ個人名や予定内容を複製しません。

失敗しやすい点と制限

  • 文字列の日付はシートの地域設定によって解釈が変わるため、セルを日時形式にそろえてください。
  • この例は候補表内の完全一致だけを確認します。既存のGoogleカレンダー予定との重複は調べません。
  • カレンダーへの閲覧権限・編集権限やApps Scriptの実行上限は確認しません。
  • 「登録計画」シートは毎回内容を作り直します。残したい結果は先に別ファイルへコピーしてください。
  • `dryRun` を変更しても、このコードに予定作成処理はありません。自動登録・自動削除・自動通知は行いません。

公開前チェックリスト

  • 元データではなくコピー環境で試した
  • タイムゾーンとシートの地域設定を確認した
  • 登録候補は200件以下に絞った
  • READYとHOLDの件数が想定どおりだった
  • 個人情報や認証情報を候補表・ログへ入れていない
  • 実際の登録は別工程として、少数ずつ人が確認する

まとめ

大量登録で怖いのは、コードが動かないことより、誤った候補がまとめて作成されることです。先に行番号つきの登録計画を作れば、開始・終了の逆転や入力漏れ、候補表内の重複を、カレンダーへ触れる前に修正できます。

朝のタスク整理や予定確認までまとめて整えたい方へ

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

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


あなたも記事の投稿・販売を
始めてみませんか?

Tipsなら簡単に記事を販売できます!
登録無料で始められます!

Tipsなら、無料ですぐに記事の販売をはじめることができます Tipsの詳細はこちら
 

この記事の販売者

ツールおじ|GoogleアカウントをAIエージェント化

おう、Gasおじだ。 AIツールとSaaSに散々突っ込んで遠回りした結果、最後に強かったのは「今あるGoogleアカウント」だった。 今ではAIエージェントであらゆるタスクを自動化して無双してるぞ。 GAS、スプレッドシート、フォーム、Gmail、AIエージェントを組み合わせれば、個人でも小さな会社でも、手作業だらけの業務はかなり自動化できる。 ここでは机上のAI論じゃなく、現場で動くテンプレ、コード、手順だけを置いていくぞ。 高いツールを増やす前に、まずお前さんのGoogleアカウントを仕事するエージェントに変えようじゃないか。

この販売者が書いた他の記事

  • Gmailの予約送信ミスを送信前に発見|GASで宛先・日時をdry-run監査

  • スプレッドシートの入力ミスを公開前に発見|GASでルール表をdry-run監査

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)

関連のおすすめ記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化

  • USBメモリで持ち運べるLinux Mint 22.1と永続的な書き込み領域の作成法【Rufus だけで作成する方法を初心者向けに徹底解説】

    ¥500
    1 %獲得
    (5 円相当)
    SmartSenior

    SmartSenior

  • 毎朝のToDoを区分・優先度・期限つきでまとめる Morning ToDo Mail Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化