カレンダー

複数のGoogleカレンダーの予定を別のカレンダーに定期的にコピーしてミーティングなどの予定調整を楽にする

freeeに関係ない話題ですが、Google Apps Script(以下GAS)でできる効率化の例として番外編としてfreee API以外のGASでの活用に関しても記事にしています。

今回は、「複数のGoogleカレンダーの予定を別のカレンダーに定期的にコピーしてミーティングなどの予定調整を楽にする」です。

複数のGoogleカレンダー を使っているとミーティング調整が面倒

複数のプロジェクトや組織・部門にまたがってそれぞれで予定をGoogleカレンダーで管理していたりする場合、ミーティング等のための空き予定枠の共有や調整に手間がかかります。

理想的には自分のカレンダーを予定詳細は見えない設定にて共有し、空いているスケジュールにミーティングのリクエストを入れてもらうのがスムーズです。

全ての予定を1つのカレンダーで管理できればよいですが、会社で利用しているGoogle Workspaceのカレンダーにプライベートな予定を入力するのは抵抗があります(非公開設定にしても特権管理者には見ることができる)。 既にプライベートでGoogleカレンダーを予定管理に利用している場合などは、予定をコピーするなど二度手間となってしまいます。

そこで、自身が管理している複数のカレンダーの予定を定期的にある特定のカレンダーにコピーするGoogle Apps Script(以下 GAS)を書いてみました。

まず完成形のコードはこちらになります。

/**
 * 複数のカレンダーの予定を別のカレンダーにコピーしていく関数
 * トリガーで1日1回実行することを想定
 */

function mergeCalendarEvents() {

  /* 予定を結合させたいコピー先カレンダーのID */
  const idMergedCal = '予定を転記したいカレンダーのID';

  /* 予定のコピー元のカレンダーの配列 */
  const idsCopyFrom = ['カレンダー1のID', 'カレンダー2のID', 'カレンダー3のID'];

  /* 後で削除する時のためのタグのキーと値 */
  // https://developers.google.com/apps-script/reference/calendar/calendar-event#setTag(String,String)
  const key = 'gas'; 
  const tag = 'copied';

  /* 操作する期間を指定 */
  const term = 28; // 予定をコピーする期間(日数)を指定
  const now = new Date(); // 現在日時を取得
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0); // 今日の日付 00:00:00を取得
  const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1, 0); // 昨日の日付 00:00:00を取得
  const endBefore = new Date(today.getFullYear(), today.getMonth(), today.getDate() + term, 0); // この日付の前日までの予定を操作

  // コピー先のカレンダーの取得
  const calMerged = CalendarApp.getCalendarById(idMergedCal);

  /* 過去に登録した予定でタグが付与されているものを重複防止の為に削除 */
  const overlapEvents = calMerged.getEvents(yesterday, endBefore); // yesterdayをtodayにすれば過去の予定は残る
  overlapEvents.forEach(event => {
    if (event.getTag(key) === tag) {
      event.deleteEvent();
      Utilities.sleep(1000); // カレンダーの連続操作はエラーになりやすい
    };
  });

  /* コピー元のカレンダーから予定をタグを付与してコピー */
  idsCopyFrom.forEach(id => {
    const calCopyFrom = CalendarApp.getCalendarById(id); // カレンダーオブジェクトをIDから取得
    copyEventsBlock_(calCopyFrom, today, endBefore, calMerged, key, tag);
  });

}

/**
 * タグを付与しながら指定期間のイベントを別のカレンダーにコピーしていく関数
 * 
 * @param   {Object}  calCopyFrom - 予定のコピー元カレンダーオブジェクト
 * @param   {Object}  startFrom - 予定コピーの開始日(Dateオブジェクト)
 * @param   {Object}  endBefore - 予定コピーの終了日(Dateオブジェクト)※操作対象日に含まれない
 * @param   {Object}  calCopyTo - 予定のコピー先カレンダーオブジェクト
 * @param   {string}  key - イベントオブジェクトに付与するキー(カレンダーUIでは不可視)
 * @param   {string}  tag - イベントオブジェクトに付与するキーとペアになる値(カレンダーUIでは不可視)
 * @return
 */

