GAS

「この予定、いつ・誰と入れたんだっけ?」をGoogleカレンダーで調べる。GAS×Claudeで作った作成日チェッカー

Shinpei Okada

複数のクライアントを抱えて仕事をしていると、半年先までびっしり予定が埋まる。そして、ふとカレンダーを眺めたときに、こう思う瞬間がある。

「この打ち合わせ、いつ入れたんだ?」
「そもそも、誰との約束だっけ……」
「日付変更の操作ミスかな?」

タイトルが雑だったり、当時の自分にしか分からない略称だったりすると、もう手がかりがない。相手に「いつお約束しましたっけ?」と聞くのも気が引ける。

本記事では、この「身に覚えのない予定問題」を、Googleカレンダーの予定が”いつ作成・更新されたか”を調べることで解決するツールを、Claudeとの対話で作った話を紹介する。

例によって動くソースコードを公開しているので、よかったら試してみてね。(注意事項はよく読んで、自己責任で)

目次

そもそも、Googleカレンダーは予定の「作成日」を見せてくれない

身に覚えのない予定があったとき、いちばん知りたいのは「いつこの予定を登録したか」だ。それが分かれば、「ああ、あの日に商談した相手だな」と記憶を辿る起点になる。

ところが困ったことに、Googleカレンダーの標準UIには、予定の作成日を表示する機能がない。 予定をクリックして詳細を開いても、開始・終了時刻や場所は出てくるが、「この予定がいつ作られたか」はどこにも書かれていない。編集しても、更新日が表示されるわけでもない。

つまり標準機能の範囲では、「身に覚えのない予定」を前にしたとき、打てる手は限られる。

  1. 議事録やメッセンジャー系を片っ端から読み返す(メール・チャット・LINE……)。地道だが、見つからないと徒労に終わる
  2. 関係しそうな相手に直接聞く。確実ではあるが、毎回やるのは気まずいし失礼にもなりかねない

どちらも筋が悪い。そこで発想を変える。予定を作成した日付さえ分かれば、その日にやり取りした相手から逆算できる——この方針なら、自分のカレンダーの中だけで完結する。

実はこのアイデア、note で Shuhei Maruyama さんが紹介していた手法が元になっている。Google Apps Script(GAS)で getDateCreated() を呼べば、予定の作成日時が取れるというものだ。標準UIにない情報も、APIレベルでは保持されている。これは目から鱗だった。

ただ、元記事のスクリプトはコードエディタで日付を直接書き換えて実行ログを読む形式。毎回コードをいじるのは少し手間だ。そこで今回は、これを日付を選ぶだけで使えるWebアプリに仕立て直し、ついでに「更新日」も取れるよう拡張してみた。

解決策:ClaudeにGASのWebアプリを作ってもらった

Googleカレンダーの作成日チェッカー

やったことはシンプルで、元記事のアイデアをClaudeに伝え、「指定した日付の予定について、作成日と更新日を一覧表示するGASのWebアプリを作って。カレンダーはプルダウンで選べるように」と頼んだだけ。最初の出力で、サーバーサイドの コード.gs とフロントの index.html が揃った動作版が出てきた。

完成したツールの構成

技術的には前回のDrive Copierと同じく、GAS(Google Apps Script)のWebアプリHtmlService でHTMLをホストし、google.script.run でサーバーサイド関数を非同期に呼び出す、オーソドックスな2ファイル構成だ。

核になっているのは、CalendarApp.getEventsForDay() で指定日の予定を取得し、各予定に対して getDateCreated()(作成日時)と getLastUpdated()(最終更新日時)を呼ぶ部分。元記事のロジックそのままだが、「作成日」と「更新日」の両方を見られるようにしたことで、「いつ作ったか」だけでなく「あとから編集したか」も分かるようになっている。

GASで完結するので、Google Workspaceの有料契約もクレジットカードも、サーバーも不要。必要なのはGoogleアカウントだけ。デプロイしたURLをブックマークしておけば、いつでも呼び出せる。

