一覧に戻る
    Office 365とGoogle Workspace、2つのカレンダーで予定管理するプラクティス
    開発ラボ
    PRこの記事には広告が含まれています

    Office 365とGoogle Workspace、2つのカレンダーで予定管理するプラクティス

    16 分で読める

    複数の組織にまたがって仕事をしていると、カレンダーが分断される。片方の組織はOffice 365で、もう片方はGoogle Workspace。それぞれのカレンダーに予定が入って、会議を設定するたびに両方を確認して、空いてる時間を探す。

    地味に面倒だった。

    最初は「まあ2つ見ればいいか」と思っていたんだけど、気づいたらダブルブッキングしてたことが2回あって、さすがにこれはまずいなと。

    既存ツールを調べる

    まずググった。「Google Calendar Outlook 同期」とか。

    出てくるのは海外のSaaS。OneCal、CalendarBridge、SyncGene、あたり。どれも月5〜10ドルくらいで、機能的には「Google ⇄ Office 365の双方向同期」ができるらしい。レビューもそこそこ良い。

    試しにOneCalを使おうとした。

    でも、できなかった。Office 365側でOAuth認証の画面が出て、「管理者の承認が必要です」で止まる。

    ああ、そういうことか。組織側で外部アプリとの連携を制限してるんだ。管理者に「カレンダー同期ツール使いたいので承認してください」って申請する?いや、それはちょっと現実的じゃないな。稟議通すコストのほうが高い。

    CalendarBridgeも試したけど同じ。SyncGeneも同じ。

    OAuth制限という壁がある以上、外部ツールは使えない。

    じゃあ自作? → いったん保留

    次に考えたのは「自作するか」だった。Lovableとか使えば1ヶ月くらいで作れるんじゃないか、とか。

    でもChatGPTに聞いたら「双方向同期は地雷が多い」と言われました。

    • 無限ループ(A→B→A→B...)
    • 同一イベントの識別(GoogleのIDとOutlookのIDは別)
    • 競合解決(同時編集されたら?)
    • 削除の扱い(片方で消したら?)

    ああ、確かに。これ、ちゃんとやろうとするとOneCal作るのと同じことになるな。

    自作はやめた。

    そもそも何がしたいのか、もう一度整理しました。

    「両方のカレンダーを見ながら、ダブルブッキングせずに予定を入れたい」

    双方向の完全同期じゃなくていい。ブッキングさえ防げればいい。

    Google → Outlook(これはあっさり解決)

    まず片方向だけやってみることにした。Google側の予定をOutlookで見られるようにする。

    OutlookのWebアプリには「カレンダーの追加」→「インターネットから」みたいな機能があります。GoogleカレンダーのICS URL(購読URL)を取得して貼り付ける。

    GoogleカレンダーのICS URLは、カレンダーの設定 → 「カレンダーの統合」→ 「非公開アドレス(iCal形式)」から取れます。

    これをOutlookに追加したら、そのまま表示されました。

    5分で終わった。

    Outlook側では、Google由来の予定も含めて空き時間が計算されます。つまり、Outlook側で新しい予定を入れようとすると「この時間は埋まってる」って判定が効く。

    片方向はこれで解決。

    Outlook → Google(ここから沼)

    じゃあ逆もやろう。Outlookの予定をGoogleで見る。

    同じようにやればいい。Outlookカレンダーを「公開」して、ICS URLを取得。GoogleカレンダーでURLから追加。

    できた。Google側でもOutlookの予定が見える。

    でも、これだとダブルブッキングを防げない。

    試しにGoogle側で新しい予定を入れようとしたら、Outlook由来の予定が入ってる時間帯でも「空いてる」扱いになります。Google側の予定作成UIが、「この時間は埋まってる」って教えてくれない。

    調べたら、GoogleカレンダーのURL購読(ICS表示)は「参照専用」で、空き時間の計算には使われない仕様らしい。見えるけど、制御できない。

    困った。

    GASで「ブロック予定」を作る案

    この時点で考えたのは、「じゃあGoogle側に実体の予定を作ればいいんじゃないか」でした。

    OutlookのICSから予定情報を読み取って、Google側の専用カレンダーに「ブロック」として同期する。タイトルとか詳細は同期しなくていい。時間帯だけを「予定あり(Busy)」として埋める。

    これなら:

    • OAuth制限に引っかからない(ICSは公開URLだから)
    • ダブルブッキング制御が効く(実体予定だから空き時間計算に使われる)
    • 情報漏洩リスクが低い(タイトルを持ち出さない)

    Google Apps Script(GAS)で書けばいける。

    最初の実装:ICSをパースする

    ICSの仕様はRFC5545。BEGIN:VEVENTEND:VEVENT の間に予定情報が入ってます。必要なのは UIDDTSTARTDTEND くらい。

    最初は単純に正規表現でパースしました。

    const m = val.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/);

    YYYYMMDDTHHMMSS 形式を想定。秒まできっちり入ってる前提。

    でも、実際にOutlookから取得したICSを見たら、こんな行がありました。

    DTSTART;TZID=Tokyo Standard Time:20260109T143000

    秒がない。HHMM

    パースに失敗して、その予定がスキップされました。

    正規表現を修正。秒あり・秒なし両対応にしました。

    // YYYYMMDDTHHMM(Z)?
    let m = val.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(Z)?$/);
    if (m) { ... }
    
    // YYYYMMDDTHHMMSS(Z)?
    m = val.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/);
    if (m) { ... }

    これで一応動くようになった。

    でも次の問題が待っていました。

    繰り返し予定で予定が消える

    定例会議とか、毎週の予定が「一部だけ」しか同期されない。

    ログを見ると、created=27 とかになってるのに、実際のカレンダーには10件くらいしかない。

    なんで?

    調べたら、繰り返し予定の場合、OutlookのICSは同じ UID を複数のイベントで使い回してました。

    例えば:

    BEGIN:VEVENT
    UID:040000008200E00074C5B7101A82E008...
    DTSTART:20251223T130000
    DTEND:20251223T140000
    RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TU
    END:VEVENT

    これがマスターイベント。「隔週火曜13:00-14:00」というルール。

    で、もし特定の週だけ時間変更されてたりすると、こういうのも出てきます。

    BEGIN:VEVENT
    UID:040000008200E00074C5B7101A82E008... (同じUID)
    RECURRENCE-ID;TZID=Tokyo Standard Time:20260106T130000
    DTSTART;TZID=Tokyo Standard Time:20260109T143000
    DTEND;TZID=Tokyo Standard Time:20260109T153000
    END:VEVENT

    RECURRENCE-ID があります。これは「1/6の回は1/9 14:30に変更されてます」みたいな意味。

    でも、最初の実装では UID だけをキーにしてたから、同じUIDの予定が来るたびに上書きされて、結果として最後の1件しか残らなかった。

    キー設計を見直しました。UID + 開始時刻 を組み合わせる。

    const instanceId = e.recurrenceIdRaw || formatIcsLocalKey_(start);
    const key = `${e.uid}|${instanceId}`;

    これで各回が別イベントとして認識されるようになりました。

    予定が重複する問題

    それでも、一部の予定が「横に2つ並ぶ」ことがありました。

    同じ時間、同じタイトル「ブロック」が2つ。

    原因を探したら、Outlookが同じインスタンスを二重に出力してた。

    • マスターイベント(RRULEあり、RECURRENCE-IDなし)
    • 個別インスタンス(RECURRENCE-IDあり)

    どっちも「1/23 13:00-14:00」を指してるんだけど、ICS上では別の VEVENT として存在してる。

    なんでこんな仕様なんだ。

    正規化処理を入れました。同じ開始時刻の予定が複数ある場合、RECURRENCE-ID を持つ方(例外インスタンス)を優先して、マスターは無視する。

    function normalizeEvents_(events) {
      const map = new Map();
      for (const e of events) {
        const k = `${e.uid}|${formatIcsLocalKey_(e.dtstart)}`;
        if (!map.has(k)) {
          map.set(k, e);
          continue;
        }
        const existing = map.get(k);
        // RECURRENCE-IDがある方を優先
        if (!existing.recurrenceIdRaw && e.recurrenceIdRaw) {
          map.set(k, e);
        }
      }
      return Array.from(map.values());
    }

    これでようやく重複が消えました。

    でもここまで来て、なんかもう疲れてきた。

    仕様を割り切る

    ここで一度立ち止まりました。

    そもそも何がしたかったのか。「ダブルブッキングを防ぐこと」だ。

    Outlookの予定を完全にGoogleに再現する必要はない。タイトルも、詳細も、参加者も、要らない。時間帯が「埋まってる」ことさえ分かればいい。

    じゃあ、同じ時間帯に複数の予定があっても、1つのブロックでいいじゃないか。

    仕様を割り切りました。

    • 開始・終了時刻が同じ予定は、1つのブロックに集約
    • タイトルは固定で「ブロック」
    • 詳細は空(情報漏洩防止)
    • ブロック用カレンダーは「捨ててもいい」前提

    この割り切りで、実装がシンプルになった。

    キーも 開始時刻 + 終了時刻 だけでいい。

    const eventKey = `${startTime.getTime()}_${endTime.getTime()}`;

    同じ時間の予定は同じキーになります。上書きされる。でもそれでいい。

    最終的な構成

    最終的に落ち着いた形:

    ソース: GoogleカレンダーにURL購読で取り込んだOffice 365カレンダー

    ターゲット: 専用の「ブロック用」Googleカレンダー

    同期頻度: 4時間おき(そんなに頻繁にカレンダーは更新されない)

    同期方式: 差分同期

    差分同期にしたのは、効率を考えて。毎回全削除+再作成だと、イベント数が増えたときにAPI呼び出しが膨らみます。今は100件程度だけど、将来を考えて差分にしました。

    差分同期の場合、マッピング(ソースのイベントキー → ターゲットのイベントID)を保持する必要があります。Script Propertiesに JSON で保存。

    const map = JSON.parse(props.getProperty(PROP_MAP) || '{}');

    同期ロジックは:

    1. ソースカレンダーから予定取得
    2. 各予定のキー(開始+終了)を計算
    3. マップにあれば更新、なければ新規作成
    4. 今回見つからなかったキーは削除

    コード量は200行くらい。

    実際に使ってみて

    今日から運用開始。

    まだ数時間だけど、今のところ問題ない。Google側で予定を入れようとすると、Outlookの予定が入ってる時間帯は「埋まってる」って表示されます。

    4時間以内には同期される。リアルタイムじゃないけど、まあ許容範囲。

    ブロック用カレンダーは、左のカレンダー一覧で色を濃いグレーにしてあります。一目で「これは触っちゃダメなやつ」って分かる。

    トリガー頻度は今のところ4時間でちょうどいい感じ。将来的に変えるかもしれないけど、当面はこれで。

    振り返り:非対称性とトレードオフ

    Google → Outlookは5分で終わった。ICS URLを追加するだけ。

    Outlook → Googleは、何日もかかった。ICSパース、繰り返し予定、重複排除、キー設計、差分同期。

    技術的に非対称だった理由:

    • OutlookのICS出力仕様(秒なし、RECURRENCE-ID、マスター+例外の二重表現)
    • GoogleのICS購読仕様(空き時間計算に使われない)
    • OAuth制限という組織的制約

    最初は「完璧に同期したい」と思ってました。でも途中で「ブロックさえ分かればいい」に変わった。

    トレードオフもあります。

    • タイトルが見えない(でもそれでいい、情報漏洩防止)
    • リアルタイムじゃない(でも4時間なら許容範囲)
    • 同じ時間の予定が複数あっても1ブロック(でも目的は達成できる)

    完璧な解は、たぶん存在しない。制約を受け入れて、必要十分を見極めて、実務で動く形に落とし込む。

    「ブロックさえ分かればいい」——その割り切りが、最後の判断でした。

    関連リソース

    この記事は役に立ちましたか?

    Coffee cup

    この記事が、何かの整理につながったら

    コーヒー1杯分の応援をもらえると嬉しいです。

    ※ これは応援とは別の話ですが、

    同じようなテーマを自分の文脈で整理したい場合は、 (文章だけだと詰まりやすい人向けに) 思考整理の壁打ちという形で対話の時間も取っています。

    対話の時間について