【0からGASを学ぶ】GASを用いてGoogleカレンダー+メール送信で翌日の予定を通知しよう

# はじめに
本シリーズでは、GASの始め方や便利な使い方、ビジネス活用まで幅広く解説します。シリーズをひと通り読んでいただければ、あなたもきっとGASマスターになれるはずです。

# シリーズの対象者
* そもそもGASってなんだか**わからない**方
* GASを学びたいけど**何から始めればいいか**わからない方
* GASはわかり始めたけど、**もっと活用ができないか**と模索している方
* とにかく**Googleが好き！** という方

# 前回記事
https://qiita.com/gas-suke/items/a1783b68fac098c8c3ae

# スプレッドシートで遊ぶ
では早速始めていきましょう。【0からGASを学ぶ】シリーズの第8回は「GASを用いてGoogleカレンダー+メール送信で翌日の予定を通知しよう」です。前回に引き続きPDFエクスポートを使った操作をやってみようと思います。これまで、`Googleカレンダー`や`Gmail`をGASから操作しました。また<a href="https://qiita.com/gas-suke/items/928fdd8d01fafc4aed1b" target="_blank">第5回</a>では、スプレッドシートのデータを評価し、メールを自動送信しました。これらを組み合わせてみましょう。

## 今回やること
1. Googleカレンダーの翌日のイベントをすべての取得する。
1. イベントをスプレッドシートに一覧でまとめる。
1. スプレッドシートをPDF変換する。
1. 変換したPDF（翌日の予定一覧）を添付ファイルとして自分自身にメールを送信する。
1. 毎日、翌日の予定一覧が届くようにトリガー登録する。

## 事前準備
### カレンダーに予定を作成
すでに予定がびっしりな方は不要ですが、今回やることではイベントが取得できないと面白くないので、何かしら作っておきましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/cfc1d83e-ab53-b015-23b4-6645f4b81c87.png" width="600">

:::note info
Googleカレンダーでイベントを作成する際は、「予定」と「タスク」があります。しかしながら、これまでお伝えした`CalendarApp`では**前者の「予定」しか取得することはできません**。「タスク」の取得はまた今度！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/190169f9-bef5-60cb-15ad-b6b27d72fe7f.png" width="300">
:::

### カレンダーテンプレートの準備
スプレッドシートでカレンダーテンプレートを適当に作成しましょう。私は以下のような様式を作成しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/7b1313e6-e0eb-796e-5fd0-a9a60da5dfc0.png" width="600">

### PDF出力場所の作成
「今回やること」にも記載しましたが、途中でスプレッドシートをPDF変換するため、その出力先を作成しておきましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/3d7d9053-eb00-b104-e209-d3584d31ad76.png" width="600">

### IDの取得
スプレッドシートはコンテナバインド型で開くためIDは不要ですが、`PDF出力先`のIDは事前に取得しておきましょう。

## プログラム開始
### GASエディタを開く
準備したカレンダーテンプレートからコンテナバインド型でGASプログラムを記述していきましょう。<a href="https://qiita.com/gas-suke/items/5cf7bd4c6c3f15a25838#%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%83%90%E3%82%A4%E3%83%B3%E3%83%89%E5%9E%8B" target="_blank">こちら</a>を参考にGASエディタを起動してください。では、どんどんいきますよ、ついてきてください。

### STEP.1　Googleカレンダーの翌日のイベントをすべての取得する
```javascript:Qiita008.gs
function Qiita008_SpreadSheet() {
  // カレンダークラスを用いて、IDを指定してGoogleカレンダーにアクセスする
  const wCalendar = CalendarApp.getCalendarById('*******************@gmail.com');

  let wToday = new Date()
      , wTriggerDate = new Date();
  // 対象日付を翌日にセットする
  wTriggerDate.setDate(wToday.getDate()+1);

  // 対象日付のイベントを取得する
  const wTomorrowEvent = wCalendar.getEventsForDay(wTriggerDate);
  // 各イベントの開始・終了時間等の詳細をログに出力する
  wTomorrowEvent.forEach(function(wEvent){
    console.log(Utilities.formatDate(wEvent.getStartTime(), 'JST', 'HH:mm:ss'));
    if (wEvent.isAllDayEvent()) {
      console.log('23:59:59')
    } else {
      console.log(Utilities.formatDate(wEvent.getEndTime(), 'JST', 'HH:mm:ss'));
    }
    console.log(wEvent.getTitle());
    console.log(wEvent.getLocation());
    console.log(wEvent.getDescription());
  });
}
```