できること

  • カレンダーをプルダウンで選択。自分がアクセスできる全カレンダーから選べる(メインカレンダーが先頭に来る)
  • 日付を選んで「調べる」を押すだけで、その日の予定と作成日・更新日が一覧表示される
  • 作成日が新しい順にソート。「最近うっかり入れた予定」を上から確認しやすい
  • 作成日と更新日が違う場合だけ更新日も表示。編集済みの予定がひと目で分かる
  • 終日/時間帯・場所・参加者(ゲスト)もまとめて表示。作成日と突き合わせれば、相手の特定がぐっと早くなる

コードを書く知識そのものより、「自分が何に困っていて、どう解決したいか」を言語化してAIに渡す。今回もその一往復で、実用的なものが形になった。

導入手順とソースコード

手順

  1. Google Apps Script にアクセスし、「新しいプロジェクト」を作成
  2. デフォルトの コード.gs に、下記の コード.gs の内容を貼り付ける
  3. エディタ左のファイル一覧で「+」→「HTML」を選択し、ファイル名を index にする(.html は自動で付く)
  4. 作成された index.html に、下記の index.html の内容を貼り付ける
  5. 「デプロイ」→「新しいデプロイ」→ 種類を「ウェブアプリ」に設定
  6. 「アクセスできるユーザー」は必ず「自分のみ」にする
  7. 「デプロイ」を押してURLを取得。ブックマークしておくと便利

初回はカレンダーへのアクセス許可を求められる。自分で作ったスクリプトなので、「このアプリはGoogleで確認されていません」という警告が出ても、「詳細」→「(プロジェクト名)に移動」と進んで許可して問題ない。

コード.gs

/**
 * Googleカレンダー 予定の「作成日・更新日」チェッカー
 * note記事(its_maru 氏)の getDateCreated() を使うアイデアを
 * Webアプリ化し、作成日に加えて更新日も取得できるように拡張したもの。
 */

// Webアプリのエントリポイント(URLにアクセスすると呼ばれる)
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('カレンダー予定の作成日チェッカー')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
}

// 利用可能なカレンダー一覧を返す(UIのプルダウン用)
function getCalendarList() {
  var cals = CalendarApp.getAllCalendars();
  var defaultId = CalendarApp.getDefaultCalendar().getId();

  // デフォルト(メイン)カレンダーを先頭に並べる
  cals.sort(function(a, b) {
    if (a.getId() === defaultId) return -1;
    if (b.getId() === defaultId) return 1;
    return a.getName().localeCompare(b.getName(), 'ja');
  });

  return cals.map(function(c) {
    return {
      id: c.getId(),
      name: c.getName(),
      isDefault: c.getId() === defaultId
    };
  });
}

