月末営業日にタスクをリマインドする ~ 使うのはSlackとGoogleアカウントだけで10分くらいで実装できるよ ~

# 月末って忙しいよね…

月末って報告書とか、領収証とか…なんかたくさん出さなきゃいけないものあるよね。センパイが「誰か作って」って言ってたのをGoogle apps scriptで作成しました。
Google apps script[GAS]触りだして4日目くらいですが、かなり便利なのでエンジニア以外のもっとたくさんの人に触れて欲しいなと思い、連日ですがGASの実用例をあげたいと思います。

# 完成品

月末の営業日にSlack投げるだけのシンプルな実装です。
コピペすれば10分で実装いけるんじゃないかと！

<img width="555" alt="スクリーンショット 2017-03-06 21.29.19.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/112929/e6e7c048-3dcd-0948-0d0d-597bf148ae4d.jpeg">

## 完成までに使用するもの

Googleアカウント[Google Apps Script]
Slack

## 完成したら

以下の2つもコード公開します
* 月初めの営業日になにかをする
* 月始めの第一金曜日になにかをする

つまりこれを応用すれば、いつ何時でもSlackで自動チャットを飛ばせるようになる。

# 月末締め…！！

画像ありでどんどんいくよ〜〜

## Slack Tokenの取得

ここを参考にしてみて！
http://qiita.com/ykhirao/items/0d6b9f4a0cc626884dbb

xoxp-18127　こんな感じの長い数字。

