Google Calendar上で操作できる予約ソフトを作る [GoogleAppsScript]

# はじめに
私の研究室では多くの高価な装置を多人数で共有して使っています。これらの装置に関して予約追加、他人の予約の確認が可能なソフトをGoogle Apps Script（以下GASと呼ぶ）で勉強がてら作ってみました。
初投稿なので改善するべき点などのフィードバック歓迎します。

## TL;DR
このソフト作成で学んだこと

- GASのinstallable scriptとcontainer-bound scriptの違い
    - container-bound scriptはboundしているサービス以外のAPIを用いることができないので、一つのサービスのみを用いるソフトに向いている。
- GASには単位時間あたりの利用回数制限(quota)がある。
- GASにおいて一つのスクリプトで設定できるtriggerは20個までである。
    - 複数のスクリプトで分散する（2個のスクリプトなら40個のtriggerが利用可能）前提で実装するか、20以内に収める努力をすることが必要である。
- GASのCalendarAppにおいてはSync tokenという判別子を用いることで、前回の実行後に変更された予定だけに処理を行うことができ、時間の節約が可能となる。
- 他人の操作に合わせた予定の追加、変更、削除の3つの機能を最も単純に実装する方法は、予定にゲストとして追加する方法である
    - これにより、All day event、詳細、開催場所などの情報をスクリプトで処理する必要がなくなった。