```:実行結果
23:09:27	お知らせ	実行開始
23:09:28	情報	00:00:00
23:09:28	情報	23:59:59
23:09:28	情報	終日の予定１
23:09:28	情報	お台場, 日本、東京都 お台場
23:09:28	情報	
23:09:28	情報	08:30:00
…
23:09:28	お知らせ	実行完了
```
まずは翌日の予定をすべて取得できました。`CalendarEvent`クラスは非常に扱いやすいクラスです。詳しくは以下をご覧ください。

https://developers.google.com/apps-script/reference/calendar/calendar-event?hl=ja

### STEP.2　イベントをスプレッドシートに一覧でまとめる
```javascript:Qiita008.gs
const COL = {
  NUM: 1
  , START: 2
  , END: 3
  , TITLE: 4
  , LOCATION: 5
  , DETAIL: 6
}

function Qiita008_SpreadSheet() {
  // カレンダークラスを用いて、IDを指定してGoogleカレンダーにアクセスする
  const wCalendar = CalendarApp.getCalendarById('*******************@gmail.com');
  // スプレッドシートクラスを用いて、バインドしているスプレッドシートにアクセスする 
  const wSpread = SpreadsheetApp.getActiveSpreadsheet(); 
  const wSheet = wSpread.getSheets()[0];

  let wToday = new Date()
      , wTriggerDate = new Date();
  // 対象日付を翌日にセットする
  wTriggerDate.setDate(wToday.getDate()+1);

  let wPasteLst = new Array();
  // スプレッドシート内の既存データをクリアする
  let wLstRow = wSheet.getLastRow();
  if (wLstRow>1) wSheet.getRange(2, COL.NUM, wLstRow-1, COL.DETAIL).clearContent();
  
  // 対象日付のイベントを取得する
  const wTomorrowEvent = wCalendar.getEventsForDay(wTriggerDate);
  // 各イベントの開始・終了時間等の詳細を貼り付け用のリストに設定する
  wTomorrowEvent.forEach(function(wEvent, eIdx){
    wPasteLst.push([
      eIdx+1
      , Utilities.formatDate(wEvent.getStartTime(), 'JST', 'HH:mm:ss')
      , wEvent.isAllDayEvent() ? '23:59:59' : Utilities.formatDate(wEvent.getEndTime(), 'JST', 'HH:mm:ss')
      , wEvent.getTitle()
      , wEvent.getLocation()
      , wEvent.getDescription()
    ]);
  });
  wSheet.getRange(2, COL.NUM, wPasteLst.length, wPasteLst[0].length).setValues(wPasteLst);
}
```
こちらを実行すると、先ほどまではログに出力したカレンダーの内容がスプレッドシートに書き込まれます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/6e07e96f-1525-1af9-ae7d-2b48f95ff034.png" width="600">
ゴールは近いですね。

:::note info
`wEvent.isAllDayEvent() ? '23:59:59' : Utilities.formatDate(wEvent.getEndTime(), 'JST', 'HH:mm:ss')`
この箇所、少しだけテクニックを入れてあります。
`isAllDayEvent`つまり終日予定の場合かそうでないかで配列に設定する値を変えたいんです。しかしながら、**配列の値をセットしている途中**にif文を記載することはできません（1要素ずつのpushなら別ですが）。
そこで三項演算子を使って、`isAllDayEvent`の返却値によって値をセットしています。三項演算子は
```
条件式 ? 条件式がtrueの場合に評価される式 : 条件式がfalseの場合に評価される式;
```
といった構文となっており、配列の中だとしても柔軟に設定を行うことができます。
:::

