GASの定期実行をピッタリの時間に行う方法

# はじめに
この記事は「[Ateam LifeDesign Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ateam-life-design)」で完走賞を狙って25記事書いているうちの23日目の記事です。ようやく終わりが見えてきた・・・！

# 作るもの

GASはトリガーを使うことでスクリプトを定期実行することができます。
ただ日次や週次、月次で定期実行するトリガーは以下の写真のように実行時間を1時間単位でしか指定することができません。
![スクリーンショット 2023-12-17 21.17.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/d3d77533-cbb0-d409-4417-6a82a268e2bc.png)

しかし実際の運用シーンを考えると「このスクリプトは毎日必ず3時ちょうどに実行したい」ってなることもありますよね。なので今日は通常のトリガー登録からは実現できない時間をピンポイントで指定した定期実行の登録の仕方を見ていこうと思います。

# 実装方針

上記の仕様を実現するためには以下のポイントを使います。

 ## ポイント1：日付を指定した場合は時間ちょうどの実行指定が可能

![スクリーンショット 2023-12-17 21.23.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/0ace691e-4753-0be9-4eb1-70d96e3435d9.png)

GASは定期実行の場合は先程のように1時間ごとでの指定しかできませんが、時間ベースのトリガーとよばれる「特定の日時」に実行するトリガーの場合は時間まで指定することができ、指定された時間ちょうどにスクリプトが実行されます。

## ポイント2：トリガーはスクリプトから動的に登録することも可能

先程の画面からトリガーを登録することが多いと思いますが、実はトリガーはスクリプト上からも`ScriptApp.newTrigger`というメソッドを呼び出すことで登録ができます。

つまり今回はスクリプトが実行されると、そのスクリプト内で、翌日に実行する分の時間ベースのトリガーを登録する処理をいれることにより、毎日決まった時間ちょうどにスクリプトが定期実行されるようにしようと思います。

# GASを書いていく

```javascript
  var triggerDay = new Date();
  triggerDay.setDate(triggerDay.getDate() + 1);
  triggerDay.setHours(3);
  triggerDay.setMinutes(0);
  triggerDay.setSeconds(0);
```

まずスクリプトが実行されたタイミングの翌日3時に次のスクリプトの実行トリガーを登録するために翌日3時の日時を取得します。`triggerDay`に`setDate`で今から24時間後の日時をセットしたうえで、日にちはそのままでよくて時間はを`setHours`、`setMinutes`、`setSeconds`を使って3時ちょうどに指定します。

```javascript
  ScriptApp.newTrigger('myFunction')
    .timeBased()
    .at(triggerDay)
    .create();
```

その後トリガーの登録を行います。トリガーの登録には`ScriptApp.newTrigger`関数を使います。トリガー名を指定し、今回は日時を指定するので`timeBased()`を呼び出し、実行する日時`triggerDay`を指定し、最後に`create()`することでトリガーが登録されます。

```javascript
  var scriptProperties = PropertiesService.getScriptProperties();
  scriptProperties.setProperty('TriggerSetAt', triggerDay.toString());
```

`PropertiesService`は、スクリプト全体でデータを保存するための機能を提供しています。`setProperty`関数を呼び出すことで、指定された名前のプロパティに値を設定します。つまり`TriggerSetAt`という名前のプロパティに`triggerDay.toString()`の値（＝日時情報）を設定することで、トリガーを設定した日時を記録しておき、後で必要なときにそれを参照することができるようになります。

ここまでの処理で実行されるたびに翌日の3時にトリガーが登録されるようになりました。

![スクリーンショット 2023-12-17 21.43.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/1aea4981-8fd4-fa38-d49e-dbdf1ce61f52.png)

しかし今のままだとスクリプトが実行されるたびにトリガーが登録されるので上記の画像のようにどんどんトリガーが増えて行ってしまいます。そのため実行するときにすでに実行されたトリガーは削除する処理を追加したいと思います。

```javascript
  var allTriggers = ScriptApp.getProjectTriggers();
  var existingTrigger = null;

  // すでに存在するmyFunctionトリガーを探す
  for (var i = 0; i < allTriggers.length; i++) {
    if (allTriggers[i].getHandlerFunction() === 'myFunction') {
      existingTrigger = allTriggers[i];
      break;
    }
  }

  // すでに存在するトリガーがあれば削除
  if (existingTrigger !== null) {
    ScriptApp.deleteTrigger(existingTrigger);
  }
```

`ScriptApp.getProjectTriggers()`を使うことでいま登録されているすべてのトリガーを取得することができます。それを1つずつ見ていきながら`getHandlerFunction()`でトリガーの名前を確認し、それが登録したときの`myFunction`と一致した場合は`ScriptApp.deleteTrigger`を使って削除するようにします。

# 完成品がこちら

## GASのコード

```javascript
function createTrigger() {
  var allTriggers = ScriptApp.getProjectTriggers();
  var existingTrigger = null;

  // すでに存在するmyFunctionトリガーを探す
  for (var i = 0; i < allTriggers.length; i++) {
    if (allTriggers[i].getHandlerFunction() === 'myFunction') {
      existingTrigger = allTriggers[i];
      break;
    }
  }

  // すでに存在するトリガーがあれば削除
  if (existingTrigger !== null) {
    ScriptApp.deleteTrigger(existingTrigger);
  }

  /* ----------------------------
  // ここに定期実行したい処理の内容をかく
  ------------------------------*/

  // 新しいトリガーを作成
  var triggerDay = new Date();
  triggerDay.setDate(triggerDay.getDate() + 1);
  triggerDay.setHours(3);
  triggerDay.setMinutes(0);
  triggerDay.setSeconds(0);

  ScriptApp.newTrigger('myFunction')
    .timeBased()
    .at(triggerDay)
    .create();

  // トリガー設定日時を記録
  var scriptProperties = PropertiesService.getScriptProperties();
  scriptProperties.setProperty('TriggerSetAt', triggerDay.toString());
}
```

## 実行結果

![スクリーンショット 2023-12-17 21.50.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/7ede8778-5234-f92e-6c8c-b14a82249ddb.png)

最後に入れた削除処理のおかげで、今回は何度実行してもトリガーは常に1つしか登録されないようになりました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200812/871c08ea-3376-0efd-85d9-af1866b92903.png" width="400">

トリガーの内容もきちんと翌日の3時ぴったりにスクリプトが実行されるようになっていることが確認できます。

# 最後に
今日はGASの定期実行を時間指定で行う方法について見ていきました。「GASはピッタリの時間には実行できない」と諦めていた方も、GUIからのトリガー登録よりは少しめんどくさくなりますが、こちらの方法を試してみてはいかがでしょうか。