この記事においては詳細の多くを省いているので、詳しく知りたい人は以下のGitHubリポジトリを参照してください。
[GitHub repo [equipment-reservation]](https://github.com/rayanti/equipment-reservation)

# 使用方法
- 装置の使用状況（予定の形で保存）をスマートフォンやPCのGoogleカレンダーアプリで確認
- アプリで空いている時間帯に予定を追加する形で装置の予約を行う
- 予約には名前が書いてあり、予定の交渉はSlackや対面で行うことを想定
    - 連絡の機能はこのソフト内にはない
    
# ソフトウェアの仕様
仕様に関してはかなり細かく突き詰めた後にプログラムの作成を行った。
## 概要
- [Google Apps Script](https://developers.google.com/apps-script/)というサービスを用いる。これはGoogleのサービスをスクリプトで操作できるサービスで、今回はSheets（表計算）とCalendar（カレンダー）を用いる。 
- Sheetsはデータベースとして動作し、ユーザーの情報と設定内容を保存している。CalendarはUIとして動作し、予約の追加、変更、確認を行う。
- カレンダーにはユーザー側で操作するカレンダー（予約追加および変更用）とサーバー側で操作するカレンダー（予約確認用）が存在する。
    - 前者のカレンダーが操作されたとき、GASが後者のカレンダーに変更を反映する。
    - 以降、前者をWriteカレンダーと呼び、後者をReadカレンダーと呼ぶ。

## Writeカレンダー（予約追加および変更用、ユーザー側で操作する）
予定を作成することで、使用する装置、装置の状態、使用時間を指定する。

- ユーザー毎にカレンダーが用意されている。
- カレンダー名は`Write [User Name]`とする。ユーザー名がTana.Y1の場合はカレンダー名は`Write Tana.Y1`である。
- 予定の表記は`[device(小文字)] [state]`とする。PVDという装置を冷却する場合は`pvd cool`とする。

## Readカレンダー（予約確認用、サーバー側で操作する）
他のユーザーの装置の使用状況を予定としてGoogleカレンダー上に表示する。

## ユーザー側の操作方法
情報に疎い人でも問題なく使用できる簡単な操作方法を目標としている。
### ユーザーの追加(初期設定)
1. カレンダーを閲覧するGoogleアカウントでGoogle Groupsに加入。
    1. これによりカレンダーの編集権限を得ることができる。
1. Sheets上で使用する装置にチェックを入れる（一枚目の画像の➂に対応）。
1. ユーザーのカレンダーを各自のカレンダーアプリに登録する。
    1. Read(読み取り専用)とWrite(書き込み専用)の二つのカレンダーのリンクが用意されている(一枚目の画像の➁では追加__できない__)。
    1. URL of Calendarというシートを選択し、二枚目の画像のようにリンクをクリックするとGoogle Calendarが立ち上がり、カレンダーを追加できる。
   2. マウスカーソルをリンク上に移動してカレンダー上に追加する。
      ![sheets1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299854/e4762f9b-5f14-548b-90d1-d1e812f2e520.png)
      ![sheets2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299854/c24ac7c7-738e-c351-fbb9-437a3f22b164.png)

### 予定の追加
1. Calendar上で予定を追加する。
   1. 予定のタイトルには`[装置名] [状態]`を記入する（画像の➀に対応）。
      1. 装置名はSheetsの一行目で設定されており、状態は任意の値の入力が可能である。
   2. 追加するカレンダーは`Write [User Name]`を選択する（画像の➁に対応）。
      ![calendar1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299854/a5d324e5-81f0-08b9-90fd-68420932d7ec.png)

## プログラムの動作
### 概要
1. ユーザーがカレンダー上で予約を追加
2. GASがカレンダーの予定の変化を検知
    2. Calendarの予定編集時のトリガーを用いる。
    3. 変化した内容をみて、その装置を使用するユーザーの読み取り専用カレンダーに予定を表示する。
       3. 予定が追加された場合は読み取り専用カレンダーに予定を追加する。
       3. 予定が変更された場合は変更前の予定は消え、変更後の予定が読み取り専用カレンダーに表示されるようにする。
       3. 予定が削除された場合は読み取り専用カレンダーから予定を削除する。

### 動作の実現方法と大事な点
- Standalone scriptを用いている。トリガーはinstallable triggerを用い、Sheets上での変更を検知する。
    - __Container-bound scriptを用いなかったのは、Sheetsにboundした場合はCalendarのAPIを呼び出す権限がなく__カレンダー名を変更できないため、今回の目的を達成できないためである。
- カレンダーは事前に作成する必要がある。__スクリプト一つにつき設定できるトリガーは20個__であるため、ユーザー19人分の読み取り専用、書き込み専用カレンダーを作成し、書き込み専用カレンダーにトリガーを設定した。残りの1個のトリガーはsheetsに設定した。人が入れ替わる場合はカレンダーを再利用することを想定している。
- 全員の予定を確認することのできるカレンダーはALL.U1というユーザー名で作成した。
- このスクリプトは次に示すようないくつかの役割を担っている。
    1. ユーザーの追加、変更
    2. ユーザーの追加、変更によるカレンダー名の変更
    3. ユーザーの追加、変更による予定内のユーザー名の変更
    4. ユーザーによる予定の追加、変更、削除
    5. ユーザーが読み取り専用カレンダーで閲覧する装置の変更

#### 1. ユーザーの追加、変更
- Sheets上に名前を追加、変更する。名前の表記はローマ字表記で行い名、姓の順で表記し間にスペースをはさむ。
- Sheets上での変更をトリガーが検知する。
- 名前を名と姓に分解して、`[姓の最初の四文字].[名の最初の文字]`をUser Name 1として求める。
- すべてのユーザーのUser Name 1を見て上の行から順に重複防止用の番号をつけていき、User Name 2とする。これを以降ユーザー名(User Name)として採用する。User Name 2の表記は`[姓の最初の四文字].[名の最初の文字][1,2,3,...]`のようになる。

#### 2. ユーザーの追加、変更によるカレンダー名の変更
- ユーザーの追加、変更が行われた後にカレンダー名の変更が行われる。
- Sheetsの行ごとにカレンダーが二つ用意されている。これは読み取り専用と書き込み専用カレンダーに対応している。それぞれのカレンダーの名前をそれぞれ`Read [User Name]`と`Write [User Name]`に変更する。


#### 3. ユーザーの追加、変更による予定内のユーザー名の変更
- ユーザーの追加、変更が行われた後にカレンダー名の変更が行われ、そのあとに予定内のユーザー名の変更が行われる。


#### 4. ユーザーによる予定の追加、変更、削除
- __Sync tokenという判別子を用いることで、変更された予定だけに処理を行う__。これにより時間の節約が可能となる。
- __追加、変更、削除の3つの機能を最も単純に実装する予定の追加方法として読み取り専用カレンダーをゲストとして予定に追加する方法を用いた__。これにより、ゲストに追加された読み取り専用カレンダーから予定を見ることができる。
  - 予定追加の場合は読み取り専用のカレンダーを予定のゲストとして追加する形で予定を追加する。
  - 予定変更の場合は、読み取り専用のカレンダーはすでに予定のゲストとして追加されてているため、もう一度ゲストとして追加しても変化はない。また、変更は自動的に読み取り専用カレンダーに適用される。
  - 予定削除の場合は、読み取り専用のカレンダーがゲストとして参加している元の予定が削除されるとその予定は読み取り専用のカレンダーから削除される。
 
#### 5. ユーザーが読み取り専用カレンダーで閲覧する装置の変更
- 閲覧する装置を変更した場合は、過去10日間にさかのぼり、閲覧するとして設定した装置の予定を追加する。前に閲覧していたが、今回閲覧しないと設定した装置に関しては予定を削除する。予定の追加、削除に関しては前と同様にゲストとして読み取り専用カレンダーに参加させることで追加する。

#### installable triggerのquotaの確認
__Google Apps Scriptには以下のquota(利用回数制限)が設定されている__。19人のユーザーがいたとしても十分な実行回数が確保できると考える。

- カレンダーの予約の作成：一日当たり5,000回実行可能
- triggerの実行時間：一日当たり90分間実行可能
    - 1回の実行時間を15秒とすると一日当たり360回実行可能
- Quotaには書いていないが、連続での予定の作成の間隔が短すぎると、予定の作成を拒否される。したがって、予定の作成の間隔を1sと設定した。

## 付録
### ユーザー名の表記
- ユーザー名は重複がないように以下のような形式になっている。数字は、姓名に重複のない場合は1であり、重複があった場合は後に作ったユーザーの数字を一つずつ増加させる。
  - `[姓の最初の4字(最初の文字は大文字)].[名の最初の一文字(大文字)][1,2,3,...]`

### ソフトウェアで用いる省略名称
スマートフォンのカレンダーアプリで表示することのできる文字数は制限されているため、装置名、状態、ユーザー名を省略表記で表示する。なお、装置の状態名は基本的に入力の簡単化のため、小文字になっている。装置名、ユーザー名はユーザーが入力することは基本的にはないため、大文字を用いている。
- Devices (__Sheets上で装置名の設定が可能__)
 
| Device | Description |
| ---- | ---- |
| cmp  | Chemical mechanical planarization |
| rie  | Reactive ion etching |
| cvd  | Chemical vapor deposition |
| pvd  | Physical vapor deposition |
| euv  | Extreme ultraviolet lithography |
| diff | Diffusion |
| cleg | Cleaning |
| dicg | Dicing |
| Pack | Packaging |
- States of equiment（__これに限らず任意の値を用いることができる__）

| State | 説明 |
| ---- | ---- |
| evac | evacuation |
| use/[NO ENTRY] | Main operation |
| cool | cooling |
- Users (__Sheets上でユーザーの追加可能__)

| User Name | 名前 |
| ---- | ---- |
| Tana.Y1 | Tanaka Yuusuke |
| Tana.Y2 | Tanabe Yuta |
| Tana.S1 | Tanahashi Shion |
| Chen.F1 | Chen Fan |
| Zhu.Y1  | Zhu Yu |