### STEP.3　スプレッドシートをPDF変換する
```javascript:Qiita008.gs
const COL = {
  NUM: 1
  , START: 2
  , END: 3
  , TITLE: 4
  , LOCATION: 5
  , DETAIL: 6
}

const PDF_OUTDIR = DriveApp.getFolderById('1rwVxGOY14fcQcJEwTFV-_IDXsaBSVy5A');

function Qiita008_SpreadSheet() {
  // カレンダークラスを用いて、IDを指定してGoogleカレンダーにアクセスする
  const wCalendar = CalendarApp.getCalendarById('*******************@gmail.com');
  // スプレッドシートクラスを用いて、バインドしているスプレッドシートにアクセスする 
  const wSpread = SpreadsheetApp.getActiveSpreadsheet(); 
  const wSheet = wSpread.getSheets()[0];

  let wToday = new Date()
      , wTriggerDate = new Date();
  // 対象日付を翌日にセットする
  wTriggerDate.setDate(wToday.getDate()+1);

  let wPasteLst = new Array();
  // スプレッドシート内の既存データをクリアする
  let wLstRow = wSheet.getLastRow();
  if (wLstRow>1) wSheet.getRange(2, COL.NUM, wLstRow-1, COL.DETAIL).clearContent();
  
  // 対象日付のイベントを取得する
  const wTomorrowEvent = wCalendar.getEventsForDay(wTriggerDate);
  // 各イベントの開始・終了時間等の詳細を貼り付け用のリストに設定する
  wTomorrowEvent.forEach(function(wEvent, eIdx){
    wPasteLst.push([
      eIdx+1
      , Utilities.formatDate(wEvent.getStartTime(), 'JST', 'HH:mm:ss')
      , wEvent.isAllDayEvent() ? '23:59:59' : Utilities.formatDate(wEvent.getEndTime(), 'JST', 'HH:mm:ss')
      , wEvent.getTitle()
      , wEvent.getLocation()
      , wEvent.getDescription()
    ]);
  });
  wSheet.getRange(2, COL.NUM, wPasteLst.length, wPasteLst[0].length).setValues(wPasteLst);

  let wPdfId = createPdf(wSpread.getId()
                         , wSheet.getSheetId()
                         , 'A1%3AF'+(wPasteLst.length+1)
                         , Utilities.formatDate(wTriggerDate, 'JST', 'yyyyMMdd')+'の予定');
}

function createPdf(spreadId, sheetId, rangeStr, fileName){
  // PDF変換するためのベースURLを作成する
  let wUrl = `https://docs.google.com/spreadsheets/d/${spreadId}/export?gid=${sheetId}&exportFormat=pdf`;
  let wPdfOpt = '&size=A4'                      // 用紙サイズ
              + '&portrait=true'                // 用紙の向き true:縦向き / false:横向き
              + '&fitw=true'                    // ページ幅を用紙にフィットさせるか true:フィットさせる / false:原寸大
              + '&top_margin=0.50'              // 上の余白
              + '&right_margin=0.50'            // 右の余白
              + '&bottom_margin=0.50'           // 下の余白
              + '&left_margin=0.50'             // 左の余白
              + '&horizontal_alignment=CENTER'  // 水平方向の位置
              + '&vertical_alignment=TOP'       // 垂直方向の位置
              + '&range='+rangeStr;             // 変換範囲

  // headersにアクセストークンを格納する
  let wOtions = {
    headers: {
      'Authorization': `Bearer ${ScriptApp.getOAuthToken()}`
    }
  }; 
  // PDFを作成する
  let wBlob = UrlFetchApp.fetch(wUrl+wPdfOpt, wOtions).getBlob().setName(fileName + '.pdf');

  //PDFを指定したフォルダに保存する
  return PDF_OUTDIR.createFile(wBlob).getId();
}
```
実行すると、事前準備で作成した`PDF出力先`に以下のようなPDFが作成されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/1fe68b95-56b1-238c-cf94-88cbcf917961.png" width="600">
ここでポイントとなるのは、<a href="https://qiita.com/gas-suke/items/a1783b68fac098c8c3ae" target="_blank">前回</a>と同様にPDF変換用のURL部`https://docs.google.com/spreadsheets/d/${spreadId}/export?gid=${sheetId}&exportFormat=pdf`　およびその他変換オプションとなります。