Slack持ってない人は[個人Slackのススメ](http://qiita.com/saitotak/items/ac0eb7ddc0d8d83cbe91)にしたがって導入をすすめます。

## GASの導入

Googleドライブにアクセス
https://drive.google.com/drive/my-drive

<img width="1192" alt="スクリーンショット 2017-03-06 21.59.16.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/112929/f0603105-eb23-a7ca-53be-8e593d104516.jpeg">

`新規 >> その他 >> アプリを追加`
から Google apps script を検索・追加

<img width="1196" alt="スクリーンショット 2017-03-06 22.00.23.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/112929/5f579f0d-2d24-34b6-8634-a7f0f4869537.jpeg">

そうすると`新規 >> その他 >> Google apps script`を押してみてください。
GAS画面になると思います。

## GASの編集

<img width="816" alt="スクリーンショット 2017-03-06 22.23.56.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/112929/fcfa979d-2652-f795-c5ba-cb7de40cab02.jpeg">

メインの部分にこれを貼り付けてみよう！

```js

/*
  メインの処理
  setTriggerDay() : 最終営業日(0時)に次の処理をする命令を出す。
  setTriggerHoursLast() : 最終営業日の18時に次の命令を出す。
  sendSlack() : slackにデータを投げる。

  サブ的な処理
  deleteTrigger() : 蓄積されている命令を削除する
  lastBusinessDay() : 今月の最終営業日を求める
  isHoliday() : 本日が[日本の祝日]かどうかチェック。土日は曜日で判定してるチェック
  .getDay()　: Dateオブジェクトから曜日を求めるメソッド(0:日, 6:土曜日)
*/

function setTriggerDay()
{  
  var last = lastBusinessDay();
  ScriptApp.newTrigger("setTriggerHoursLast")
    .timeBased()
    .atDate(last.getFullYear(), last.getMonth()+1, last.getDate())
    .create();
}

function setTriggerHoursLast()
{
  deleteTrigger("setTriggerHoursLast");
  ScriptApp.newTrigger("sendSlack")
    .timeBased()
    .after( 18 * 60 * 60 * 1000 )
    .create();
}

function sendSlack() 
{
  deleteTrigger("sendSlack");
  var options = 
  {
    "method" : "POST",
    "payload" : 
    {
      "token": "********************",
      "channel": "bot-test",
      "text": "領収書出しましたか？忘れてないよね。\ｎhttp://.../"
    }
  }
  var url = "https://slack.com/api/chat.postMessage"
  UrlFetchApp.fetch(url, options);
}

function lastBusinessDay() 
{
  var today = new Date();
  
  var lastDayOfThisMonth = new Date(today.getFullYear(), today.getMonth()+1, 0);
  var day; // 0->日曜日
  
  for (var i = 0; i < 30; i++) {
    day = lastDayOfThisMonth.getDay();
    if (day == 0 || day == 6 || isHoliday(lastDayOfThisMonth)) {
      lastDayOfThisMonth = new Date(today.getFullYear(), today.getMonth()+1, -1 * i);
      continue;
    }
  }
  return lastDayOfThisMonth;
}

function deleteTrigger(name) 
{
  var triggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == name) {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

function isHoliday(day) 
{
  var startDate = new Date(day.setHours(0, 0, 0, 0));
  var endDate = new Date(day.setHours(23, 59, 59));
  var cal = CalendarApp.getCalendarById("ja.japanese#holiday@group.v.calendar.google.com");
  var holidays =  cal.getEvents(startDate, endDate);
  return holidays.length != 0; // 祝日ならtrue
}

```
sendSlackのここは個人設定に変えてください！

- "token": "********************",
- "channel": "bot-test",
- "text": "領収書出しましたか？忘れてないよね。\ｎhttp://.../"


## setTriggerDay()の実行と取り消しと毎月実行
実行 >> setTriggerDay
をおしてみよう。

<img width="527" alt="スクリーンショット 2017-03-06 22.26.52.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/112929/6eb86e29-a4f9-4fd0-e62d-76a8af233a72.jpeg">

リソース >> 現在のプロジェクトのトリガー
ここからトリガー（何時にあれを実行しなさい）を確認できます。

<img width="648" alt="スクリーンショット 2017-03-06 22.27.40.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/112929/daefaa91-51fc-0fbf-4057-b316c8be45b1.jpeg">

ここで 2017/03/21 夜中丁度に setTriggerHoursLast が実行されるようになっていたら大丈夫。setTriggerHoursLastは18時間後にSlack送りなさいという命令です。

<img width="838" alt="スクリーンショット 2017-03-06 22.27.47.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/112929/bb6cf9a2-aef7-769c-c69b-ba4eaa9b3e98.jpeg">

またついでにこの画面でsetTriggerDayを毎月1日6時くらいにセットします。

<img width="836" alt="スクリーンショット 2017-03-06 22.37.18.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/112929/3288c9c8-dea7-5bdd-eefa-43fc7735fcfb.jpeg">


そうすると毎月1日にsetTriggerDayが実行されて
-> setTriggerDay :最終日に次を実行
-> setTriggerHoursLast :18時に次を実行
-> sendSlack :Slackを実行

って感じで毎月、最終日トリガーを作成してくれます。

## functionごとに見ていく

簡単にみていきます。

### setTriggerDay

```js
function setTriggerDay()
{  
  var last = lastBusinessDay();
  ScriptApp.newTrigger("setTriggerHoursLast")
    .timeBased()
    .atDate(last.getFullYear(), last.getMonth()+1, last.getDate())
    .create();
}
```

lastBusinessDay()で営業日最終日のDateオブジェクトを取ってきて
.atDate(last.getMonth()+1)としているところは、月は0月始まりとなっているからです。

### setTriggerHoursLast

```js
function setTriggerHoursLast()
{
  deleteTrigger("setTriggerHoursLast");
  ScriptApp.newTrigger("sendSlack")
    .timeBased()
    .after( 18 * 60 * 60 * 1000 )
    .create();
}
```
.after( 18 * 60 * 60 * 1000 )でミリ秒後にトリガーをセットしてます。

### sendSlack

```js
function sendSlack() 
{
  deleteTrigger("sendSlack");
  var options = 
  {
    "method" : "POST",
    "payload" : 
    {
      "token": "********************",
      "channel": "bot-test",
      "text": "領収書出しましたか？忘れてないよね。\ｎhttp://.../"
    }
  }
  var url = "https://slack.com/api/chat.postMessage"
  UrlFetchApp.fetch(url, options);
}
```

こういう書き方だと思っていただければ…
詳しくは公式ドキュメントで　https://api.slack.com/methods/chat.postMessage

### lastBusinessDay

```js
function lastBusinessDay() 
{
  var today = new Date();
  var lastDayOfThisMonth = new Date(today.getFullYear(), today.getMonth()+1, 0);
  var day; // 0->日曜日

  for (var i = 0; i < 30; i++) {
    day = lastDayOfThisMonth.getDay();
    if (day == 0 || day == 6 || isHoliday(lastDayOfThisMonth)) {
      lastDayOfThisMonth = new Date(today.getFullYear(), today.getMonth()+1, -1 * i);
      continue;
    }
    break;
  }
  return lastDayOfThisMonth;
}
```

day == 0 || day == 6 || isHoliday(lastDayOfThisMonth)
ここで土日もしくは祝日判定をしています。

### deleteTrigger

```js

function deleteTrigger(name) 
{
  var triggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == name) {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

```

実は自分でセットしたtriggerってこんな感じで1分ごとにたまっていきます。
今回だと"MAIAMAIN"って名前のトリガーを全削除する機能になってます。
<img width="735" alt="スクリーンショット 2017-03-06 23.14.29.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/112929/ac6f4c7c-f142-f4ca-f7dc-f95088bc7118.jpeg">

参考：　http://qiita.com/sumi-engraphia/items/465dd027e17f44da4d6a

### isHoliday

```js

function isHoliday(day) 
{
  var startDate = new Date(day.setHours(0, 0, 0, 0));
  var endDate = new Date(day.setHours(23, 59, 59));
  var cal = CalendarApp.getCalendarById("ja.japanese#holiday@group.v.calendar.google.com");
  var holidays =  cal.getEvents(startDate, endDate);
  return holidays.length != 0; // 祝日ならtrue
}

```

getCalendarByIdでGoogleの[日本の祝日]を取得してきてます。

参考：　http://qiita.com/kamatama_41/items/be40e05524530920a9d9

終わりました〜〜
次は月初めの営業日を取っていきます〜〜

# 月初めの営業日

```js

/*
  メインの処理
  setTriggerDay() : はじめの営業日(0時)に次の処理をする命令を出す。
  setTriggerHoursFirst() : はじめの営業日の11時に次の命令を出す。
  sendSlack() : slackにデータを投げる。

  サブ的な処理
  deleteTrigger() : 蓄積されている命令を削除する
  firstNextBusinessDay() : 来月のはじめの営業日を求める
  isHoliday() : 本日が[日本の祝日]かどうかチェック。土日は曜日で判定してるチェック
  .getDay()　: Dateオブジェクトから曜日を求めるメソッド(0:日, 6:土曜日)
*/

function setTriggerDay2()
{  
  var first = firstNextBusinessDay();
  ScriptApp.newTrigger("setTriggerHoursFirst")
    .timeBased()
    .atDate(first.getFullYear(), first.getMonth()+1, first.getDate())
    .create();
}

function setTriggerHoursFirst()
{
 ScriptApp.newTrigger("sendSlack")
   .timeBased()
   .after(11 * 60 * 60 * 1000)
   .create();
}

function sendSlack() 
{
  var options = 
  {
    "method" : "POST",
    "payload" : 
    {
      "token": "********************",
      "channel": "bot-test",
      "text": "月が始まりました。"
    }
  }
  var url = "https://slack.com/api/chat.postMessage"
  UrlFetchApp.fetch(url, options);
}

function firstNextBusinessDay() 
{
  var today = new Date();  
  var firstDayOfNextMonth = new Date(today.getFullYear(), today.getMonth()+1, 1);
  var day; // 0->日曜日
  
  for (var i = 2; i < 32; i++) {
    day = firstDayOfNextMonth.getDay();
    if (day == 0 || day == 6 || isHoliday(firstDayOfNextMonth)) {
      firstDayOfThisMonth = new Date(today.getFullYear(), today.getMonth(), i);
      continue;
    }
  }
  return firstDayOfNextMonth;
}

function deleteTrigger() 
{
  var triggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == "main") {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

function isHoliday(day) 
{
  var startDate = new Date(day.setHours(0, 0, 0, 0));
  var endDate = new Date(day.setHours(23, 59, 59));
  var cal = CalendarApp.getCalendarById("ja.japanese#holiday@group.v.calendar.google.com");
  var holidays =  cal.getEvents(startDate, endDate);
  return holidays.length != 0; // 祝日ならtrue
}

```

# 第一月曜日営業日

```js

function firstNextBusinessMonDay() {
  var today = new Date();
  
  var firstDayOfNextMonth = new Date(today.getFullYear(), today.getMonth()+1, 1);
  var day; // 0->日曜日
  
  for (var i = 2; i < 32; i++) {
    day = firstDayOfNextMonth.getDay();
    if (day != 1 || isHoliday(firstDayOfNextMonth)) {
      firstDayOfThisMonth = new Date(today.getFullYear(), today.getMonth(), i);
      continue;
    }
    break;
  }
  return firstDayOfNextMonth;
}

```
ここだけ変えればいいかな、こんな感じ？
1日から探索して、月曜日以外もしくは祝日なら次の日にいく。


以上です。


# 終わりに

重要なことはそのままおコピペしてなんかやらかしちゃったとか責任とれないんで(ミスあっても、Slackがあれるとか、もしくは通知されないとか？)自身の環境でデバックしてからお使いください！

Google Apps Script楽しい！
















.