[GAS] 具体事例から学ぶScriptAppによるトリガー生成コードサンプル（３）～平日の日中のみ○分ごとに実行したい場合

# はじめに
スプレッドシートを始めとしたGoogleのクラウドアプリは、**時間主導型トリガー**（以降、タイムトリガー）を使用して、定型処理を定期的に自動実行させることができます。

タイムトリガーはＵＩ画面から簡単に設定可能ですが、ＵＩにて設定できる項目が限られているため、要望する内容によってはGASの`ScriptApp`を使ってプログラム側からトリガーを生成する必要があります。

本稿では、ＵＩ画面からの設定では対応できない内容について、当方にご相談頂いた内容を元にした事例別の参考コードを不定期連載形式で紹介していきたいと思います。（ネタが溜まってきたら、随時公開予定です）

今回はカレンダー連携が必要なケースとして、**平日の日中のみ○分ごとに実行したい場合**のご紹介です。

::: note
**前提条件**
1. 仕様上、実行遅延（タイムラグ）がおよそ数分以内のレベルで発生します。そのため、厳密な時間管理が必要な処理には不向きです。（[公式](https://developers.google.com/apps-script/reference/script/clock-trigger-builder)にて一部メソッドにおいては±15分と明記されています）
2. 同様に、仕様上、エラー通知の頻度をスクリプト側で設定できないため、 **毎日通知を受け取る** がデフォルトとなります。
3. 拙記事ですが、[こちら](https://qiita.com/STSHISHO/items/2985c7b92c80b28f1018)を参考にタイムゾーンの設定が実行環境と合っているか確認してください。
4. サンプルコードでは１日あたりの総実行時間、１ユーザーあたり２０トリガーまで等、GASの制限事項は考慮していません。
:::

::: note
**設置時の共通事項**
1. トリガー生成ロジックは独立した関数として定義し、メイン処理内に組み込みます。組み込み位置は末尾を推奨しておきますが、好みや状況に合わせて適宜ご対応ください。
2. [GASの制限事項](https://developers.google.com/apps-script/guides/services/quotas#current_quotas)を考慮し、適宜メイン処理の軽量化、高速化をしましょう。また、実行回数に関係なく、実行済みトリガーの削除処理の実装を推奨します。
3. 初回のトリガー設定のみ手動で行いますが、設定画面経由でもエディタから当該関数の直接実行でもどちらでも結構です。
:::

# カレンダー連携が必要なケース
## 平日の日中のみ○分ごとに実行したい
今回のご要望の重要ポイントは主に２点です。つまり、**平日の判定** と **日中の判定** をどうするか、ということになります。

また、今回は年末年始、お盆期間、GW等の **例外的な休日** は考慮していません。あくまでも暦通りの判定となります。 **例外的な休日** に対応させるには別途対応が必要となりますが、それほど難しい内容ではありませんので、末尾で補足させていただきます。

### 平日の判定
見出しの通り、Googleカレンダーとの連携が必要になります。連携方法にはいくつか方針がありますが、事前に祝日情報を取得しておき、参照用データとしてスプレッドシートへ保存しておくという手法がよく使われるという印象です。（コードレビューからの個人的感想です）

もちろんその方法も有効ですが、自動処理の対象がスプレッドシート以外の場合はそれほどメリットは感じられません。

最近の例では東京オリンピック開催延期に伴う特例もあり、何らかのタイミングで祝日情報を更新する必要があるのは、個人的には面倒と思っています。

そのため、本稿では都度、カレンダーにアクセスする手法を採用します。タイミングとしては当日最後の実行時となります。

### 日中の判定
一般的な就業時間、午前９時から午後１８時を日中と仮定し、指定時間ごとの実行時に判定するのが王道かと思います。就業時間を過ぎていた場合には翌平日の午前９時に実行予約をする、というイメージで良いと思います。

::: note
同様のお問い合わせは会社の事務系担当の方からいただくことが多いため、以降は社内利用をイメージした内容となります。
:::

### ロジック
- Javascriptにて現在時を取得し、就業時間中なら**○分後**、１８時を超えていたら**翌営業日の指定日時**を設定する
  - 翌営業日の判定方法：**翌日**が土日祝以外の平日になるまで再帰的に判定
- `ScriptApp`にて**特定の日時**としてトリガーを生成する

就業時間中の**○分ごとに実行する**ロジックは前回までの内容と変わりません。今回のポイントは**翌営業日を算出し実行予約をする**ロジックになります。

### サンプルコード

```Javascript: 平日の日中（午前9時～午後18時）のみ30分ごとにメイン処理を実行する
// 初期設定用オブジェクト
function init(){
  return {
    START_TIME: '09:00', // hh:mm形式で指定
    END_TIME: '18:00',
    EXE_SPAN: 30, // 実行間隔（分単位）
    TRIGGER_HUNDLER: 'main' // 自動実行対象の関数名
  };
}

// メイン処理
function main() {
  /* メイン処理省略 */

  setTrigger();
}

// トリガー生成処理
function setTrigger() {
  const now = new Date();
  const INIT = init();
  const startTime = INIT.START_TIME.split(':');
  const endTime   = INIT.END_TIME.split(':');

  // 指定日時のDateオブジェクト生成
  const createNewDate = (date, hour, minute) => new Date(date.getFullYear(), date.getMonth()+1, date.getDate(), hour, minute);

  // 翌営業日（Next Business Day）の取得
  const getNBD = date => {
    const today = new Date(date);
    const tomorrow = new Date(today.setDate(today.getDate()+1));

    // 土曜日なら日曜日をスキップし月曜日の判定
    if(tomorrow.getDay() === 6) return getNBD(new Date(tomorrow.setDate(tomorrow.getDate()+1)));

    // 祝日なら翌日の判定
    if(isHoliday(tomorrow)) return getNBD(tomorrow);

    // 翌営業日の就業開始時間を返す
    return createNewDate(tomorrow, startTime[0]*1, startTime[1]*1);
  };

  // 終業時間を超えていたら翌営業日（NBD）、就業時間中なら○分後を設定
  const closingTime = createNewDate(now, endTime[0]*1, endTime[1]*1);
  const isOverTime  = now.getTime() > closingTime.getTime();
  const targetDate = isOverTime? getNBD(now): new Date(now.setMinutes(now.getMinutes()+INIT.EXE_SPAN));

  ScriptApp.newTrigger(INIT.TRIGGER_HUNDLER)
  .timeBased()
  .at(targetDate)
  .create();
}

// 祝日の判定
function isHoliday(date){
  const holidays = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com');
  const events = holidays.getEventsForDay(date);
  return events.length > 0;
}
```

::: note
**平日判定について**
翌日が土曜日の場合、その翌日は日曜日であるのは明白なため、翌月曜日の判定に移行しています。

**就業時間中判定について**
当日初回実行は始業時間が前提のため、終業時間を超えているかどうかの判定のみとしています。
:::

### 初回設定
初回はＵＩ画面より**特定の日時**で営業日の就業時間中を指定し、`main`を実行させます。以下は2022年6月30日の午前9時に実行する場合の設定例となります。

- **実行する関数を選択**：main
- **時間ベースのトリガーのタイプを選択**：特定の日時
- **日時を入力**：2022-06-30 09:00

::: note warn
**注意事項**
サンプルコードではトリガーの削除処理を実装していないため、１ユーザーあたりのトリガー登録数が２０を超えないようにする必要があります。

つまり、上記の場合だと、約２７分間隔以下を指定しているなら、実行済みトリガーの削除処理を実装する必要があります。別のトリガーを登録している場合は、その数も考慮する必要があります。
:::

# 最後に
今回のポイントのうち、平日判定はあくまでも暦通りとしました。冒頭でも触れています、 **例外的な休日** への対応については、管理面も踏まえると別途 **休業日カレンダー** を設置し、休業日判定を組み込む方法が簡単なのではないかと思います。

なお、次回は今回の応用編として、**指定休業日以外の営業日ごとに実行したい場合** を執筆予定です。

誰かのお役に立てたなら幸いです。

# [参考・引用]
- [Class ClockTriggerBuilder  |  Apps Script  |  Google Developers](https://developers.google.com/apps-script/reference/script/clock-trigger-builder)
- [[GAS] 簡単になったGAS実行環境のタイムゾーン設定](https://qiita.com/STSHISHO/items/2985c7b92c80b28f1018)
- [Quotas for Google Services  |  Apps Script  |  Google Developers](https://developers.google.com/apps-script/guides/services/quotas#current_quotas)