この記事は KLab Advent Calendar 2018 16日目の記事です。
はじめに
最近、業務でミーティングを開催する側になることが増えたのですが、日中は会議室が空いていないことが多く、特に参加メンバーが多い場合に予定のすり合わせで苦労することが増えました。
KLabでは会議室や備品などもGoogle Calendarのリソースとして登録されており、ブラウザ上から予約を行うのですが、
明日までに同僚10人が参加する1時間のミーティングを組まないといけない!
といった状況で、会議室が空いている時間と同僚10人のカレンダーをにらめっこするのはかなり骨が折れます。
そこで、予定のすり合わせをもっと簡単に行えないかと思い、最近社内でツール作成によく使われるGoの勉強がてら、Google Calendar APIをいじってみました。
GoでGoogle Calendar APIを叩く
Go Quickstart を見つつサンプル通りにやっていくだけで簡単に自分のカレンダーを取得できました。
(途中、Goのバージョンが古いと怒られたので1.9
から1.11.2
にあげました1)
取得した予定を出力するとこのようになります。(伏せ字で見づらくてすみません)
リソース(会議室)の予定を取得する
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
リソース(会議室)の予定を取得するには、会議室のcalendarId
が必要です。
カレンダーIDはブラウザ上のGoogle Calendarから取得することができます。
まずは 同僚のカレンダーを追加
から必要なリソースを追加します。
追加したカレンダーを選択し、[設定]->[カレンダーの統合]
と進むと
カレンダーIDが記載してあります。
これで会議室の予定も取得できるようになりました。
予定の表現の仕方
自分と会議室の予定が取得できたら、それらから「双方の予定が空いている時間」を探します。
今回、上手いやり方が思いつかなかったので、ビット演算でやることにしました。
会議室の予約のほとんどは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
を元にビットを立てていきます。
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
// 自分の予定
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
となる位置(=どちらも予定がない時刻)を探します。
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時間のミーティングを組まないといけない!
といった状況でも、簡単に空いている時間を探すことができます。
また、今回は時間がありませんでしたが、予定の取得だけでなく登録もできるので、提示された選択肢を選ぶだけでカレンダー登録まで行えるようにしたいです。
ゆくゆくはSlackbotにすることで、誰もが簡単に(ブラウザでGoogle Calendarとにらめっこすることなく)ミーティングを設定できるようになればと思います。