CloudSchedulerで毎月第一営業日に実行する

# はじめに
`Cloud Scheduler`で、毎月第一月曜日とか毎月第一営業日とかにジョブを実行 (Pub/Subトピックにメッセージを送信) する方法が分からなかったので、備忘録として残しておく。
ドキュメントには、

> Unix cron 形式でスケジュールを定義して、1 日に複数回ジョブを実行したり、1 年のうちの特定の日や月にジョブを実行したりできます。

と書かれてある。
が、cronの仕様だと、同時に **日にち** と **曜日** の指定をすると、
ANDではなくOR条件になるみたいなので一筋縄ではいかなさそう。

https://cloud.google.com/scheduler?hl=ja

# 第一営業日を判定する関数の作成
```typescript:isFirstBusinessDayOfMonth.ts
const MONTHS = {
    JAN: 0,
    FEB: 1,
    MAR: 2,
    APR: 3,
    MAY: 4,
    JUN: 5,
    JUL: 6,
    AUG: 7,
    SEP: 8,
    OCT: 9,
    NOV: 10,
    DEC: 11,
} as const;

const NATIONAL_HOLIDAYS = [
    new Date(2023, MONTHS.JAN, 1), // 元日
    new Date(2023, MONTHS.JAN, 2), // 振替休日
  　　　　new Date(2023, MONTHS.JAN, 9), // 成人の日
  　　　　new Date(2023, MONTHS.FEB, 11), // 建国記念の日
    new Date(2023, MONTHS.FEB, 23), // 天皇誕生日
    new Date(2023, MONTHS.MAR, 21), // 春分の日
    new Date(2023, MONTHS.APR, 29), // 昭和の日
    new Date(2023, MONTHS.MAY, 3), // 憲法記念日
    new Date(2023, MONTHS.MAY, 4), // みどりの日
    new Date(2023, MONTHS.MAY, 5), // こどもの日
    new Date(2023, MONTHS.JUL, 17), // 海の日
    new Date(2023, MONTHS.AUG, 11), // 山の日
    new Date(2023, MONTHS.SEP, 18), // 敬老の日
    new Date(2023, MONTHS.SEP, 23), // 秋分の日
    new Date(2023, MONTHS.OCT, 9), // スポーツの日
    new Date(2023, MONTHS.NOV, 3), // 文化の日
    new Date(2023, MONTHS.NOV, 23), // 勤労感謝の日
];

const isSaturday = (date: Date): boolean => date.getDay() === 6;
const isSunday = (date: Date): boolean => date.getDay() === 0;
const isNationalHoliday = (date: Date): boolean => NATIONAL_HOLIDAYS.includes(date);

export const isFirstBusinessDayOfMonth = (date: Date): boolean => {
    let firstBusinessDayOfMonth = new Date(
        date.getFullYear(),
        date.getMonth(),
        1
    );
    
    while (
        isSaturday(firstBusinessDayOfMonth) ||
        isSunday(firstBusinessDayOfMonth) ||
        isNationalHoliday(firstBusinessDayOfMonth)
    ) {
        firstBusinessDayOfMonth = new Date(
            firstBusinessDayOfMonth.getFullYear(),
            firstBusinessDayOfMonth.getMonth(),
            firstBusinessDayOfMonth.getDate() + 1
        );
    }

    return (
        date.getFullYear() === firstBusinessDayOfMonth.getFullYear() &&
        date.getMonth() === firstBusinessDayOfMonth.getMonth() &&
        date.getDate() === firstBusinessDayOfMonth.getDate()
    );
};
```

# スケジュールの指定
```typescript:index.ts
import * as functions from 'firebase-functions';

import { isFirstBusinessDayOfMonth } from './your/path';

export const someFuncExec = functions
    .region('asia-northeast1')
    .runWith({
        memory: '1GB',
        timeoutSeconds: 300,
     })
    .pubsub.schedule('0 9 * * 1-5') // ここでは複雑な指定ができないため、平日9:00に実行するように指定
    .timeZone('Asia/Tokyo')
    .onRun(() => {
        const today = new Date();
        if (!isFirstBusinessDayOfMonth(today)) { // ここで先ほど作った関数を利用し、第一営業日じゃなかったら実行をスキップする
            return Promise.resolve();      
        } 
        return handleSomeFunc();
    });
```

![scheduler-pubsub-func.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/6d5d8186-98e5-4621-1963-2dceaafd5353.png)

以上、CloudSchedulerで毎月第一営業日にジョブを実行する方法の備忘録でした。

# 参考
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/Date

https://cloud.google.com/scheduler/docs/tut-pub-sub?hl=ja

https://dev.classmethod.jp/articles/try-cloud-functions-scheduler-pubsub/

https://qiita.com/mikan3rd/items/781dd4eed6b3bbcb514b