:::note info
コード内でも一部は使用しましたが、変換オプションには以下のパラメタを使用することができます。
| パラメタ | 役割 | 設定例 | 
| ---- | ---- | ---- |
| size | 用紙サイズ | A3, A4 など |
| portrait | 用紙の向き |  true:縦向き / false:横向き | 
| fitw | ページ幅を用紙にフィットさせるか | true:フィットさせる / false:原寸大 | 
| top_margin | 上の余白 | 0, 0.5, 1.0など | 
| right_margin | 右の余白 | 0, 0.5, 1.0など | 
| bottom_margin | 下の余白 | 0, 0.5, 1.0など | 
| left_margin | 左の余白 | 0, 0.5, 1.0など | 
| horizontal_alignment | 水平方向の位置 | LEFT, CENTER, RIGHT | 
| vertical_alignment | 垂直方向の位置 | TOP, MIDDLE, BOTTOM | 
| range | セル範囲 | A1%3AF5 <br/> `%3Aは:を意味します。つまり例はA1:F5をURIencodingしているということです` | 
| scale | 表示スケール | 1：標準100% / 2：幅に合わせる / 3：高さに合わせる / 4：ページに合わせる | 
| printtitle | スプレッドシート名をPDFに表示するか | true：表示する / false：表示しない | 
| sheetnames | シート名をPDFに表示するか | true：表示する / false：表示しない | 
| pagenum | ページ番号を表示するか | true：表示する / false：表示しない | 
| gridlines | グリッドラインを表示するか | true：表示する / false：表示しない | 
| fzr | 固定行を表示するか | true：表示する / false：表示しない | 
| fzc | 固定列を表示するか | true：表示する / false：表示しない | 
:::

