[Google Apps Script] カレンダーファイルを生成する

バイトのシフトなど、予定を手入力でカレンダーアプリに登録していませんか？
私のバイト先ではシフトがexelの表で公開されるので、毎回手入力でカレンダーに登録していました。そして登録漏れによってバイトを欠席してしまった事も...

「Exelでデータがあるなら、カレンダーファイルを送れ！！」


ということでスマホやパソコンのカレンダーアプリに予定を追加できるカレンダーファイル(.ics)についての備忘録です。


## できること
カレンダーファイルを生成し、ダウンロードURLを生成する。
LINEやメールに添付することでシフト管理などが容易になる。
## 実行環境
Google Apps Script
JavaScript
## カレンダーファイルとは
テキストファイル(.txt)の拡張子を.icsに変えただけです。
ファイルの中身は以下のtxtファイルのように、
　　`BEGIN`：タグ
　　　　　〜
　　`END`:タグ
構造をとっています。
```txt:calender.txt
BEGIN:VCALENDAR

PRODID:-//sample//sample//JP
CALSCALE:GREGORIAN
VERSION:2.0
BEGIN:VTIMEZONE
TZID;X-RICAL-TZSOURCE=TZINFO:Asia/Tokyo
BEGIN:STANDARD
DTSTART:19510908T020000
RDATE:19510908T020000
TZOFFSETFROM:+1000
TZOFFSETTO:+0900
TZNAME:JST
END:STANDARD
END:VTIMEZONE

BEGIN:VEVENT
DTSTART;TZID=Asia/Tokyo;VALUE=DATE-TIME:20221020T163000
DTEND;TZID=Asia/Tokyo;VALUE=DATE-TIME:20221020T180000
ATTENDEE:
DESCRIPTION:英語4 オンライン
SUMMARY:英語オンライン
LOCATION:
END:VEVENT

END:VCALENDAR

```
このtxtファイルで注目していただきたい点は、
　1. **txtファイル全体は`VCALENDER`タグで囲まれている**
　2. **`VTIMEZONE`タグで囲われている部分は編集の必要がない。おまじない**
　3. **`VEVENT`の中身が本体であり、`VEVENT`は複数個作って良い**
これさえ知ってしまえば、あとは`VEVENT`を量産してtxtファイルに追記していくだけです。


以下の記事にics形式の詳細な説明があるので、是非参考にしてください。

https://qiita.com/S_Kosaka/items/bcc1f18ab6aab20bd807

## GASでtxtファイルを作る
GASでは、とある関数に（文字列、ファイル形式、ファイル名）を送るだけでtxtファイルを作成できます。

以下のサンプルコードではtxtファイルを"text/calendar"形式で生成しています。
また、ファイルを生成しただけではプログラム終了時に消えてしまうので、`folderID`で指定したGoogle Driveに保存します。

