日本の祝日API【Deno KV / Deno Cron / Deno Deploy】

# 今日が祝日かどうか

```bash
curl https://holiday.deno.dev
```

# 特定の日が祝日かどうか

```bash
curl https://holiday.deno.dev/20240101
curl https://holiday.deno.dev/2024-01-01
```

# 明日が祝日かどうか

```bash
# 明日
curl https://holiday.deno.dev/tomorrow
# 昨日
curl https://holiday.deno.dev/yesterday
# 明後日
curl https://holiday.deno.dev/2
# 一昨日
curl https://holiday.deno.dev/-2
# 一週間後
curl https://holiday.deno.dev/7
```

# ケンオール互換API

https://kenall.jp/docs/API/holidays/

```bash
# 全ての祝日
curl https://holiday.deno.dev/kenall.json
# 2024年の祝日
curl https://holiday.deno.dev/kenall.json?year=2024
# 2024年11月03日以降の祝日
curl https://holiday.deno.dev/kenall.json?from=2024-11-03
# 2023年11月03日以降の2023年の祝日
curl "https://holiday.deno.dev/kenall.json?from=2023-11-03&year=2023"
# 1955年01月15日以前の祝日
curl https://holiday.deno.dev/kenall.json?to=1955-01-15
# 1956年01月15日以前の1956年の祝日
curl "https://holiday.deno.dev/kenall.json?to=1956-01-15&year=1956"
# 2024年09月16日から2024年11月03日までの祝日
curl "https://holiday.deno.dev/kenall.json?from=2024-09-16&to=2024-11-03"
# 3件の祝日
curl https://holiday.deno.dev/kenall.json?limit=3
# 逆順の祝日
curl https://holiday.deno.dev/kenall.json?reverse
```

# Holidays JP 互換API

https://holidays-jp.github.io/

```bash
# 全ての祝日
curl https://holiday.deno.dev/list.json
# 2024年の祝日
curl https://holiday.deno.dev/list.json?year=2024
# 2024年11月03日以降の祝日
curl https://holiday.deno.dev/list.json?from=2024-11-03
# 2023年11月03日以降の2023年の祝日
curl "https://holiday.deno.dev/list.json?from=2023-11-03&year=2023"
# 1955年01月15日以前の祝日
curl https://holiday.deno.dev/list.json?to=1955-01-15
# 1956年01月15日以前の1956年の祝日
curl "https://holiday.deno.dev/list.json?to=1956-01-15&year=1956"
# 2024年09月16日から2024年11月03日までの祝日
curl "https://holiday.deno.dev/list.json?from=2024-09-16&to=2024-11-03"
# 3件の祝日
curl https://holiday.deno.dev/list.json?limit=3
# 逆順の祝日
curl https://holiday.deno.dev/list.json?reverse
```

# CSV API

```bash
curl https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv | iconv -f CP932 -t UTF8
```

# JavaScript実装例

