🗓️

複数のGoogleカレンダーを統合して共有する

2021/04/09に公開

やりたいこと

「これだけ見れば袴谷の予定がわかるカレンダー」を作成する

背景

僕は元インターン先と現職、プライベートで合計3つのGoogleアカウントを所有しています。日程調整のためにカレンダーを見てもらうことが多いのですが、複数のカレンダーを見てもらうのは手間をかけてしまうため気が引けますよね。そこで、「これだけ見れば袴谷の予定がわかるカレンダー」の作成に至りました。

はじめに

早めに白状しておくと、この記事はパクリです。以下の記事を参考にしつつ、自分がよりしっくりくる実装に微調整しただけなので、パクリ反対派の方は元記事をご覧ください。
https://koyacode.com/sync-google-calendars-with-gas/

既知の問題

以下の問題は未解決です。これらを実装するために来てくださった方は、残念ですがお帰りください。

  • 更新のタイミングで一時的(1分未満)に統合用カレンダーのイベントが見えなくなる
  • 出欠回答に "いいえ" || "未定" と回答したイベントも "Busy" 扱いになってしまう

下準備

まずは、共有専用の統合カレンダー (integratedCalendar) を新規作成する必要があります。以下を参考に、好きな名前のカレンダーを作成してください。ちなみに自分は 袴谷優介 (Integrated) にしています。
https://support.google.com/calendar/answer/37095?hl=ja

結論

最終的なコードは下記の通りです。最新版はこちらからご覧ください。

main.gs
const calendarIntegrated = CalendarApp.getCalendarById('統合用カレンダーのID');
const calendarPersonal = CalendarApp.getCalendarById('個人用カレンダーのID');
const calendarWork = CalendarApp.getCalendarById('仕事用カレンダーのID');

function syncToday() {
  deleteEventsForDays(calendarIntegrated, 1);
  copyEventsForDays(calendarPersonal, calendarIntegrated, 'Personal', 1);
  copyEventsForDays(calendarWork, calendarIntegrated, 'Work', 1);
}

function syncThisWeek() {
  deleteEventsForDays(calendarIntegrated, 7);
  copyEventsForDays(calendarPersonal, calendarIntegrated, 'Personal', 7);
  copyEventsForDays(calendarWork, calendarIntegrated, 'Work', 7);
}

function syncThisMonth() {
  deleteEventsForDays(calendarIntegrated, 30);
  copyEventsForDays(calendarPersonal, calendarIntegrated, 'Personal', 30);
  copyEventsForDays(calendarWork, calendarIntegrated, 'Work', 30);
}
sync.gs
function deleteEventsForDays(calendar, days) {
  let date = new Date();

  for (let i = 0; i < days; i++) {
    const events = calendar.getEventsForDay(date);

    for (const event of events) {
      event.deleteEvent();
    }
    date.setDate(date.getDate() + 1);
  }
}

function copyEventsForDays(calendarOrigin, calendarIntegrated, organization, days) {
  let date = new Date();

  for (let i = 0; i < days; i++) {
    const events = calendarOrigin.getEventsForDay(date);

    for (const event of events) {
      let startTime, endTime;

      startTime = event.getStartTime();
      endTime = event.getEndTime();
      calendarIntegrated.createEvent(
        setTitle(organization),
        startTime,
        endTime
      )
    }
    date.setDate(date.getDate() + 1);
  }
}
utils.gs
function setTitle(organization) {
  return `Busy (${organization})`
}

実装内容の説明

main.gs

まず、カレンダーのIDを設定します。ここでは統合用と個人用、仕事用の3つを用意しています。統合したいカレンダーが3つ以上ある場合は、その分増やしてください。

main.gs
const calendarIntegrated = CalendarApp.getCalendarById('統合用カレンダーのID');
const calendarPersonal = CalendarApp.getCalendarById('個人用カレンダーのID');
const calendarWork = CalendarApp.getCalendarById('仕事用カレンダーのID');

次に、ベースとなる関数を記述します。基本的な流れは以下の通りです。

  1. 統合用カレンダーを全て削除する
  2. 個人用カレンダーのイベントを統合用カレンダーにコピーする
  3. 仕事用カレンダーのイベントを統合用カレンダーにコピーする
    毎回全削除 → 全コピーを行うという不健全な実装になっている理由は、カレンダーの追加/削除を検知するAPIが用意されていないためです。もしこれよりも良いアイデアがあればお知らせください。