function copyEventsBlock_(calCopyFrom, startFrom, endBefore, calCopyTo, key, tag) {

  // コピーする予定を非公開に
  const visibility = CalendarApp.Visibility.PRIVATE;

  // カレンダー内の予定を取得
  const events = calCopyFrom.getEvents(startFrom, endBefore);

  // カレンダー内の予定を一つずつコピーしていく
  for (const event of events) {

    //終日イベントか時間単位イベントか判別
    if (event.isAllDayEvent()) {
      const title = event.getTitle(); //コピー元予定のタイトルを取得
      const startDate = event.getAllDayStartDate(); //コピー元予定の開始日を取得
      const endDate = event.getAllDayEndDate(); //コピー元予定の終了日を取得

      // 新しい非公開イベントを作成し各値とタグを付与
      // タイトルにはコピーされた予定とわかるようにCopied:の接頭詞
      const copiedAllDayEvent = calCopyTo.createAllDayEvent(`Copied:${title}`, startDate, endDate).setTag(key, tag).setVisibility(visibility);
      copiedAllDayEvent.setColor(CalendarApp.EventColor.PALE_GREEN); // イベントのカラーを指定 https://developers.google.com/apps-script/reference/calendar/event-color
      Utilities.sleep(1000);

    } else {
      const title = event.getTitle(); //コピー元予定のタイトルを取得
      const startTime = event.getStartTime(); //コピー元予定の開始時間を取得
      const endTime = event.getEndTime(); //コピー元予定の終了時間を取得

      // 新しい非公開イベントを作成し各値とタグを付与
      // タイトルにはコピーされた予定とわかるようにCopied:の接頭詞
      const copiedEvent = calCopyTo.createEvent(`Copied:${title}`, startTime, endTime).setTag(key, tag).setVisibility(visibility);
      copiedEvent.setColor(CalendarApp.EventColor.PALE_GREEN); // イベントのカラーを指定 https://developers.google.com/apps-script/reference/calendar/event-color
      Utilities.sleep(1000);
    }
  }
}

mergeCalendarEventsという実行関数とcopyEventsBlock_というタグを付与しながら指定期間のイベントを別のカレンダーにコピーしていく関数の2つで実装しています。

それぞれのスクリプトがどんな機能を持っているのか、詳細を説明していきます。

カレンダーにはそれぞれIDがある

まず必要になってくるのは、カレンダーに固有のIDです。コピー元、コピー先のそれぞれのカレンダーIDを確認しましょう。カレンダーのIDの確認方法は、以下の記事で紹介されています。

いつも隣にITのお仕事|Google Apps ScriptでGoogleカレンダーを操作する最初の一歩のスクリプト

IDが確認できたら、コピー先のカレンダーIDとコピー元のカレンダーIDを定数に格納しておきます。コピー元のカレンダーIDは複数になることが想定されるので配列に格納します。

  /* 予定を結合させたいコピー先カレンダーのID */
  const idMergedCal = '予定を転記したいカレンダーのID';

  /* 予定のコピー元のカレンダーの配列 */
  const idsCopyFrom = ['カレンダー1のID', 'カレンダー2のID', 'カレンダー3のID'];

Googleカレンダーの予定には画面上で確認できないタグ(キーと値のペア)を付与できる

ここが今回のスクリプトのポイントになります。Googleカレンダーの予定をGASから操作する場合、UI(ユーザーインターフェイス)からは確認(見ることが)できないタグ(キーと値のペア)を設定することができます。

これはCalendarEventクラスのsetTag(key, value)というメソッドで実現できます。タグのセットだけでなく、削除や参照するメソッドも当然用意されています。以下がタグを操作するメソッドの一覧です。

このタグの機能を利用して、GASを用いてコピーした予定を判別できるようにしたいと思います。これは、定期的に予定をアップデート(コピー元のカレンダーの予定変更や削除)に対応するためにGASで登録した予定を一度全て削除してから新しい予定をコピーできるようにするためです。

  /* 後で削除する時のためのタグのキーと値 */
  const key = 'gas'; 
  const tag = 'copied';

予定をコピーする期間(日数)を指定する

カレンダーの予定のコピーや削除は意外と負荷がかかるみたいで、あまりにも件数が多いとエラーになったりします。そのためあらかじめ予定をコピーする期間を指定しています。

  /* 操作する期間を指定 */
  const term = 28; // 予定をコピーする期間(日数)を指定
  const now = new Date(); // 現在日時を取得
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0); // 今日の日付 00:00:00を取得
  const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1, 0); // 昨日の日付 00:00:00を取得
  const endBefore = new Date(today.getFullYear(), today.getMonth(), today.getDate() + term, 0); // この日付の前日までの予定を操作

