GoでGoogle Calendar APIをいじって会議室を探す話

この記事は [KLab Advent Calendar 2018](https://qiita.com/advent-calendar/2018/klab) 16日目の記事です。


# はじめに
最近、業務でミーティングを開催する側になることが増えたのですが、日中は会議室が空いていないことが多く、特に参加メンバーが多い場合に予定のすり合わせで苦労することが増えました。

KLabでは会議室や備品などもGoogle Calendarのリソースとして登録されており、ブラウザ上から予約を行うのですが、
`明日までに同僚10人が参加する1時間のミーティングを組まないといけない！`
といった状況で、会議室が空いている時間と同僚10人のカレンダーをにらめっこするのはかなり骨が折れます。

そこで、予定のすり合わせをもっと簡単に行えないかと思い、最近社内でツール作成によく使われるGoの勉強がてら、Google Calendar APIをいじってみました。


## GoでGoogle Calendar APIを叩く
[Go Quickstart](https://developers.google.com/calendar/quickstart/go) を見つつサンプル通りにやっていくだけで簡単に自分のカレンダーを取得できました。
（途中、Goのバージョンが古いと怒られたので`1.9`から`1.11.2`にあげました[^1]）

取得した予定を出力するとこのようになります。（伏せ字で見づらくてすみません）
![goQuickstart.png](https://qiita-image-store.s3.amazonaws.com/0/48175/842bb3ed-5313-fb72-9741-80069e94a4ca.png)


## リソース（会議室）の予定を取得する

```go:calendar-gen.go
func (r *EventsService) List(calendarId string) *EventsListCall {
	c := &EventsListCall{s: r.s, urlParams_: make(gensupport.URLParams)}
	c.calendarId = calendarId
	return c
}
```
[google-api-go-client](https://github.com/googleapis/google-api-go-client/blob/master/calendar/v3/calendar-gen.go#L6702)
リソース（会議室）の予定を取得するには、会議室の`calendarId`が必要です。
カレンダーIDはブラウザ上のGoogle Calendarから取得することができます。
まずは `同僚のカレンダーを追加` から必要なリソースを追加します。
![colleague.png](https://qiita-image-store.s3.amazonaws.com/0/48175/0c2ca30e-606d-6955-a977-e718121486e0.png)

追加したカレンダーを選択し、`[設定]->[カレンダーの統合]` と進むと
カレンダーIDが記載してあります。
![calendarid.png](https://qiita-image-store.s3.amazonaws.com/0/48175/650e34ae-995e-729c-2014-2b35c84fd5d0.png)
これで会議室の予定も取得できるようになりました。

## 予定の表現の仕方

自分と会議室の予定が取得できたら、それらから「双方の予定が空いている時間」を探します。
今回、上手いやり方が思いつかなかったので、ビット演算でやることにしました。

会議室の予約のほとんどは30分単位で行われているので、1日を30分ごとのブロックに分割し、`予定がある = 1` `予定がない = 0`というビットで表現しました。

つまり、右から1ビット目が「0:00〜0:30」の予定を、2ビット目が「0:30〜1:00」の予定を、21ビット目が「10:00〜10:30」の予定を表しているという状態です。
1日は24時間なので、30分毎に分割しても48ブロックであり、64bitあれば余裕で表現することができます


例えば、12月16日の
- 午前1時〜2時
- 午前10時〜10時半
- 午後15時〜16時半
- 午後20時〜24時
に予定が入っていた場合、

```
00 00 00 00 00 00 00 00              // 使用しない左16bit
11 11 11 11 00 00 00 01 11 00 00 00  // 左端は23:30、右端は12:00を表す
00 01 00 00 00 00 00 00 00 00 11 00  // 左端は11:30、右端は0:00を表す

つまり12月16日の予定を64ビット表記にすると
0000000000000000111111110000000111000000000100000000000000001100
```
と表すことができます。

## 自分と会議室の予定を比較

まず、Calendar APIから取得できる日付はただの文字列なので、扱いやすいように`time.Time`型にパースします

```
format := "2006-01-02T15:04:05-07:00"
startTime, err := time.Parse(format, item.Start.Datetime)
```

次に、日付とその日の予定を表すビット配列でmapを作り、
取得した予定の`startTime` `endTime`を元にビットを立てていきます。

```go:calendar.go
m := map[string]uint64{}
for _, item := range events.Items {
    /*
      時刻のパースなど、省略
    */
    
    // 予定の開始時刻のビット位置を計算
    startTimeBit := uint64(startTime.Hour() * 2)
    if startTime.Minute() >= 30 {
        startTimeBit++
    }
    // 予定の終了時刻のビット位置を計算
    endTimeBit := uint64(endTime.Hour() * 2)
    if endTime.Minute() == 0 {
        endTimeBit--
    } else if endTime.Minute() > 30 {
        endTimeBit++
    }
    // 予定の時刻にビットを立てる
    date := item.Start.Date()  // 例： "2018-12-17"
    for i := startTimeBit; i <= endTimeBit; i++ {
        m[date] |= 1 << i
    }
```




実際に取得できた自分および会議室の予定のmapはこのようになります。[^2]

```go:sample
// 自分の予定
mySchedules := map[string]uint64{
    "2018-12-17": 0000000000000000111111110011100000100110110011111111111111111111,
    "2018-12-18": 0000000000000000111111111111101111111111110011111111111111111111,
}
// 会議室の予定
mtgRoomSchedules := map[string]uint64{
    "2018-12-17": 0000000000000000000000000011111111111111000000000000000000000000,
    "2018-12-18": 0000000000000000000000110101111111111011110000000000000000000000,
}
```




最後に、自分と会議室の予定を比較して両方が空いている時間を探します。
ビットが立っている位置が予定がある時刻なので、論理和を取って`0`となる位置（=どちらも予定がない時刻）を探します。

```go:calendar.go
for key, _ := range mySchedules {
    // 論理和をとって自分と会議室両方が空いている時間を算出
    schedule := mySchedules[key] | mtgRoomSchedules
    // bitを時刻にデコード
    fmt.Printf("-------%v-------\n", key)
    for i := uint(0); i < 48; i++ {
        if schedule&(1<<i) != 1<<i {
            hour := i / 2
            minute := i % 2 * 30
            fmt.Printf("%v時 %v分 is OK!\n", hour, minute)
        }
    }
}
```

出力した結果がこちらです。

```
-------2018-12-17-------
19時 0分 is OK!
19時 30分 is OK!
-------2018-12-18-------
10時 0分 is OK!
10時 30分 is OK!
```
`12月17日 19時〜20時` `12月18日 10時〜11時` であれば
自分、会議室ともに空いていることがわかりました。


# 今後の展望など

今回は自分と会議室1つだけの予定を比較しましたが、他の会議室すべてと比較したり、同僚のカレンダーIDから予定を取得することで、冒頭の
`明日までに同僚10人が参加する1時間のミーティングを組まないといけない！`
といった状況でも、簡単に空いている時間を探すことができます。

また、今回は時間がありませんでしたが、予定の取得だけでなく[登録](https://developers.google.com/calendar/v3/reference/events/insert)もできるので、提示された選択肢を選ぶだけでカレンダー登録まで行えるようにしたいです。

ゆくゆくはSlackbotにすることで、誰もが簡単に（ブラウザでGoogle Calendarとにらめっこすることなく）ミーティングを設定できるようになればと思います。

[^1]: 1年に1回程しかgoを触っていないのがバレます。
[^2]: 早朝や深夜に会議室を使うことはまず無いので、最初からこの時間のビットは省けば32bitでも十分足りることに途中で気付きました。