<p class="codepen" data-height="430" data-default-tab="js,result" data-slug-hash="ZEPaZvx" data-user="04" style="height: 430px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/04/pen/ZEPaZvx">
  Untitled</a> by John Doe (<a href="https://codepen.io/04">@04</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# ソースコード

https://dash.deno.com/playground/holiday

[Deno Cron](https://deno.com/blog/cron)で毎日最新版に更新してます。

```typescript
import Encoding from "npm:encoding-japanese"

const headers = { 'Access-Control-Allow-Origin': '*' }

const kv = await Deno.openKv()

Deno.serve(async (req: Request) => {
    const url = new URL(req.url)
    const path = url.pathname.slice(1)
    if (path === '') {
        /**
         * @example
         * // 今日の祝日
         * https://holiday.deno.dev/
         */
        const today = new Date()
        // JSTに変換する。
        today.setHours(today.getHours() + 9)
        const holiday = await kv.get([today.getFullYear(), today.getMonth()+1, today.getDate()])
        return new Response(holiday.value, { headers })
    } else if (/^\d{4}-?\d{2}-?\d{2}$/.test(path)) {
        /**
         * @example
         * // 元旦
         * https://holiday.deno.dev/20240101
         * https://holiday.deno.dev/2024-01-01
         */
        const holiday = await kv.get(path.match(/(\d{4})-?(\d{2})-?(\d{2})/).slice(1).map(s => Number(s)))
        return new Response(holiday.value, { headers })
    } else if (['yesterday', 'tomorrow'].includes(path) || !isNaN(path)) {
        /**
         * @example
         * // 明日の祝日
         * https://holiday.deno.dev/tomorrow
         * // 昨日の祝日
         * https://holiday.deno.dev/yesterday
         * // 明後日の祝日
         * https://holiday.deno.dev/2
         * // 一昨日の祝日
         * https://holiday.deno.dev/-2
         */
        let delta
        if (path === 'yesterday') delta = -1
        else if (path === 'tomorrow') delta = 1
        else delta = Number(path)
        const date = new Date()
        date.setDate(date.getDate() + delta)
        // JSTに変換する。
        date.setHours(date.getHours() + 9)
        const holiday = await kv.get([date.getFullYear(), date.getMonth()+1, date.getDate()])
        return new Response(holiday.value, { headers })
    } else if (path === 'list.json') {
        /**
         * @example
         * // 全ての祝日
         * https://holiday.deno.dev/list.json
         * // 2024年の祝日
         * https://holiday.deno.dev/list.json?year=2024
         * // 2024年11月03日以降の祝日
         * https://holiday.deno.dev/list.json?from=2024-11-03
         * // 2023年11月03日以降の2023年の祝日
         * https://holiday.deno.dev/list.json?from=2023-11-03&year=2023
         * // 1955年01月15日以前の祝日
         * https://holiday.deno.dev/list.json?to=1955-01-15
         * // 1956年01月15日以前の1956年の祝日
         * https://holiday.deno.dev/list.json?to=1956-01-15&year=1956
         * // 2024年09月16日から2024年11月03日までの祝日
         * https://holiday.deno.dev/list.json?from=2024-09-16&to=2024-11-03
         * // 3件の祝日
         * https://holiday.deno.dev/list.json?limit=3
         * // 逆順の祝日
         * https://holiday.deno.dev/list.json?reverse
         */
        const params = { prefix: [] }
        const options = {}
        if (url.searchParams.has('year')) {
            params.prefix = [Number(url.searchParams.get('year'))]
        }
        if (url.searchParams.has('from') && url.searchParams.has('to')) {
            delete params.prefix
            params.start = url.searchParams.get('from').split('-').map(s=>Number(s))
            params.end = url.searchParams.get('to').split('-').map(s=>Number(s))
            /**
             * 指定した日を含める。
             * @see {@link https://docs.deno.com/deploy/kv/manual/operations#list}
             */
            params.end[2]++
        } else if (url.searchParams.has('from')) {
            params.start = url.searchParams.get('from').split('-').map(s=>Number(s))
        } else if (url.searchParams.has('to')) {
            params.end = url.searchParams.get('to').split('-').map(s=>Number(s))
            /**
             * 指定した日を含める。
             * @see {@link https://docs.deno.com/deploy/kv/manual/operations#list}
             */
            params.end[2]++
        }
        if (url.searchParams.has('limit')) {
            options.limit = Number(url.searchParams.get('limit'))
        }
        if (url.searchParams.has('reverse')) {
            options.reverse = true
        }
        const iter = kv.list<string>(params, options)
        const holidays = []
        for await (const res of iter) holidays.push(res)
        return Response.json(Object.fromEntries(holidays.map(({key: [year, month, day], value}) => [new Date(year, month - 1, day).toLocaleDateString('sv'), value])), { headers })
    } else if (path === 'kenall.json') {
        /**
         * 日本の祝日API
         * 
         * @example
         * // 全ての祝日
         * https://holiday.deno.dev/kenall.json
         * // 2024年の祝日
         * https://holiday.deno.dev/kenall.json?year=2024
         * // 2024年11月03日以降の祝日
         * https://holiday.deno.dev/kenall.json?from=2024-11-03
         * // 2023年11月03日以降の2023年の祝日
         * https://holiday.deno.dev/kenall.json?from=2023-11-03&year=2023
         * // 1955年01月15日以前の祝日
         * https://holiday.deno.dev/kenall.json?to=1955-01-15
         * // 1956年01月15日以前の1956年の祝日
         * https://holiday.deno.dev/kenall.json?to=1956-01-15&year=1956
         * // 2024年09月16日から2024年11月03日までの祝日
         * https://holiday.deno.dev/kenall.json?from=2024-09-16&to=2024-11-03
         * // 3件の祝日
         * https://holiday.deno.dev/kenall.json?limit=3
         * // 逆順の祝日
         * https://holiday.deno.dev/kenall.json?reverse
         * 
         * @see {@link https://kenall.jp/docs/API/holidays/}
         */
        const params = { prefix: [] }
        const options = {}
        if (url.searchParams.has('year')) {
            params.prefix = [Number(url.searchParams.get('year'))]
        }
        if (url.searchParams.has('from') && url.searchParams.has('to')) {
            delete params.prefix
            params.start = url.searchParams.get('from').split('-').map(s=>Number(s))
            params.end = url.searchParams.get('to').split('-').map(s=>Number(s))
            /**
             * 指定した日を含める。
             * @see {@link https://docs.deno.com/deploy/kv/manual/operations#list}
             */
            params.end[2]++
        } else if (url.searchParams.has('from')) {
            params.start = url.searchParams.get('from').split('-').map(s=>Number(s))
        } else if (url.searchParams.has('to')) {
            params.end = url.searchParams.get('to').split('-').map(s=>Number(s))
            /**
             * 指定した日を含める。
             * @see {@link https://docs.deno.com/deploy/kv/manual/operations#list}
             */
            params.end[2]++
        }
        if (url.searchParams.has('limit')) {
            options.limit = Number(url.searchParams.get('limit'))
        }
        if (url.searchParams.has('reverse')) {
            options.reverse = true
        }
        const iter = kv.list<string>(params, options)
        const holidays = []
        for await (const res of iter) holidays.push(res)
        return Response.json({
            data: holidays.map(({key: [year, month, day], value: title}) => {
                const date = new Date(year, month - 1, day)
                return {
                    title,
                    date: date.toLocaleDateString('sv'),
                    day_of_week: date.getDay(),
                    day_of_week_text: date.toLocaleString('en', {weekday:'long'}).toLowerCase()
                }
            })
        }, { headers })
    }
    return new Response('', { headers })
})

Deno.cron("sample cron", "0 0 * * *", async () => {
    /**
     * 「国民の祝日」について
     * @see {@link https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html#syukujitu}
     */
    const buffer = await fetch('https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv').then(r=>r.arrayBuffer())
    /**
     * Convert character encoding from CP932 to UNICODE.
     * @see {@link https://github.com/polygonplanet/encoding.js#example-usage}
     */
    const csv = Encoding.codeToString(Encoding.convert(new Uint8Array(buffer), { from: 'CP932', to: 'UNICODE' }))
    // 行ごとに分割し、ヘッダーを削除し、空行を削除し、列を分割する。
    const holidays = csv.split('\r\n').slice(1).filter(line=>line).map(line => line.split(','))
    
    for await (const [date, name] of holidays) {
        await kv.set(date.split('/').map(s=>Number(s)), name);
    }
})
```