```JavaScript:makeics.gs
const folderID = "sample-id-XXXXXXXXX";

function make_ics(data,filename){
  const blob = Utilities.newBlob(data, "text/calendar", file_name);  //ファイルを生成
  var folder = DriveApp.getFolderById(folderID);  //IDで指定したフォルダを開く
  folder.createFile(blob);  //引数file_nameで指定したファイル名でicsファイルを作成
}
```
上の関数の
　`data`に、前述のtxtファイルのような文字列
　`filename`に、"sample.ics"のような拡張子icsの文字列
を送れば、無事カレンダーファイルが生成されるというわけです。
フォルダIDがわからない場合は、こちらの記事[【GAS】GoogleドライブのフォルダIDの取得方法](https://tetsuooo.net/gas/748/)を参考にしてください。

## ダウンロードURLの取得
ダウンロードURLの取得は2ステップでできます。
#### 1. カレンダーファイルのファイルIDを取得する
```JavaScript
  var fileId = DriveApp.getFolderById(folderID).getFilesByName(file_name).next()
```
#### 2. URLを発行する
```JavaScript
  var url = fileId.getDownloadUrl();  //ダウンロード用のURLを生成
```
実際に発行したURLがこちらになります。[sample.ics](	https://drive.google.com/uc?openExternalBrowser=1&id=1QJmPvJNY-77QxpYhfUUb4fMTmr0fdeER&export=download)


:::note warn
注意
カレンダーファイルのダウンロードは問題ありませんが、実際にカレンダーへ追加すると削除は手作業なので、下の画像の画面を見たら「完了」を押して終了しましょう。
:::
<img width = '200' src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989725/db89743c-32f5-cbfe-f305-64aa8580a2da.jpeg">

このように2022/10/6、2022/10/7に予定が入ったでしょうか？
### ここまででカレンダーファイルの生成は完了です！

## 実装
では実装を見ていきましょう。
カレンダーデータは以下のような2次元配列で用意します。
```JavaScript
function debug() {
  const data = [
    [2022, 10, 6, 17, 0, 21, 0, "sample", "サンプル"],
    [2022, 10, 7, 17, 0, 21, 0, "sample", "サンプル"]
  ]  //[yyyy, mm, dd, begin_hour, begin_min, end_hour, end_min,title,subtitle]
  console.log(make_ics("sample", data));  //make_ics(ファイル名、予定データ)
}
```
次にicsファイルに格納する文字列を用意します。
```JavaScript
function make_ics(filename, data) {
  var contents = "";
  for (var i in data) {
    var yy = String(data[i][0]);
    var mm = String(data[i][1]);
    var dd = String(data[i][2]);
    var begin_h = String(data[i][3]);
    var begin_m = String(data[i][4]);
    var end_h = String(data[i][5]);
    var end_m = String(data[i][6]);
    const title = String(data[i][7]);
    const subtitle = String(data[i][8]);
    if (yy.length < 2) yy = "0" + yy;  //yyyy/mm/ddの形式にするため
    if (mm.length < 2) mm = "0" + mm;  //yyyy/mm/ddの形式にするため
    if (dd.length < 2) dd = "0" + dd;  //yyyy/mm/ddの形式にするため
    if (begin_h.length < 2) begin_h = "0" + begin_h;  //yyyy/mm/ddの形式にするため
    if (begin_m.length < 2) begin_m = "0" + begin_m;  //yyyy/mm/ddの形式にするため
    if (end_h.length < 2) end_h = "0" + end_h;  //yyyy/mm/ddの形式にするため
    if (end_m.length < 2) end_m = "0" + end_m;  //yyyy/mm/ddの形式にするため
    contents += `\nBEGIN:VEVENT\nDTSTART;TZID=Asia/Tokyo;VALUE=DATE-TIME:${yy}${mm}${dd}T${begin_h}${begin_m}00\nDTEND;TZID=Asia/Tokyo;VALUE=DATE-TIME:${yy}${mm}${dd}T${end_h}${end_m}00\nATTENDEE:\nDESCRIPTION:${subtitle}\nSUMMARY:${title}\nLOCATION:\nEND:VEVENT\n`
  }
  var body_text = `BEGIN:VCALENDAR\nPRODID:-//${productID}//${developer_name}//JP\nCALSCALE:GREGORIAN\nVERSION:2.0\nBEGIN:VTIMEZONE\nTZID;X-RICAL-TZSOURCE=TZINFO:Asia/Tokyo\nBEGIN:STANDARD\nDTSTART:19510908T020000\nRDATE:19510908T020000\nTZOFFSETFROM:+1000\nTZOFFSETTO:+0900\nTZNAME:JST\nEND:STANDARD\nEND:VTIMEZONE\n${contents}\nEND:VCALENDAR`
  console.log(body_text)
  return make_url_ics(`${filename}.ics`, body_text);
}
```
・for文の中では、`contents`に2次元配列のデータを元に作られた文字列が追加されていきます。
・for文が終わると、`body_contents`に（「おまじない」と紹介したヘッダー文、for文で作った`contents`）が格納されます。
・最後に`make_url_ics`関数でファイル生成・リンク取得を行えば完了です。
## ちょっとした工夫
**1. Google Drive内の保存先に同じ名前のファイルを増殖させない
2. iPhoneやAndroid版のLINEアプリでリンクを踏んでも正常に動作させる**

以上の二つの目標を達成していきましょう。
**1つ目の目標は以下のコードで実現できます**
```JavaScript
  var isreleased = folder.getFilesByName(file_name).hasNext(); //既に同じファイル名が存在する場合はtrue
  if (isreleased) folder.getFilesByName(file_name).next().setTrashed(true);  //isreleased==trueの時、現在あるファイルを削除
```
フォルダ内をファイル名で検索→ファイル名で対象を削除
とてもシンプルですね！

**二つ目の目標はURLにパラメータを追加することで実現できます。**
iPhoneやAndroidではカレンダーアプリへの追加がデフォルトブラウザ経由でしか行えません。試しにこちらのリンクをLINEやGoogle Chromeなどで開いてみてください。
https://drive.google.com/uc?id=1QJmPvJNY-77QxpYhfUUb4fMTmr0fdeER&export=download
下の画像のように、白い画面でフリーズしてしまいます。
<img width = '100' src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989725/566ef18b-f9e9-e872-2b0c-c2329fd08614.png">
そのため、URLのパラメータに`openExternalBrowser=1`を挿入します。
URLのパラメータとは
　**1. URL中の"?"の後ろの部分
　2. 複数パラメータを追加する場合は"&"で繋ぐ
　3. パラメータは"キー:値"の形で表記する**
というものであり、上記のURLでは`id`と`export`をキーとするパラメータが既に存在します。
ここで、`openExternalBrowser=1`をパラメータを先頭につけるとURLは
https://drive.google.com/uc?openExternalBrowser=1&id=1QJmPvJNY-77QxpYhfUUb4fMTmr0fdeER&export=download
となり、無事にデフォルトブラウザが開かれたと思います。
実際のコードは以下のようになります。
```JavaScript
function defaultbrowser_url(url) {
  //LINEの場合アプリ内ブラウザで開かれるので、強制的にデフォルトブラウザで開くパラメータをURLに付与
  //iPhoneやAndroidの場合、アプリ内ブラウザからカレンダーアプリにアクセスすることができない。
  const quest_pointer = url.indexOf("?")
  const first_url = url.slice(0, quest_pointer)
  const last_url = url.slice(quest_pointer + 1)
  url = first_url + "?openExternalBrowser=1&" + last_url;
  return url
}
```
## 最後に
ソースコードは[こちら](https://github.com/kento2247/gas_examples/blob/main/make_ics)に載せてあります。

https://github.com/kento2247/gas_examples/blob/main/make_ics

GASとLINE botの連携をすると、以下の画像のようなメッセージも送れて大変便利です。
<img width = '200' src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989725/c08e9d8d-5f1e-20ac-b40d-8235b700d01c.png">
バイトのシフト管理をしている方、積極的な導入をお願いします！

初めてのQiita投稿ですが、役立つ情報を発信できればと思います。