サンプルコードの場合は28日間を指定しています。これは予定の取得開始日(スクリプト実行当日)を含む28日間の予定をコピーするよう指定します。

例:2022年8月6日(土)から9月2日(金)まで

注意点は、このスクリプトをトリガー機能を使って自動実行する場合、const now = new Date() を予定の取得開始日時にすると実行時より前の時間に開始される予定が取得できなくなるので、取得開始日用にtodayの定数に実行日の 00:00:00 の時間のDateオブジェクトを格納しています。

定数 yesterdayは、マージしたカレンダーから過去の予定を削除するための取得開始日として準備しています。これは所属している組織のカレンダーにプライベートのカレンダーから予定をコピーする場合などで、今後の予定調整に不要な過去の予定を削除するために用意をしています(退職後等に管理者などに見られてしまうため)。

そして一番のポイントが予定取得の終了日となる定数 endBeforeです。これは定数 termで指定した日数を加算したDateオブジェクトになりますが、GASのカレンダー操作に使うメソッドで引数に指定する取得終了日は、予定の取得対象の日付には含まれない(その日付の直前までの予定が操作対象となる)点が注意が必要です。

例:2022年8月6日に上記関数を実行した場合 → endBeforeは、2022年9月3日 00:00:00のDateオブジェクトとなる(= 9月2日までの予定が取得対象となる)。

カレンダーの予定は常に変わるもの…アップデート時には過去にコピーした予定を削除する必要がある

続いていよいよカレンダーに予定をコピーしていく段階ですが、その前にカレンダーの予定は常に変化するものなので、過去に自動コピーした予定が今も有効な予定としてコピー元のカレンダーに残っているとは限りません。

そのためこのスクリプトの実行時には、一度自動コピーした予定を削除して、新しくコピー元のカレンダーから予定を取得してコピーする必要があります。

  // コピー先のカレンダーの取得
  const calMerged = CalendarApp.getCalendarById(idMergedCal);

  /* 過去に登録した予定でタグが付与されているものを重複防止の為に削除 */
  const overlapEvents = calMerged.getEvents(yesterday, endBefore); // yesterdayをtodayにすれば過去の予定は残る
  overlapEvents.forEach(event => {
    if (event.getTag(key) === tag) {
      event.deleteEvent();
      Utilities.sleep(1000); // カレンダーの連続操作はエラーになりやすい
    };
  });

ここで有効なのが、先程準備しておいたタグ(キーと値のペア)です。このタグはGASからしか設定できないので、特定のタグをGASで生成した予定に付与しておけば、この特定タグが付与された予定だけをフィルタリングして削除することができます。

コピー先のカレンダーオブジェクトに対してgetEvents(startTime, endTime) メソッドで全ての予定を取得し、その中から特定のタグ(キーと値のペア)を付与された予定だけを削除しています。

この予定の削除や作成の操作を連続して行うとエラーが出ることがあるので、予定の削除毎に1秒間のスリープ処理を入れています。

Utilities.sleep(1000); // 1000ミリ秒の待機処理

今回のスクリプトは先に述べた通り、コピー先カレンダーから過去の予定を削除する処理を実装しています。このスクリプトはトリガーで1日1回実行することを想定していますので、削除対象の予定の取得開始日を前日にしています。

const overlapEvents = calMerged.getEvents(yesterday, endBefore);

もし過去の予定を削除したくない場合は、上記の該当箇所のgetEvents() メソッドの第1引数に渡しているyesterdayをtodayに変えるとよいでしょう。

キーとタグを付与しながら指定期間のイベントを別のカレンダーにコピーしていく関数

続いていよいよ予定をコピーしていきます。

コピー元のカレンダーは複数存在する想定なので、カレンダー1つづつに対して、指定期間の予定を取得してコピー先カレンダーにコピーしていく処理をする関数を切り出しました。