main.gs
function syncToday() {
  // 今日のイベントをcalendarIntegratedから全て削除
  deleteEventsForDays(calendarIntegrated, 1);
 // 今日のイベントをcalendarPersonalからcalendarIntegratedへ全てコピー
  copyEventsForDays(calendarPersonal, calendarIntegrated, 'Personal', 1);
  // 今日のイベントをcalendarWorkからcalendarIntegratedへ全てコピー
  copyEventsForDays(calendarWork, calendarIntegrated, 'Work', 1);
}

function syncThisWeek() {
  deleteEventsForDays(calendarIntegrated, 7);
  copyEventsForDays(calendarPersonal, calendarIntegrated, 'Personal', 7);
  copyEventsForDays(calendarWork, calendarIntegrated, 'Work', 7);
}

function syncThisMonth() {
  deleteEventsForDays(calendarIntegrated, 30);
  copyEventsForDays(calendarPersonal, calendarIntegrated, 'Personal', 30);
  copyEventsForDays(calendarWork, calendarIntegrated, 'Work', 30);
}

sync.gs

ここが本プログラムの中核を成す部分です。関数2つの機能はそれぞれ以下の通りです。

  • deleteEventsForDays()
    • 第一引数: 削除対象のカレンダーオブジェクトです。ここでは、calendarIntegratedのみが渡されます
    • 第二引数: 日数です。この日数分のイベントを全て削除します
  • copyEventsForDays()
    • 第一引数: コピー元のカレンダーオブジェクトです。ここでは、calendarPersonalもしくはcalendarWorkのいずれかが渡されます
    • 第二引数: コピー先のカレンダーオブジェクトです。ここでは、calendarIntegratedのみが渡されます
    • 第三引数: 日数です。この日数分のイベントを全てコピーします。
sync.gs
function deleteEventsForDays(calendar, days) {
  // dateに今日の日付を入れる
  let date = new Date();

  // 渡された日数分繰り返す
  for (let i = 0; i < days; i++) {
    const events = calendar.getEventsForDay(date);

    // その日のイベントを全て削除する
    for (const event of events) {
      event.deleteEvent();
    }
    // dateを1日進める
    date.setDate(date.getDate() + 1);
  }
}

function copyEventsForDays(calendarOrigin, calendarIntegrated, organization, days) {
  let date = new Date();

  // 渡された日数分繰り返す
  for (let i = 0; i < days; i++) {
    const events = calendarOrigin.getEventsForDay(date);

    // その日のイベントを全て削除する
    for (const event of events) {
      let startTime, endTime;

      startTime = event.getStartTime();
      endTime = event.getEndTime();
      calendarIntegrated.createEvent(
        setTitle(organization),
        startTime,
        endTime
      )
    }
    // dateを1日進める
    date.setDate(date.getDate() + 1);
  }
}

utils.gs

utils と言いつつも関数は一つだけです。setTitle() は上述の copyEventsForDays() から呼び出されます。たとえばPersonalの時は、イベントのタイトルが Busy (Personal) になります。

utils.gs
function setTitle(organization) {
  return `Busy (${organization})`
}

トリガーのセット

まずは上記のプログラムをScript Editorにコピペし、一度実行してみてください。それが成功したら、最後にトリガーをセットし、自動化します。先述のエラーを防ぐため、最も負荷の高い syncThisMonth() は頻度を低く、syncToday() は最新の情報であることが求められるため頻度を高く設定します。これはご自身の感覚で設定してください。参考までに自分の設定をお示しします。

  • syncToday(): 1時間おき
  • syncThisWeek(): 12時間おき
  • syncThisMonth(): 24時間おき(毎朝6~7時の間)

    main.gs に記載した関数3点に対して、それぞれトリガーをセット

    たとえば、syncToday() は1時間おきに実行する

参考にした記事

先述の通り、この記事は他人のパクリです。私の説明が不足している点は、ぜひこちらと併せてご確認ください。
https://koyacode.com/sync-google-calendars-with-gas/

Discussion