// 指定日の予定と、その作成日・更新日を取得する
//   dateStr   : 'yyyy-MM-dd' 形式の日付文字列
//   calendarId: カレンダーID(空 or 'default' でメインカレンダー)
function getEventsInfo(dateStr, calendarId) {
  var cal = (calendarId && calendarId !== 'default')
    ? CalendarApp.getCalendarById(calendarId)
    : CalendarApp.getDefaultCalendar();

  if (!cal) {
    throw new Error('カレンダーが見つかりませんでした(ID: ' + calendarId + ')');
  }

  // 'yyyy-MM-dd' をローカル日付として解釈して当日0時を作る
  var parts = String(dateStr).split('-');
  var y = Number(parts[0]);
  var m = Number(parts[1]);
  var d = Number(parts[2]);
  if (!y || !m || !d) {
    throw new Error('日付の形式が正しくありません: ' + dateStr);
  }
  var date = new Date(y, m - 1, d, 0, 0, 0);

  var tz = cal.getTimeZone() || Session.getScriptTimeZone();
  var events = cal.getEventsForDay(date); // 記事と同じく指定日の予定を取得

  var fmt = function(dt) {
    if (!dt) return '';
    return Utilities.formatDate(dt, tz, 'yyyy/MM/dd HH:mm:ss');
  };

  var rows = events.map(function(ev) {
    return {
      title:    ev.getTitle() || '(タイトルなし)',
      allDay:   ev.isAllDayEvent(),
      start:    fmt(ev.getStartTime()),
      end:      fmt(ev.getEndTime()),
      created:  fmt(ev.getDateCreated()),
      updated:  fmt(ev.getLastUpdated()),
      location: ev.getLocation() || '',
      guests:   ev.getGuestList().map(function(g){ return g.getEmail(); }).join(', ')
    };
  });

  // 作成日が新しい順に並べ替え
  rows.sort(function(a, b) {
    return (a.created < b.created) ? 1 : (a.created > b.created) ? -1 : 0;
  });

  return {
    calendarName: cal.getName(),
    timeZone: tz,
    date: Utilities.formatDate(date, tz, 'yyyy/MM/dd (E)'),
    count: rows.length,
    events: rows
  };
}

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <base target="_top">
  <meta charset="utf-8">
  <style>
    :root {
      --bg: #f4f5f7;
      --card: #ffffff;
      --ink: #1f2933;
      --muted: #687387;
      --line: #e3e6ec;
      --brand: #3b6ef5;
      --brand-dark: #2a55d4;
      --accent: #fff4e5;
      --accent-line: #f0c178;
      --shadow: 0 1px 3px rgba(16,24,40,.08), 0 6px 18px rgba(16,24,40,.06);
    }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans",
                   "Noto Sans JP", Meiryo, sans-serif;
      background: var(--bg);
      color: var(--ink);
      line-height: 1.6;
      -webkit-font-smoothing: antialiased;
    }
    .wrap { max-width: 880px; margin: 0 auto; padding: 32px 20px 64px; }

    header h1 {
      font-size: 22px; margin: 0 0 4px; letter-spacing: .01em;
    }
    header p { margin: 0; color: var(--muted); font-size: 13.5px; }

    .panel {
      background: var(--card);
      border: 1px solid var(--line);
      border-radius: 14px;
      box-shadow: var(--shadow);
      padding: 22px;
      margin-top: 22px;
    }

    .controls {
      display: grid;
      grid-template-columns: 1fr 1fr auto;
      gap: 14px;
      align-items: end;
    }
    @media (max-width: 640px) {
      .controls { grid-template-columns: 1fr; }
    }
    label {
      display: block; font-size: 12.5px; font-weight: 600;
      color: var(--muted); margin-bottom: 6px;
    }
    input[type="date"], select {
      width: 100%; padding: 10px 12px; font-size: 14px;
      border: 1px solid var(--line); border-radius: 9px;
      background: #fff; color: var(--ink); font-family: inherit;
    }
    input[type="date"]:focus, select:focus {
      outline: none; border-color: var(--brand);
      box-shadow: 0 0 0 3px rgba(59,110,245,.15);
    }
    button {
      padding: 11px 22px; font-size: 14px; font-weight: 600;
      border: none; border-radius: 9px; cursor: pointer;
      background: var(--brand); color: #fff; white-space: nowrap;
      transition: background .15s;
    }
    button:hover { background: var(--brand-dark); }
    button:disabled { opacity: .55; cursor: default; }

    .status {
      margin-top: 16px; font-size: 13.5px; color: var(--muted);
      min-height: 20px;
    }
    .status.err { color: #c0392b; }

    .summary {
      display: none;
      margin-top: 4px; font-size: 13.5px; color: var(--ink);
    }
    .summary b { color: var(--brand); }

    .results { margin-top: 8px; }
    .event {
      border: 1px solid var(--line);
      border-radius: 12px;
      padding: 16px 18px;
      margin-top: 12px;
      background: #fff;
    }
    .event .ttl {
      font-size: 15.5px; font-weight: 700; margin: 0 0 10px;
      display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    }
    .badge {
      font-size: 11px; font-weight: 600; padding: 2px 8px;
      border-radius: 999px; background: #eef2fb; color: var(--brand-dark);
    }
    .badge.allday { background: #e9f6ee; color: #1f7a44; }

    .meta {
      display: grid; grid-template-columns: repeat(2, minmax(0,1fr));
      gap: 6px 22px; font-size: 13px;
    }
    @media (max-width: 520px) { .meta { grid-template-columns: 1fr; } }
    .meta .row { display: flex; gap: 8px; }
    .meta .k { color: var(--muted); min-width: 64px; }
    .meta .v { color: var(--ink); }
    .meta .v.created { font-weight: 700; }

    .empty {
      text-align: center; color: var(--muted);
      padding: 36px 0; font-size: 14px;
    }
    .note {
      margin-top: 26px; font-size: 12.5px; color: var(--muted);
      background: var(--accent); border: 1px solid var(--accent-line);
      border-radius: 10px; padding: 12px 14px;
    }
    .spinner {
      display: inline-block; width: 14px; height: 14px;
      border: 2px solid #cdd5e3; border-top-color: var(--brand);
      border-radius: 50%; animation: spin .7s linear infinite;
      vertical-align: -2px; margin-right: 7px;
    }
    @keyframes spin { to { transform: rotate(360deg); } }
  </style>
</head>
<body>
  <div class="wrap">
    <header>
      <h1>🗓 カレンダー予定の作成日チェッカー</h1>
      <p>指定した日の予定が「いつ作成・更新されたか」を調べます。</p>
    </header>

    <div class="panel">
      <div class="controls">
        <div>
          <label for="cal">カレンダー</label>
          <select id="cal"><option>読み込み中…</option></select>
        </div>
        <div>
          <label for="date">調べたい日付</label>
          <input type="date" id="date">
        </div>
        <div>
          <button id="run" onclick="run()">調べる</button>
        </div>
      </div>
      <div class="status" id="status"></div>
      <div class="summary" id="summary"></div>
    </div>

    <div class="results" id="results"></div>

    <div class="note">
      ヒント: 作成日が「身に覚えのある日」と一致すれば、その日にやり取りした相手との予定である可能性が高いです。
      予定を編集している場合は更新日が作成日より後になります。
    </div>
  </div>

  <script>
    // ── 起動時: カレンダー一覧の取得 & 日付を今日に初期化 ──
    window.addEventListener('load', function() {
      var today = new Date();
      var iso = today.getFullYear() + '-'
              + String(today.getMonth() + 1).padStart(2, '0') + '-'
              + String(today.getDate()).padStart(2, '0');
      document.getElementById('date').value = iso;

      google.script.run
        .withSuccessHandler(fillCalendars)
        .withFailureHandler(function(e){ showError(e.message); })
        .getCalendarList();
    });

    function fillCalendars(list) {
      var sel = document.getElementById('cal');
      sel.innerHTML = '';
      list.forEach(function(c) {
        var opt = document.createElement('option');
        opt.value = c.id;
        opt.textContent = c.name + (c.isDefault ? '(メイン)' : '');
        sel.appendChild(opt);
      });
    }

    // ── 実行 ──
    function run() {
      var calId = document.getElementById('cal').value;
      var date  = document.getElementById('date').value;
      if (!date) { showError('日付を選んでください。'); return; }

      var btn = document.getElementById('run');
      btn.disabled = true;
      document.getElementById('summary').style.display = 'none';
      document.getElementById('results').innerHTML = '';
      setStatus('<span class="spinner"></span>取得中…', false);

      google.script.run
        .withSuccessHandler(function(data) { btn.disabled = false; render(data); })
        .withFailureHandler(function(e) { btn.disabled = false; showError(e.message); })
        .getEventsInfo(date, calId);
    }

    // ── 結果表示 ──
    function render(data) {
      setStatus('', false);
      var sum = document.getElementById('summary');
      sum.style.display = 'block';
      sum.innerHTML = '<b>' + escapeHtml(data.calendarName) + '</b> / '
                    + escapeHtml(data.date) + ' の予定: <b>' + data.count + '件</b>'
                    + '(タイムゾーン: ' + escapeHtml(data.timeZone) + ')';

      var box = document.getElementById('results');
      if (!data.events.length) {
        box.innerHTML = '<div class="panel"><div class="empty">この日には予定がありませんでした。</div></div>';
        return;
      }

      var html = '';
      data.events.forEach(function(ev) {
        var badge = ev.allDay
          ? '<span class="badge allday">終日</span>'
          : '<span class="badge">' + escapeHtml(timePart(ev.start)) + '〜' + escapeHtml(timePart(ev.end)) + '</span>';
        var updatedRow = (ev.updated && ev.updated !== ev.created)
          ? '<div class="row"><span class="k">更新日</span><span class="v">' + escapeHtml(ev.updated) + '</span></div>'
          : '';
        var locRow = ev.location
          ? '<div class="row"><span class="k">場所</span><span class="v">' + escapeHtml(ev.location) + '</span></div>'
          : '';
        var guestRow = ev.guests
          ? '<div class="row"><span class="k">参加者</span><span class="v">' + escapeHtml(ev.guests) + '</span></div>'
          : '';

        html +=
          '<div class="event">' +
            '<p class="ttl">' + escapeHtml(ev.title) + ' ' + badge + '</p>' +
            '<div class="meta">' +
              '<div class="row"><span class="k">作成日</span><span class="v created">' + escapeHtml(ev.created || '不明') + '</span></div>' +
              updatedRow + locRow + guestRow +
            '</div>' +
          '</div>';
      });
      box.innerHTML = html;
    }

    function timePart(s) {
      // 'yyyy/MM/dd HH:mm:ss' → 'HH:mm'
      var t = (s || '').split(' ')[1] || '';
      return t.slice(0, 5);
    }
    function setStatus(html, isErr) {
      var el = document.getElementById('status');
      el.innerHTML = html;
      el.className = 'status' + (isErr ? ' err' : '');
    }
    function showError(msg) {
      setStatus('エラー: ' + escapeHtml(msg || '不明なエラー'), true);
    }
    function escapeHtml(s) {
      return String(s).replace(/[&<>"']/g, function(c) {
        return { '&':'&','<':'<','>':'>','"':'"',"'":''' }[c];
      });
    }
  </script>
</body>
</html>

使用上の注意

便利なツールだが、運用にあたって押さえておきたいポイントがある。

デプロイ時のアクセス権限は「自分のみ」に

ウェブアプリとして公開する際、「アクセスできるユーザー」を「全員」にすると、URLを知る第三者があなたの権限でカレンダーを読めてしまう。必ず「自分のみ」に設定し、自分専用のプライベートツールとして運用すること。

見られるのは自分がアクセスできるカレンダーだけ

このツールが扱えるのは、あなたのアカウントが閲覧権限を持つカレンダーに限られる。他人のカレンダーを勝手に覗けるわけではないので、その点は安心してほしい。逆に言えば、共有された相手のカレンダーであれば作成日も取得できる。

getEventsForDay() の挙動

指定日に「開催される」予定を返す仕様なので、複数日にまたがる予定は開始日・終了日の両方の検索でヒットする。1件を厳密に追いたい場合だけ、頭の片隅に置いておくといい。

GASの「6分ルール」

GASは1回の実行が最大6分という制限がある。1日分の予定を読む程度ならまず問題にならないが、何かと組み合わせて大量処理する場合は気に留めておきたい。

実際にかかった時間と使ったツール

今回は要件がシンプルだったこともあり、最初のプロンプト一回で、Webアプリ UI 込みの動作版が出てきた。 元記事のロジックがしっかりしていたおかげで、こちらは「作成日に加えて更新日も」「カレンダーはプルダウンで」と方針を渡すだけで済んだ。実働は数分。

使った生成AIは Claude。GAS のような単機能のツールは、要件さえ言語化できれば一往復でほぼ完成形まで届くことが多い。前回のDrive Copierでも感じたが、「最初から完璧な指示を出す必要はない」——大まかに伝えて、動くものを触って、足りなければ追加で頼む。このループで実用品ができてしまう。

まとめ

Googleカレンダーは予定の作成日を標準では見せてくれない。だが、APIレベルではちゃんと保持されていて、GASの getDateCreated() を使えば取り出せる。元になったnote記事の手法を、日付を選ぶだけで使えるWebアプリにして、更新日まで確認できるよう少し広げてみた。

こうした「ちょっとした不便」は、業務のあちこちに転がっている。標準機能でできないと分かった瞬間に諦めるのではなく、「APIなら取れるのでは?」「AIに頼めば数分で道具になるのでは?」と一歩進めてみる。その小さな習慣の積み重ねが、結局いちばん効いてくると思う。

身に覚えのない予定を前にして途方に暮れたら、まずは作成日を調べてみてね。

  • URLをコピーしました!

この記事を書いた人

地方テレビ局、歯科コンサル、中堅SIerを経て独立。ダイヤルアップ接続の時代にHTMLに魅せられ、なんだかんだ10年以上WEB制作に関わり続けている。近年はNotionとn8nを軸にしたワークフロー構築に注力。生活しているだけでユーザーの哲学や日々の情報を抽出・蓄積し、AIによるデータ活用が可能になるシステム「MIMIR」の開発に取り組んでいます。

目次