/**
 * タグを付与しながら指定期間のイベントを別のカレンダーにコピーしていく関数
 * 
 * @param   {Object}  calCopyFrom - 予定のコピー元カレンダーオブジェクト
 * @param   {Object}  startFrom - 予定コピーの開始日(Dateオブジェクト)
 * @param   {Object}  endBefore - 予定コピーの終了日(Dateオブジェクト)※操作対象日に含まれない
 * @param   {Object}  calCopyTo - 予定のコピー先カレンダーオブジェクト
 * @param   {string}  key - イベントオブジェクトに付与するキー(カレンダーUIでは不可視)
 * @param   {string}  tag - イベントオブジェクトに付与するキーとペアになる値(カレンダーUIでは不可視)
 * @return
 */


function copyEventsBlock_(calCopyFrom, startFrom, endBefore, calCopyTo, key, tag) {

  // コピーする予定を非公開に
  const visibility = CalendarApp.Visibility.PRIVATE;

  // カレンダー内の予定を取得
  const events = calCopyFrom.getEvents(startFrom, endBefore);

  // カレンダー内の予定を一つずつコピーしていく
  for (const event of events) {

    //終日イベントか時間単位イベントか判別
    if (event.isAllDayEvent()) {
      const title = event.getTitle(); //コピー元予定のタイトルを取得
      const startDate = event.getAllDayStartDate(); //コピー元予定の開始日を取得
      const endDate = event.getAllDayEndDate(); //コピー元予定の終了日を取得

      // 新しい非公開イベントを作成し各値とタグを付与
      // タイトルにはコピーされた予定とわかるようにCopied:の接頭詞
      const copiedAllDayEvent = calCopyTo.createAllDayEvent(`Copied:${title}`, startDate, endDate).setTag(key, tag).setVisibility(visibility);
      copiedAllDayEvent.setColor(CalendarApp.EventColor.PALE_GREEN); // イベントのカラーを指定 https://developers.google.com/apps-script/reference/calendar/event-color
      Utilities.sleep(1000);

    } else {
      const title = event.getTitle(); //コピー元予定のタイトルを取得
      const startTime = event.getStartTime(); //コピー元予定の開始時間を取得
      const endTime = event.getEndTime(); //コピー元予定の終了時間を取得

      // 新しい非公開イベントを作成し各値とタグを付与
      // タイトルにはコピーされた予定とわかるようにCopied:の接頭詞
      const copiedEvent = calCopyTo.createEvent(`Copied:${title}`, startTime, endTime).setTag(key, tag).setVisibility(visibility);
      copiedEvent.setColor(CalendarApp.EventColor.PALE_GREEN); // イベントのカラーを指定 https://developers.google.com/apps-script/reference/calendar/event-color
      Utilities.sleep(1000);
    }
  }
}

予定の公開・非公開の設定

今回は、予定を結合するコピー先カレンダーの予定の詳細は、自分自身しか確認できないようにしたかったので、全ての予定を非公開の設定でコピーしています。

  // コピーする予定を非公開に
  const visibility = CalendarApp.Visibility.PRIVATE;

予定の公開・非公開の設定はVisibilityというEnumプロパティで指定できます。このEnumプロパティでは以下の4つのプロパティが用意されています。

  • CONFIDENTIAL → 非公開(互換性のために提供)
  • DEFAULT → ユーザーのデフォルト設定と同じ
  • PRIVATE → 非公開(イベントの参加者のみ詳細確認可能)
  • PUBLIC → 公開(カレンダーの読み取り権限できるアカウントが確認可能)

Enumってなに?と思われた方は、私のGASの先生であるつじけさんの以下のブログを参考にしてください。

学習と成長のブログ|[GAS]Enumってなに?(初級編)

終日イベントか時間枠のあるイベントかを判定して条件分岐する

まず、コピー元のカレンダーから指定期間の予定を全て取得します。

  // カレンダー内の予定を取得
  const events = calCopyFrom.getEvents(startFrom, endBefore);

getEvents(startTime, endTime) メソッドはCalendarEventオブジェクトを格納した配列を戻り値として返してくれますので、この配列の要素であるカレンダーイベントオブジェクト全てをfor of文でコピー先カレンダーにコピーしていきます。

Googleカレンダーの予定には、終日の予定と時間枠の予定の2種類が存在します。そのため上記で取得したイベントオブジェクトがどちらのイベントであるのかをisAllDayEvent()メソッドで条件分岐して判定しています。