### STEP.4　変換したPDF（翌日の予定一覧）を添付ファイルとして自分自身にメールを送信する
これは簡単です。STEP.3まででPDFはできているので、それを**添付するだけ**です。
```javascript:Qiita008.gs
const COL = {
  NUM: 1
  , START: 2
  , END: 3
  , TITLE: 4
  , LOCATION: 5
  , DETAIL: 6
}

const PDF_OUTDIR = DriveApp.getFolderById('1rwVxGOY14fcQcJEwTFV-_IDXsaBSVy5A');

function Qiita008_SpreadSheet() {
  // カレンダークラスを用いて、IDを指定してGoogleカレンダーにアクセスする
  const wCalendar = CalendarApp.getCalendarById('*******************@gmail.com');
  // スプレッドシートクラスを用いて、バインドしているスプレッドシートにアクセスする 
  const wSpread = SpreadsheetApp.getActiveSpreadsheet(); 
  const wSheet = wSpread.getSheets()[0];

  let wToday = new Date()
      , wTriggerDate = new Date();
  // 対象日付を翌日にセットする
  wTriggerDate.setDate(wToday.getDate()+1);

  let wPasteLst = new Array();
  // スプレッドシート内の既存データをクリアする
  let wLstRow = wSheet.getLastRow();
  if (wLstRow>1) wSheet.getRange(2, COL.NUM, wLstRow-1, COL.DETAIL).clearContent();
  
  // 対象日付のイベントを取得する
  const wTomorrowEvent = wCalendar.getEventsForDay(wTriggerDate);
  // 各イベントの開始・終了時間等の詳細を貼り付け用のリストに設定する
  wTomorrowEvent.forEach(function(wEvent, eIdx){
    wPasteLst.push([
      eIdx+1
      , Utilities.formatDate(wEvent.getStartTime(), 'JST', 'HH:mm:ss')
      , wEvent.isAllDayEvent() ? '23:59:59' : Utilities.formatDate(wEvent.getEndTime(), 'JST', 'HH:mm:ss')
      , wEvent.getTitle()
      , wEvent.getLocation()
      , wEvent.getDescription()
    ]);
  });
  wSheet.getRange(2, COL.NUM, wPasteLst.length, wPasteLst[0].length).setValues(wPasteLst);
  
  // スプレッドシートをPDFにエクスポートする
  let wPdfId = createPdf(wSpread.getId()
                         , wSheet.getSheetId()
                         , 'A1%3AF'+(wPasteLst.length+1)
                         , Utilities.formatDate(wTriggerDate, 'JST', 'yyyyMMdd')+'の予定');
  
  // 自分自身のメールアドレス宛にPDFを送信する
  GmailApp.sendEmail(
    '*******************@gmail.com'
    , '【自動送信メール】明日の予定'
    , '' // 自分あてなので本文はなくても笑
    , {attachments: DriveApp.getFileById(wPdfId).getBlob()}
  );
}

function createPdf(spreadId, sheetId, rangeStr, fileName){
  // PDF変換するためのベースURLを作成する
  let wUrl = `https://docs.google.com/spreadsheets/d/${spreadId}/export?gid=${sheetId}&exportFormat=pdf`;
  let wPdfOpt = '&size=A4'                      // 用紙サイズ
              + '&portrait=true'                // 用紙の向き true:縦向き / false:横向き
              + '&fitw=true'                    // ページ幅を用紙にフィットさせるか true:フィットさせる / false:原寸大
              + '&top_margin=0.50'              // 上の余白
              + '&right_margin=0.50'            // 右の余白
              + '&bottom_margin=0.50'           // 下の余白
              + '&left_margin=0.50'             // 左の余白
              + '&horizontal_alignment=CENTER'  // 水平方向の位置
              + '&vertical_alignment=TOP'       // 垂直方向の位置
              + '&range='+rangeStr;             // 変換範囲

  // headersにアクセストークンを格納する
  let wOtions = {
    headers: {
      'Authorization': `Bearer ${ScriptApp.getOAuthToken()}`
    }
  }; 
  // PDFを作成する
  let wBlob = UrlFetchApp.fetch(wUrl+wPdfOpt, wOtions).getBlob().setName(fileName + '.pdf');

  //PDFを指定したフォルダに保存する
  return PDF_OUTDIR.createFile(wBlob).getId();
}
```
こちらを実行するとメールにて予定が送られてきましたよね。これでひとまずは完成です。

**が！！！！** 翌日の予定一覧が欲しいときに**GASエディタを開いていちいち実行**というのはめんどくさいですよね。そこで、今回つくったメソッドが**毎日定時に実行されるようスケジュール登録**していきましょう。

### STEP.5　毎日、翌日の予定一覧が届くようにトリガー登録する
<table>
  <tr>
    <th>№</td>
    <th>手順</td>
    <th>イメージ</td>
  </tr>
  <tr>
    <td>1</td>
    <td>GASエディタの左メニューから「トリガー」をクリックします。</td>
    <td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/49fd755f-4dc4-bc29-68cc-ecd50c4f6a02.png" width="400">
</td>
  </tr>
  <tr>
    <td>2</td>
    <td>トリガー一覧画面に切り替わったら右下の「トリガーを追加」をクリックします。</td>
    <td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/1c7705dc-f0e2-44c1-a62f-e3d0396b03f2.png" width="600"></td>
  </tr>
  <tr>
    <td>3</td>
    <td>出てきたメニュー上で右イメージにあるように「日付ベースのタイマー」と右下の「保存」をクリックします。<br/>※時刻はお好きな時間を選択してください。</td>
    <td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/0ae3e813-040e-32a1-c589-394460d2662d.png" width="400"></td>
  </tr>
  <tr>
    <td>4</td>
    <td>「保存」をクリック後、例のごとく権限設定のダイアログが出ますので許可すると、右イメージのようにトリガーが追加されていることを確認できます。</td>
    <td><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/d2a100f4-24ec-46d8-0da5-90faaf480851.png" width="600"></td>
  </tr>
</table>

これで毎日上記で登録した時刻にメソッドが動き出し、翌日の予定を自身に送信してくれるようになりました。

:::note info
このやり方の場合、[時刻を選択]で**選択できる時間には幅**があり、例えば22:00きっかりにという指定ができません。でも、スケジュール実行ならそうしたいですよね。**安心してください、できます**。次回、そういった**小技**をお伝えしていきます。
:::

### 
# おわりに
お疲れ様でした。
第8回は「GASを用いてGoogleカレンダー+メール送信で翌日の予定を通知しよう」ということで、前回に引き続きPDF変換およびメール送信、さらにはトリガー登録と、より**実業務で活用できそうなこと**をちりばめてみました。次回は、私がこれまで習得してきたGASのちょっとした小技をいくつか紹介します。あえて言っておきます、**神回**です。
記事を読んで、「良いな」や「今後に期待できる！」と感じて頂けたら**いいね**や**フォロー**、**コメント**いただけると幸いです。それではまた次回をお楽しみに！

## ブログでより詳しく解説しています！
以下画像をクリックしてブログにアクセス！！
<a href="https://gas-4u.com/gas07/" target="_blank"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3575702/a6e99a83-8e73-91dd-ed4a-34969968d546.png" width="600"/></a>