isAllDayEvent()メソッドは、その予定が終日イベントであればtrueをそうでなければflaseを返すメソッドです。

if (event.isAllDayEvent()) {
  /* 終日イベント */
} else {
  /* 時間枠イベント */
}

予定にタグ・公開範囲・予定のカラーを設定してコピーしていく

終日イベントを新たに作成するのは、createAllDayEvent(title, startDate, endDate)メソッド
時間枠イベントを新たに作成するのは、createEvent(title, startTime, endTime)メソッド

を使用します。

const title = event.getTitle(); //コピー元予定のタイトルを取得
const startDate = event.getAllDayStartDate(); //コピー元予定の開始日を取得
const endDate = event.getAllDayEndDate(); //コピー元予定の終了日を取得

// 新しい非公開イベントを作成し各値とタグを付与
// タイトルにはコピーされた予定とわかるようにCopied:の接頭詞
const copiedAllDayEvent = calCopyTo.createAllDayEvent(`Copied:${title}`, startDate, endDate).setTag(key, tag).setVisibility(visibility);
copiedAllDayEvent.setColor(CalendarApp.EventColor.PALE_GREEN); // イベントのカラーを指定
Utilities.sleep(1000);

イベントのタイトルや開始・終了日時を取得するために

といったメソッドを使用しています。

コピーした予定には、GoogleカレンダーのUI上でも確認できるようにタイトルにCopied:のような接頭詞を付与しています。

またコピーしたイベントにタグ・公開範囲・予定の表示カラーを設定するために

メソッドを使用しています。

予定の表示色もEventColorというEnumプロパティで指定できます。

const title = event.getTitle(); //コピー元予定のタイトルを取得
const startTime = event.getStartTime(); //コピー元予定の開始時間を取得
const endTime = event.getEndTime(); //コピー元予定の終了時間を取得

// 新しい非公開イベントを作成し各値とタグを付与
// タイトルにはコピーされた予定とわかるようにCopied:の接頭詞
const copiedEvent = calCopyTo.createEvent(`Copied:${title}`, startTime, endTime).setTag(key, tag).setVisibility(visibility);
copiedEvent.setColor(CalendarApp.EventColor.PALE_GREEN); // イベントのカラーを指定
Utilities.sleep(1000);

時間枠のイベントも終日イベントとほぼ同様です。異なるのは時間枠イベントに対して使用できるメソッドを用いている点です。

トリガーの設定

最後にmergeCalendarEvents関数を定期的に自動実行するためにトリガーの設定を行います。今回のスクリプトは1日1回実行される(前日までの予定を削除し予定の変更などがアップデートされる)想定で書いていますので、カレンダー操作をあまり行わない時間帯にトリガーを設定します。

おわりに

今回は、「複数のGoogleカレンダーの予定を別のカレンダーに定期的にコピーしてミーティングなどの予定調整を楽にする」をお届けしました。

プライベートと仕事のカレンダーの予定の統合や、パラレルキャリアや副業などの複数のプロジェクトや組織にまたがったカレンダーの予定を統合することで、どのカレンダーを共有しても自分の時間枠の空き具合が明確になります。

今回のスクリプトは1日1回の頻度で更新するように設計されていますが、予定の出入りが激しい方などは開始時間と終了時間の設定を工夫して、もっと短いスパンでトリガーを実行するとよいでしょう。

もし実際に活用された方がいらっしゃったらTwitterなどで感想いただけると嬉しいです。

Google Apps Scriptを勉強したい方へ

この記事を見て、GASを勉強したいなと思われた方はぜひノンプログラマーのためのスキルアップ研究会(通称 ノンプロ研)にご参加ください。私も未経験からこの学習コミュニティに参加し、講座を受講したことでGASが書けるようになりました。

学習コミュニティ「ノンプログラマーのためのスキルアップ研究会」

挫折しがちなプログラミングの学習も、コミュニティの力で継続できます。ノンプロ研でお待ちしております!

タグ: ,
Share on:
Previous Post
フィルターコーヒー
freeeAPI

GAS x freeeAPIライブラリのトリセツ「自動同期で取得した取引に自動でタグを付与しよう」その1 – 条件で指定した更新対象のfreee取引のみ取得する

Next Post
スライスされたリンゴ
GAS活用法

メルマガをCloud Text-to-Speechの5000文字/リクエストの制限にあわせて分割する