bizdays-toolkit 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/README.md +209 -197
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/uk/index.d.ts +49 -0
- package/dist/uk/index.js +83 -0
- package/dist/uk/rules.d.ts +46 -0
- package/dist/uk/rules.js +165 -0
- package/package.json +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.0] - 2026-10-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 英国の bank holidays の提供元を `bizdays-toolkit/uk` に追加した。地域ごとに `england`(`GB-ENG`)・`wales`(`GB-WLS`)・`scotland`(`GB-SCT`)・`northernIreland`(`GB-NIR`)の 4 つで、タイムゾーンは `Europe/London`、対応年は 2012〜2099 年。2012 年から GOV.UK が公表している最後の年(2028 年)までのすべての日付と名称が、GOV.UK のデータ(現在の `https://www.gov.uk/bank-holidays.json` と、それより前の年は GOV.UK の公式リポジトリ alphagov/calendars・alphagov/frontend の履歴)と一致することをテストで確かめている。それより後の年は規則による予測(`confirmed: false`)。GOV.UK では England と Wales が 1 つの区分だが、別の提供元にした(日付は現在同じ)。Wales の祝日には、GOV.UK のウェールズ語版の名称を `names.cy` に付ける。種別(`kind`)は `"regular"`(毎年の祝日。布告で移動した年を含む)・`"substitute"`(振替)・`"special"`(布告による臨時の祝日)で、型は `UkHolidayKind`・`KnownUkHolidayKind`・`UkHoliday`
|
|
12
|
+
- `pnpm verify:govuk`(`scripts/verify-govuk.ts`)を追加した。GOV.UK の JSON とウェールズ語の名称(alphagov/frontend の翻訳ファイル)を取得し、ライブラリの計算結果・テスト用フィクスチャと照合する
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- 公式データの反映(法改正・官報公表・布告による祝日の新設・移動・臨時の祝日、春分・秋分の日の確定、GOV.UK の新しい年の公表)を、マイナー版ではなく**パッチ版**で公開する方針に改めた(RELEASING.md)。0.x の期間は `^0.8.0` が 0.9.0 を受け取らないため、マイナー版では臨時の祝日が利用者に届かない。パッチ版なら 0.x でも 1.0.0 以降でも、`^`・`~` のどちらの指定にも届く
|
|
17
|
+
- README を英語を既定にした(`README.md`)。日本語版は `docs/README.ja.md` に移し、それぞれの冒頭に切り替えのリンクを置いた。npm に公開するファイルは変わらない(日本語版はパッケージに含めない。npm はルートの `README*` を `files` の指定にかかわらず含めるため、`docs/` に置く)。README に書いている minify + gzip のサイズを測り直し、日本の祝日込みを約 8.5KB から約 9.0KB に直した(0.7.0 までの記載が実際より小さかった)。英国の祝日込みは約 9.0KB
|
|
18
|
+
|
|
19
|
+
### 開発
|
|
20
|
+
|
|
21
|
+
- `bizdays-toolkit/uk` のサイズの上限(gzip 6,700 バイト)を `size-limits.json` に追加した(エントリーの追加。計測は 6,225 バイト)。README の minify + gzip のサイズは、ルートのすべてと国の提供元(`jp`、または英国の 4 つ)を読み込む入口を esbuild で bundle・minify して gzip で測った(ルート 7,394 バイト、日本 8,953 バイト、英国 9,043 バイト)
|
|
22
|
+
- GOV.UK の bank holidays を毎日照合するワークフロー(`.github/workflows/govuk-watch.yml`)を追加した。差分があれば `pnpm verify:govuk` の結果と alphagov/frontend の該当ファイルの最新のコミットを本文にした issue(ラベル `govuk-data`)を立て、同じ issue が開いていれば内容が変わったときだけ本文を更新してコメントする。取得に失敗した場合も、監視が機能していないことを issue で知らせる。60 日間動きのないリポジトリで定期実行が止まらないよう、実行のたびにワークフローを有効化し直す。フィクスチャはワークフローでは更新せず、規則の修正と同じコミットで更新する
|
|
23
|
+
- CI に、ワークフローを actionlint(1.7.12。版と SHA-256 を固定した公式のリリースを取得)で検査するジョブを追加した。`run:` のシェルスクリプトもランナーの shellcheck で検査する。ワークフローの誤りは `pnpm check` では見つからないため
|
|
24
|
+
- CI・リリース・監視のワークフローの Node.js を 24.21.0 から 24.22.0 に、pnpm を 12.11.2 から 12.14.0 に上げた(手元の開発環境に合わせる)
|
|
25
|
+
- `pnpm check`(`scripts/check-package.ts`)で、`README.md` と `docs/README.ja.md` の見出しの数(段ごと)とコードブロックの数が一致し、互いへの切り替えのリンクがあることを確かめるようにした
|
|
26
|
+
- `pnpm check` が、パッケージの中身を `files` の指定からの推定ではなく、実際の `npm pack --dry-run --json` の結果で確かめるようにした(npm が指定にかかわらず含めるファイルを見落とさないため)。ルートに `README.ja.md` があれば失敗する
|
|
27
|
+
|
|
7
28
|
## [0.7.0] - 2026-10-16
|
|
8
29
|
|
|
9
30
|
### Added
|
|
@@ -158,7 +179,8 @@
|
|
|
158
179
|
|
|
159
180
|
- 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
|
|
160
181
|
|
|
161
|
-
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.
|
|
182
|
+
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.8.0...HEAD
|
|
183
|
+
[0.8.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.7.0...v0.8.0
|
|
162
184
|
[0.7.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.6.0...v0.7.0
|
|
163
185
|
[0.6.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.5.0...v0.6.0
|
|
164
186
|
[0.5.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.4.0...v0.5.0
|
package/README.md
CHANGED
|
@@ -14,31 +14,32 @@
|
|
|
14
14
|
<img src="https://img.shields.io/badge/types-included-blue" alt="TypeScript types included">
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
<p align="center"><b>English</b> | <a href="https://github.com/inohan77/bizdays-toolkit/blob/main/docs/README.ja.md">日本語</a></p>
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
- **繰り返しの規則**: 「第 3 水曜定休」「毎月末日は棚卸」「月曜休館(祝日の場合は翌平日)」のような規則を、祝日に当たったときの扱い(祝日に従う・祝日でも当てはめる・翌日に振り替える)とあわせて指定できます
|
|
21
|
-
- **日ごとの値(営業時間など)**: 「火曜定休、祝日は日曜の営業時間、元日は休業、大晦日は短縮営業」のように、曜日・祝日・特定日で変わる値を同じ規則で引けます
|
|
22
|
-
- **設定の診断**: `bizdays-toolkit/diagnose` の `diagnoseCalendar`・`diagnoseSchedule` が、決して使われない規則・取り除いても結果が変わらない規則・キーの打ち間違い・営業日が 1 日もない設定などを、例外を投げずに一覧で返します。テストに 1 行足せば、設定の変更で規則が死んでいないかを確かめられます
|
|
23
|
-
- **月の期日**: 「20 日締め翌月 10 日払い(休業日なら翌営業日)」「月末の最終営業日」のような期日を、金融で使われる 4 つの調整方法(following・preceding・modified-following・modified-preceding)で計算します
|
|
24
|
-
- **営業日の計算**: 定休日の曜日・毎年の休業日(年末年始のような年をまたぐ期間も可)・臨時休業日・臨時営業日と祝日を組み合わせ、「N 営業日後」「締め時刻を過ぎたら翌営業日」などを計算します
|
|
25
|
-
- **タイムゾーンを間違えない**: `Date` などの瞬間は、カレンダーのタイムゾーン(日本は `Asia/Tokyo`)で暦日に変換してから判定します。UTC で動くサーバーでも、日本時間 0〜9 時が前日扱いになりません
|
|
26
|
-
- **軽量・依存ゼロ**: minify + gzip で約 7.4KB(日本の祝日込みで約 8.5KB)。ESM・型定義同梱・`sideEffects: false` で、Node.js・ブラウザー・Edge ランタイムで動作します
|
|
27
|
-
- **他の国・地域に拡張できる**: 祝日の提供元はインターフェース(`HolidayProvider`)で差し替えられます。現在は日本のみ同梱しています
|
|
19
|
+
A business-day calendar that combines public holidays with company or store closures.
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
- **Japanese holidays verified day by day against the Cabinet Office data**: from 1955 through the latest year announced in the Official Gazette, every date in the Cabinet Office "National Holidays" CSV is checked in the tests (substitute holidays, citizens' holidays, holidays for imperial ceremonies and the Olympic moves included)
|
|
22
|
+
- **UK bank holidays verified day by day against GOV.UK**: choose England, Wales, Scotland or Northern Ireland. From 2012 through the last year GOV.UK has published, every date and name is checked against the GOV.UK data in the tests, and differences are watched daily
|
|
23
|
+
- **Recurring rules**: rules such as "closed on the 3rd Wednesday", "stocktaking on the last day of the month" or "closed on Mondays (on the next weekday if Monday is a holiday)", together with what happens when they fall on a holiday (follow the holiday, apply anyway, or move to the next day)
|
|
24
|
+
- **Day-by-day values (e.g. store hours)**: values that change by weekday, holiday and specific date, such as "closed on Tuesdays, Sunday hours on holidays, closed on New Year's Day, short hours on New Year's Eve", are looked up with the same rules
|
|
25
|
+
- **Option diagnostics**: `diagnoseCalendar` and `diagnoseSchedule` from `bizdays-toolkit/diagnose` list rules that can never apply, rules whose removal changes nothing, typos in option keys, calendars with no business day and more, without throwing. One line in your tests keeps a change of settings from silently killing a rule
|
|
26
|
+
- **Month-based due dates**: due dates such as "closing on the 20th, paid on the 10th of the next month (next business day if closed)" or "the last business day of the month", with the four adjustment conventions used in finance (following, preceding, modified-following, modified-preceding)
|
|
27
|
+
- **Business-day arithmetic**: closed weekdays, annual closures (including ranges across the new year), specific closed and open dates, and holidays combine to compute "N business days later", "next business day after the cut-off time" and more
|
|
28
|
+
- **Time-zone safe**: instants such as `Date` are converted to a civil date in the calendar's time zone (`Asia/Tokyo` for Japan, `Europe/London` for the UK) before evaluation. A server running in UTC does not treat 00:00–09:00 in Japan as the previous day
|
|
29
|
+
- **Lightweight, zero dependencies**: about 7.4 KB min+gzip (about 9.0 KB with Japanese holidays, about 9.0 KB with UK bank holidays). ESM, bundled types and `sideEffects: false`; runs on Node.js, browsers and edge runtimes
|
|
30
|
+
- **Extensible to other countries and regions**: holiday providers are pluggable through the `HolidayProvider` interface. Japan and the UK are bundled
|
|
30
31
|
|
|
31
|
-
##
|
|
32
|
+
## Installation
|
|
32
33
|
|
|
33
34
|
```sh
|
|
34
35
|
pnpm add bizdays-toolkit
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
Node.js 22.12
|
|
38
|
+
Node.js 22.12 or later. The package is ESM only, but Node.js 22.12 and later can also load it from CommonJS with `require()`.
|
|
38
39
|
|
|
39
|
-
##
|
|
40
|
+
## Usage
|
|
40
41
|
|
|
41
|
-
###
|
|
42
|
+
### Looking up holidays
|
|
42
43
|
|
|
43
44
|
```ts
|
|
44
45
|
import { getHoliday, holidaysInYear, isHoliday } from "bizdays-toolkit/jp";
|
|
@@ -48,15 +49,46 @@ getHoliday("2026-09-22");
|
|
|
48
49
|
// { date: "2026-09-22", name: "国民の休日", nameEn: "Citizens' Holiday", kind: "citizens", confirmed: true }
|
|
49
50
|
holidaysInYear(2027);
|
|
50
51
|
|
|
51
|
-
//
|
|
52
|
-
isHoliday(new Date("2026-11-22T15:30:00Z")); // true
|
|
52
|
+
// Instants (Date) are evaluated in Japan time
|
|
53
|
+
isHoliday(new Date("2026-11-22T15:30:00Z")); // true (Nov 23 in Japan time)
|
|
53
54
|
```
|
|
54
55
|
|
|
55
|
-
`kind`
|
|
56
|
+
`kind` is one of `national` (national holiday), `substitute` (substitute holiday), `citizens` (citizens' holiday, a day between two holidays) and `special` (holidays for imperial ceremonies and similar events).
|
|
56
57
|
|
|
57
|
-
`confirmed`
|
|
58
|
+
`confirmed` tells whether the date is officially fixed. The vernal and autumnal equinox days are fixed in the Official Gazette in February of the previous year, so later years are predictions from an approximation formula (`confirmed: false`).
|
|
58
59
|
|
|
59
|
-
###
|
|
60
|
+
### UK bank holidays
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { createCalendar } from "bizdays-toolkit";
|
|
64
|
+
import { england, northernIreland, scotland, wales } from "bizdays-toolkit/uk";
|
|
65
|
+
|
|
66
|
+
const edinburgh = createCalendar({ holidays: scotland, closedWeekdays: [0, 6] });
|
|
67
|
+
edinburgh.isBusinessDay("2026-01-02"); // false (2nd January)
|
|
68
|
+
edinburgh.addBusinessDays("2026-06-12", 1); // "2026-06-16" (June 15 is the World Cup bank holiday)
|
|
69
|
+
|
|
70
|
+
scotland.holidaysInYear(2026)[0];
|
|
71
|
+
// { date: "2026-01-01", name: "New Year’s Day", nameEn: "New Year’s Day", kind: "regular", confirmed: true }
|
|
72
|
+
wales.holidaysInYear(2026)[0]?.names; // { cy: "Dydd Calan" }
|
|
73
|
+
|
|
74
|
+
// Across several regions, use the union (a holiday in any of them is a holiday)
|
|
75
|
+
const ukWide = createCalendar({ holidays: [england, scotland, northernIreland] });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Provider | ID | GOV.UK division | Notes |
|
|
79
|
+
| ----------------- | -------- | ----------------- | ------------------------------------------------ |
|
|
80
|
+
| `england` | `GB-ENG` | England and Wales | |
|
|
81
|
+
| `wales` | `GB-WLS` | England and Wales | Welsh names (`names.cy`) included |
|
|
82
|
+
| `scotland` | `GB-SCT` | Scotland | 2nd January, St Andrew’s Day and others |
|
|
83
|
+
| `northernIreland` | `GB-NIR` | Northern Ireland | St Patrick’s Day, Battle of the Boyne and others |
|
|
84
|
+
|
|
85
|
+
- `kind` is one of `regular` (an annual bank holiday, including years when a proclamation moved it), `substitute` (a substitute day for a bank holiday on a Saturday or Sunday) and `special` (a one-off bank holiday by proclamation, such as a jubilee or a state funeral). A substitute day has the name of the original holiday, as on GOV.UK
|
|
86
|
+
- England and Wales are one division on GOV.UK and currently have the same dates, but they are separate providers, so your code keeps working if they ever differ
|
|
87
|
+
- Supported years are 2012–2099. Years after the last year GOV.UK has published (`confirmedThrough`, currently 2028) are predictions from the rules (`confirmed: false`). One-off bank holidays are unknown until announced, so predicted years never contain them
|
|
88
|
+
- Local holidays set by Scottish councils are not included. Whether a bank holiday is a day off depends on the employment contract and similar arrangements
|
|
89
|
+
- One-off bank holidays may be announced only about ten days ahead. Differences from GOV.UK are watched daily and released as patch versions
|
|
90
|
+
|
|
91
|
+
### Business-day calendars
|
|
60
92
|
|
|
61
93
|
```ts
|
|
62
94
|
import { createCalendar } from "bizdays-toolkit";
|
|
@@ -64,97 +96,102 @@ import { jp } from "bizdays-toolkit/jp";
|
|
|
64
96
|
|
|
65
97
|
const shipping = createCalendar({
|
|
66
98
|
holidays: jp,
|
|
67
|
-
closedWeekdays: [0], //
|
|
68
|
-
closedAnnual: [{ from: "12-31", to: "01-03", name: "
|
|
69
|
-
closedDates: [{ from: "2027-08-12", to: "2027-08-16", name: "
|
|
70
|
-
openDates: ["2026-12-27"], //
|
|
71
|
-
holidayPolicy: "closed", //
|
|
99
|
+
closedWeekdays: [0], // Sunday (0 = Sunday … 6 = Saturday)
|
|
100
|
+
closedAnnual: [{ from: "12-31", to: "01-03", name: "New Year closure" }],
|
|
101
|
+
closedDates: [{ from: "2027-08-12", to: "2027-08-16", name: "Summer closure" }],
|
|
102
|
+
openDates: ["2026-12-27"], // a specific open day
|
|
103
|
+
holidayPolicy: "closed", // closed on holidays ("open": open / "ignore": not used)
|
|
72
104
|
});
|
|
73
105
|
|
|
74
|
-
shipping.isBusinessDay("2026-11-23"); // false
|
|
106
|
+
shipping.isBusinessDay("2026-11-23"); // false (Labor Thanksgiving Day)
|
|
75
107
|
shipping.addBusinessDays("2026-12-30", 1); // "2027-01-04"
|
|
76
|
-
shipping.businessDaysBetween("2026-09-01", "2026-10-01"); // from
|
|
108
|
+
shipping.businessDaysBetween("2026-09-01", "2026-10-01"); // business days from (inclusive) to (exclusive)
|
|
77
109
|
|
|
78
|
-
// 14
|
|
110
|
+
// Orders before 14:00 ship today, later orders on the next business day
|
|
79
111
|
shipping.businessDayOnOrAfter(new Date(), { cutoff: "14:00" });
|
|
80
112
|
|
|
81
113
|
shipping.dayInfo("2027-01-01");
|
|
82
|
-
// { date: "2027-01-01", weekday: 5, businessDay: false, reason: "closed-annual", name: "
|
|
114
|
+
// { date: "2027-01-01", weekday: 5, businessDay: false, reason: "closed-annual", name: "New Year closure", holiday: {…} }
|
|
83
115
|
```
|
|
84
116
|
|
|
85
|
-
|
|
117
|
+
Each day is decided in this order of priority (top first).
|
|
86
118
|
|
|
87
|
-
1. `openDates
|
|
88
|
-
2. `closedDates
|
|
89
|
-
3. `closedAnnual
|
|
90
|
-
4. `openRecurring
|
|
91
|
-
5.
|
|
92
|
-
6. `openRecurring
|
|
93
|
-
7. `closedWeekdays
|
|
94
|
-
8.
|
|
119
|
+
1. `openDates` (specific open days)
|
|
120
|
+
2. `closedDates` (specific closed days)
|
|
121
|
+
3. `closedAnnual` (annual closures)
|
|
122
|
+
4. `openRecurring` and `closedRecurring` rules with `onHolidayRule: "over-holiday"` (applied even on holidays)
|
|
123
|
+
5. Holidays (according to `holidayPolicy`; `"open"` makes them business days even on closed weekdays)
|
|
124
|
+
6. `openRecurring` and `closedRecurring` (the default `"under-holiday"`, and the days moved to by `"after-holiday"`)
|
|
125
|
+
7. `closedWeekdays` (closed weekdays)
|
|
126
|
+
8. Otherwise a business day
|
|
95
127
|
|
|
96
|
-
|
|
128
|
+
This order is fixed. Only the relation between a recurring rule and holidays can be chosen per rule with `onHolidayRule` (next section).
|
|
97
129
|
|
|
98
|
-
###
|
|
130
|
+
### Recurring rules (weekly, nth weekday, day of month)
|
|
99
131
|
|
|
100
|
-
|
|
132
|
+
Recurring closed and open days such as "closed on the 3rd Wednesday", "closed on the 2nd and 4th Saturdays", "stocktaking on the last day of the month" or "closed on Mondays (on the next weekday if Monday is a holiday)" go in `closedRecurring` and `openRecurring`.
|
|
101
133
|
|
|
102
134
|
```ts
|
|
103
135
|
const museum = createCalendar({
|
|
104
136
|
holidays: jp,
|
|
105
|
-
holidayPolicy: "open", //
|
|
137
|
+
holidayPolicy: "open", // open on holidays
|
|
106
138
|
closedRecurring: [
|
|
107
|
-
{ weekday: 1, name: "
|
|
108
|
-
{
|
|
109
|
-
|
|
139
|
+
{ weekday: 1, name: "Closed on Mondays", onHolidayRule: "after-holiday" }, // closed on the next weekday instead
|
|
140
|
+
{
|
|
141
|
+
weekday: 3,
|
|
142
|
+
nth: 3,
|
|
143
|
+
months: [2, 8],
|
|
144
|
+
name: "Closed on the 3rd Wednesday in February and August",
|
|
145
|
+
},
|
|
146
|
+
{ day: -1, name: "Stocktaking", onHolidayRule: "over-holiday" }, // closed even on holidays
|
|
110
147
|
],
|
|
111
148
|
});
|
|
112
149
|
|
|
113
|
-
museum.isBusinessDay("2026-10-12"); // true
|
|
150
|
+
museum.isBusinessDay("2026-10-12"); // true (open on Monday, Sports Day)
|
|
114
151
|
museum.dayInfo("2026-10-13");
|
|
115
152
|
// { date: "2026-10-13", weekday: 2, businessDay: false, reason: "closed-recurring",
|
|
116
|
-
// name: "
|
|
153
|
+
// name: "Closed on Mondays", shiftedFrom: "2026-10-12" }
|
|
117
154
|
```
|
|
118
155
|
|
|
119
|
-
|
|
|
120
|
-
| ------------------------ |
|
|
121
|
-
| `{ weekday: 1 }` |
|
|
122
|
-
| `{ weekday: 3, nth: 3 }` |
|
|
123
|
-
| `{ day: 20 }` |
|
|
124
|
-
| `months: [8, 12]` |
|
|
156
|
+
| Pattern | Meaning |
|
|
157
|
+
| ------------------------ | ------------------------------------------------------------------------------------- |
|
|
158
|
+
| `{ weekday: 1 }` | Every Monday |
|
|
159
|
+
| `{ weekday: 3, nth: 3 }` | The 3rd Wednesday. `nth` is 1–5 or -1 (last); an array such as `[2, 4]` gives several |
|
|
160
|
+
| `{ day: 20 }` | The 20th of every month. `day` is 1–31 or -1 (last day); an array gives several |
|
|
161
|
+
| `months: [8, 12]` | Narrows any pattern to these months |
|
|
125
162
|
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
163
|
+
- **The nth weekday** is the nth occurrence of that weekday in the month (days 1–7 hold the 1st). It is not the weekday in the nth row of a printed calendar
|
|
164
|
+
- **Days that do not exist in a month** (`nth: 5` or `day: 31` in a month without one) do not match. Use `day: -1` for the last day
|
|
165
|
+
- Among recurring rules, the one written first in the array wins, and `openRecurring` wins over `closedRecurring`
|
|
129
166
|
|
|
130
|
-
|
|
167
|
+
**What happens on a holiday** (`onHolidayRule`) is chosen per rule.
|
|
131
168
|
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
| `"under-holiday"
|
|
135
|
-
| `"over-holiday"`
|
|
136
|
-
| `"after-holiday"`
|
|
169
|
+
| Value | Meaning | How a store would announce it |
|
|
170
|
+
| --------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
171
|
+
| `"under-holiday"` (default) | Under the holiday: follows the holiday handling (`holidayPolicy`) | "Closed on the 3rd Wednesday (open if it is a holiday)" |
|
|
172
|
+
| `"over-holiday"` | Over the holiday: applies even on holidays | "Closed on the 3rd Wednesday (holidays included)" |
|
|
173
|
+
| `"after-holiday"` | After the holiday: not applied on holidays, moved to the next non-holiday | "Closed on Mondays (on the next weekday if a holiday)" |
|
|
137
174
|
|
|
138
|
-
- `"under-holiday"`
|
|
139
|
-
- `"after-holiday"`
|
|
140
|
-
-
|
|
175
|
+
- With `holidayPolicy: "ignore"` (holidays not used), `"under-holiday"` rules also apply on holidays
|
|
176
|
+
- The day an `"after-holiday"` rule moves to is the first non-holiday on or after that day. After a run of holidays (such as Golden Week) it is the day after the run. Specific open or closed days and annual closures on that day take priority. `dayInfo()` for a moved day includes the original date as `shiftedFrom`
|
|
177
|
+
- The "holidays" used for moving are only those of the holiday provider (such as `jp`), not annual closures
|
|
141
178
|
|
|
142
|
-
|
|
179
|
+
In a schedule, the same patterns with a `value` go in `recurring`.
|
|
143
180
|
|
|
144
181
|
```ts
|
|
145
182
|
const hours = createSchedule<Hours>({
|
|
146
183
|
holidays: jp,
|
|
147
184
|
weekly: [sunday, weekday, weekday, weekday, weekday, weekday, weekday],
|
|
148
185
|
recurring: [
|
|
149
|
-
{ weekday: 1, value: null, name: "
|
|
150
|
-
{ day: -1, value: { open: "10:00", close: "17:00" }, name: "
|
|
186
|
+
{ weekday: 1, value: null, name: "Closed on Mondays", onHolidayRule: "after-holiday" },
|
|
187
|
+
{ day: -1, value: { open: "10:00", close: "17:00" }, name: "Short hours at month end" },
|
|
151
188
|
],
|
|
152
189
|
});
|
|
153
190
|
```
|
|
154
191
|
|
|
155
|
-
###
|
|
192
|
+
### Common recipes
|
|
156
193
|
|
|
157
|
-
|
|
194
|
+
**Treat citizens' holidays (days between two holidays) as business days**: wrap the holiday provider and filter by kind.
|
|
158
195
|
|
|
159
196
|
```ts
|
|
160
197
|
import { jp } from "bizdays-toolkit/jp";
|
|
@@ -166,7 +203,7 @@ const jpWithoutCitizensHoliday = {
|
|
|
166
203
|
const calendar = createCalendar({ holidays: jpWithoutCitizensHoliday, closedWeekdays: [0, 6] });
|
|
167
204
|
```
|
|
168
205
|
|
|
169
|
-
|
|
206
|
+
**Build per-store calendars from shared options**: options are plain objects, so spread and extend them.
|
|
170
207
|
|
|
171
208
|
```ts
|
|
172
209
|
const base = {
|
|
@@ -178,9 +215,9 @@ const shibuya = createCalendar({ ...base, openDates: ["2026-12-27"] });
|
|
|
178
215
|
const yokohama = createCalendar({ ...base, closedRecurring: [{ weekday: 3, nth: 3 }] });
|
|
179
216
|
```
|
|
180
217
|
|
|
181
|
-
###
|
|
218
|
+
### Month-based due dates and date adjustment
|
|
182
219
|
|
|
183
|
-
|
|
220
|
+
Dates that fall on closed days are adjusted to business days with the convention you choose. The nth business day of a month and due dates such as "the nth of every month (next business day if closed)" are also available.
|
|
184
221
|
|
|
185
222
|
```ts
|
|
186
223
|
import { closingMonth, createCalendar, monthOf } from "bizdays-toolkit";
|
|
@@ -189,113 +226,113 @@ import { jp } from "bizdays-toolkit/jp";
|
|
|
189
226
|
const calendar = createCalendar({
|
|
190
227
|
holidays: jp,
|
|
191
228
|
closedWeekdays: [0, 6],
|
|
192
|
-
closedAnnual: [{ from: "12-31", to: "01-03", name: "
|
|
229
|
+
closedAnnual: [{ from: "12-31", to: "01-03", name: "New Year closure" }],
|
|
193
230
|
});
|
|
194
231
|
|
|
195
|
-
calendar.adjust("2026-10-31", { convention: "following" }); // "2026-11-02"
|
|
196
|
-
calendar.adjust("2026-10-31", { convention: "modified-following" }); // "2026-10-30"
|
|
232
|
+
calendar.adjust("2026-10-31", { convention: "following" }); // "2026-11-02" (Oct 31 is a Saturday)
|
|
233
|
+
calendar.adjust("2026-10-31", { convention: "modified-following" }); // "2026-10-30" (the month would change, so backwards)
|
|
197
234
|
|
|
198
235
|
calendar.firstBusinessDayOfMonth("2027-01"); // "2027-01-04"
|
|
199
236
|
calendar.lastBusinessDayOfMonth("2026-12"); // "2026-12-30"
|
|
200
|
-
calendar.nthBusinessDayOfMonth("2026-10", 5); // "2026-10-07"
|
|
237
|
+
calendar.nthBusinessDayOfMonth("2026-10", 5); // "2026-10-07" (-1 is the last, -2 the second to last)
|
|
201
238
|
|
|
202
|
-
//
|
|
203
|
-
calendar.dueDate("2026-10", { day: 10, convention: "following" }); // "2026-10-13"
|
|
239
|
+
// Paid on the 10th of every month (next business day if closed)
|
|
240
|
+
calendar.dueDate("2026-10", { day: 10, convention: "following" }); // "2026-10-13" (Oct 12 is Sports Day)
|
|
204
241
|
|
|
205
|
-
//
|
|
206
|
-
const billing = closingMonth("2026-10-21", 20); // "2026-11"
|
|
242
|
+
// Closing on the 20th, paid at the end of the next month (previous business day if closed)
|
|
243
|
+
const billing = closingMonth("2026-10-21", 20); // "2026-11" (Oct 21 to Nov 20 belongs to November)
|
|
207
244
|
calendar.dueDate(monthOf(billing, 1), { day: -1, convention: "preceding" }); // "2026-12-30"
|
|
208
245
|
```
|
|
209
246
|
|
|
210
|
-
|
|
247
|
+
**Adjustment conventions** (`convention`, required)
|
|
211
248
|
|
|
212
|
-
|
|
|
213
|
-
| ---------------------- |
|
|
214
|
-
| `"following"` |
|
|
215
|
-
| `"preceding"` |
|
|
216
|
-
| `"modified-following"` |
|
|
217
|
-
| `"modified-preceding"` |
|
|
249
|
+
| Convention | On a closed day |
|
|
250
|
+
| ---------------------- | -------------------------------------------------------------------- |
|
|
251
|
+
| `"following"` | The next business day |
|
|
252
|
+
| `"preceding"` | The previous business day |
|
|
253
|
+
| `"modified-following"` | The next business day, or the previous one if the month would change |
|
|
254
|
+
| `"modified-preceding"` | The previous business day, or the next one if the month would change |
|
|
218
255
|
|
|
219
|
-
-
|
|
220
|
-
-
|
|
221
|
-
- `modified
|
|
222
|
-
-
|
|
256
|
+
- On a business day, every convention returns the same day
|
|
257
|
+
- Whether the month changes is judged against **the month of the date before adjustment**. For `dueDate` it is the date after mapping to the last day of the month (for example, payment on the 31st in February uses the last day of February, and `"modified-following"` adjusts within February)
|
|
258
|
+
- With `modified-…`, if the month has no business day in the opposite direction either (the whole month is closed), the date is adjusted in the original direction even though the month changes
|
|
259
|
+
- There is no "nearest business day" (nearest), because the tie-breaking rule for equal distances differs by industry
|
|
223
260
|
|
|
224
|
-
|
|
261
|
+
**Month helpers** (root functions; business days are not considered)
|
|
225
262
|
|
|
226
|
-
- `monthOf(dateOrMonth, offset?)`:
|
|
227
|
-
- `closingMonth(date, closingDay)`:
|
|
263
|
+
- `monthOf(dateOrMonth, offset?)`: the month `offset` months after (before, if negative) a date or a month `"YYYY-MM"`
|
|
264
|
+
- `closingMonth(date, closingDay)`: the month a date belongs to when months are closed on `closingDay` (1–31, `-1` for the last day). The day after a closing day up to the next closing day belongs to the month of the next closing day. If the closing day does not exist in a month (closing on the 31st in February), the last day is used
|
|
228
265
|
|
|
229
|
-
`
|
|
266
|
+
The `day` of `dueDate` also maps days that do not exist in a month (such as February 30) to the last day. Recurring rules (`{ day: 31 }`) do not match such days, so the two behave differently: a due date must always fall somewhere in its month.
|
|
230
267
|
|
|
231
|
-
###
|
|
268
|
+
### Day-by-day values (e.g. store hours)
|
|
232
269
|
|
|
233
|
-
`createSchedule`
|
|
270
|
+
`createSchedule` returns any value per day from weekday, holiday and specific-date rules. The value type is up to you, so it also fits booking slots, delivery fee classes and more.
|
|
234
271
|
|
|
235
272
|
```ts
|
|
236
273
|
import { createSchedule } from "bizdays-toolkit";
|
|
237
274
|
import { jp } from "bizdays-toolkit/jp";
|
|
238
275
|
|
|
239
|
-
type Hours = { open: string; close: string } | null; // null
|
|
276
|
+
type Hours = { open: string; close: string } | null; // null means closed
|
|
240
277
|
const weekday = { open: "10:00", close: "21:00" };
|
|
241
278
|
const sunday = { open: "10:00", close: "19:00" };
|
|
242
279
|
|
|
243
280
|
const storeHours = createSchedule<Hours>({
|
|
244
281
|
holidays: jp,
|
|
245
|
-
weekly: [sunday, weekday, null, weekday, weekday, weekday, weekday], // [
|
|
246
|
-
holiday: { sameAs: 0 }, //
|
|
247
|
-
annual: [{ date: "01-01", value: null, name: "
|
|
248
|
-
dates: [{ date: "2026-12-31", value: { open: "10:00", close: "17:00" }, name: "
|
|
282
|
+
weekly: [sunday, weekday, null, weekday, weekday, weekday, weekday], // [Sun, Mon, …, Sat]; closed on Tuesdays
|
|
283
|
+
holiday: { sameAs: 0 }, // Sunday hours on holidays ("weekday": by weekday / { value: … }: a fixed value)
|
|
284
|
+
annual: [{ date: "01-01", value: null, name: "Closed on New Year's Day" }], // annual dates (ranges too)
|
|
285
|
+
dates: [{ date: "2026-12-31", value: { open: "10:00", close: "17:00" }, name: "New Year's Eve" }],
|
|
249
286
|
});
|
|
250
287
|
|
|
251
|
-
storeHours.valueOn("2026-11-03"); // { open: "10:00", close: "19:00" }
|
|
288
|
+
storeHours.valueOn("2026-11-03"); // { open: "10:00", close: "19:00" } (Culture Day uses Sunday hours)
|
|
252
289
|
storeHours.dayInfo("2027-01-01");
|
|
253
|
-
// { date: "2027-01-01", weekday: 5, value: null, reason: "annual", name: "
|
|
290
|
+
// { date: "2027-01-01", weekday: 5, value: null, reason: "annual", name: "Closed on New Year's Day", holiday: {…} }
|
|
254
291
|
```
|
|
255
292
|
|
|
256
|
-
|
|
293
|
+
Values are decided in the order `dates` (specific dates and ranges) → `annual` (annual dates and ranges) → `recurring` (`"over-holiday"`) → holidays → `recurring` (default and moved days) → `weekly` (weekday). When rules of the same type overlap, the one written first in the array wins.
|
|
257
294
|
|
|
258
|
-
|
|
295
|
+
Checking a time range such as "is the store open now" is left to your app, because industries differ (opening past midnight, breaks and so on). With `civilOf` you get the civil date and time in the time zone, and the check is short.
|
|
259
296
|
|
|
260
297
|
```ts
|
|
261
|
-
const { date, time } = storeHours.civilOf(new Date()); // { date: "2026-10-14", time: "16:30" }
|
|
298
|
+
const { date, time } = storeHours.civilOf(new Date()); // { date: "2026-10-14", time: "16:30" } (Japan time)
|
|
262
299
|
const hours = storeHours.valueOn(date);
|
|
263
300
|
const isOpen = hours !== null && time !== null && hours.open <= time && time < hours.close;
|
|
264
301
|
```
|
|
265
302
|
|
|
266
|
-
`civilOf`
|
|
303
|
+
`civilOf` is also on business-day calendars, and the same conversion is available as `toJapanDateTime` in `bizdays-toolkit/jp` and `toDateTimeParts(input, timeZone)` in the root.
|
|
267
304
|
|
|
268
|
-
###
|
|
305
|
+
### Date and date-time inputs
|
|
269
306
|
|
|
270
|
-
|
|
307
|
+
Every method accepts the following inputs. Returned dates are always `"YYYY-MM-DD"` strings.
|
|
271
308
|
|
|
272
|
-
|
|
|
273
|
-
| ---------------------------------------------------- |
|
|
274
|
-
| `"2026-11-23"` |
|
|
275
|
-
| `"2026-11-23T10:00"` |
|
|
276
|
-
| `"2026-11-22T15:30:00Z"
|
|
277
|
-
| `Date` |
|
|
278
|
-
| Temporal
|
|
279
|
-
| Temporal
|
|
309
|
+
| Input | Treated as |
|
|
310
|
+
| ---------------------------------------------------- | ----------------------------------------------------------------- |
|
|
311
|
+
| `"2026-11-23"` | A civil date |
|
|
312
|
+
| `"2026-11-23T10:00"` | A date and time in the calendar's time zone (wall clock) |
|
|
313
|
+
| `"2026-11-22T15:30:00Z"`, `"2026-11-23T00:30+09:00"` | An instant, converted to a civil date in the calendar's time zone |
|
|
314
|
+
| `Date` | An instant |
|
|
315
|
+
| Temporal `PlainDate`, `PlainDateTime` | A civil date or date and time (no dependency on Temporal) |
|
|
316
|
+
| Temporal `Instant`, `ZonedDateTime` | An instant |
|
|
280
317
|
|
|
281
|
-
|
|
318
|
+
The calendar's time zone is the `timeZone` option, then the holiday provider's time zone, then `"UTC"`.
|
|
282
319
|
|
|
283
|
-
###
|
|
320
|
+
### Holidays of other countries and regions
|
|
284
321
|
|
|
285
|
-
`HolidayProvider`
|
|
322
|
+
Implement `HolidayProvider` and pass it as `holidays`. Several providers give their union (if their time zones differ, `timeZone` is required).
|
|
286
323
|
|
|
287
324
|
```ts
|
|
288
325
|
import { createCalendar, type HolidayProvider } from "bizdays-toolkit";
|
|
289
326
|
|
|
290
327
|
const myCountry: HolidayProvider = {
|
|
291
328
|
id: "XX",
|
|
292
|
-
timeZone: "Europe/
|
|
329
|
+
timeZone: "Europe/Paris",
|
|
293
330
|
supportedYears: { from: 2000, to: 2100 },
|
|
294
331
|
dataVersion: "2026-10",
|
|
295
332
|
holidaysInYear: (year) => [
|
|
296
333
|
{
|
|
297
334
|
date: `${year}-12-25`,
|
|
298
|
-
name: "
|
|
335
|
+
name: "Noël",
|
|
299
336
|
nameEn: "Christmas Day",
|
|
300
337
|
kind: "public",
|
|
301
338
|
confirmed: true,
|
|
@@ -305,12 +342,12 @@ const myCountry: HolidayProvider = {
|
|
|
305
342
|
const calendar = createCalendar({ holidays: myCountry, closedWeekdays: [0, 6] });
|
|
306
343
|
```
|
|
307
344
|
|
|
308
|
-
|
|
345
|
+
Holidays can carry names in other languages in `names`. The keys are canonical BCP 47 language tags (`"zh-Hant"`, `"ko"` and so on). The bundled Japanese holidays have no `names` (only the officially sourced Japanese and English names are bundled); among the UK providers, only Wales has Welsh names. If you need more, wrap the provider.
|
|
309
346
|
|
|
310
347
|
```ts
|
|
311
348
|
import { jp } from "bizdays-toolkit/jp";
|
|
312
349
|
|
|
313
|
-
const zhHant: Record<string, string> = { "2026-01-01": "元日" /* …
|
|
350
|
+
const zhHant: Record<string, string> = { "2026-01-01": "元日" /* … your own translations */ };
|
|
314
351
|
const jpWithNames = {
|
|
315
352
|
...jp,
|
|
316
353
|
holidaysInYear: (year: number) =>
|
|
@@ -322,13 +359,13 @@ const jpWithNames = {
|
|
|
322
359
|
createCalendar({ holidays: jpWithNames }).getHoliday("2026-01-01")?.names; // { "zh-Hant": "元日" }
|
|
323
360
|
```
|
|
324
361
|
|
|
325
|
-
###
|
|
362
|
+
### Outside the supported years
|
|
326
363
|
|
|
327
|
-
|
|
364
|
+
Evaluating a date outside the holiday provider's supported years (1955–2099 for Japan, 2012–2099 for the UK) throws a `RangeError`. To treat such dates as having no holidays, set `outOfRange: "ignore"`.
|
|
328
365
|
|
|
329
|
-
###
|
|
366
|
+
### Errors
|
|
330
367
|
|
|
331
|
-
|
|
368
|
+
Invalid options and inputs throw ordinary exceptions such as `RangeError` and `TypeError`, with a `code`. Messages may change, so branch on `code`.
|
|
332
369
|
|
|
333
370
|
```ts
|
|
334
371
|
import { createCalendar, isBizdaysError } from "bizdays-toolkit";
|
|
@@ -337,16 +374,16 @@ try {
|
|
|
337
374
|
createCalendar({ closedDates: ["2026-12-32"] });
|
|
338
375
|
} catch (error) {
|
|
339
376
|
if (isBizdaysError(error) && error.code === "invalid-date") {
|
|
340
|
-
//
|
|
377
|
+
// An invalid date (the message includes the location in the options, "closedDates[0].date")
|
|
341
378
|
}
|
|
342
379
|
}
|
|
343
380
|
```
|
|
344
381
|
|
|
345
|
-
|
|
382
|
+
Main codes: `invalid-date`, `invalid-month`, `invalid-month-day`, `invalid-time`, `invalid-range`, `invalid-weekday`, `invalid-time-zone`, `invalid-option`, `out-of-range`, `invalid-provider`, `invalid-provider-data`, `no-business-day`. Future versions may add codes.
|
|
346
383
|
|
|
347
|
-
###
|
|
384
|
+
### Option diagnostics
|
|
348
385
|
|
|
349
|
-
`bizdays-toolkit/diagnose`
|
|
386
|
+
`diagnoseCalendar` and `diagnoseSchedule` from `bizdays-toolkit/diagnose` examine options and return a list of problems. They never throw (an empty array means no problems).
|
|
350
387
|
|
|
351
388
|
```ts
|
|
352
389
|
import { diagnoseCalendar } from "bizdays-toolkit/diagnose";
|
|
@@ -356,12 +393,12 @@ diagnoseCalendar({
|
|
|
356
393
|
holidays: jp,
|
|
357
394
|
closedWeekdays: [0],
|
|
358
395
|
closedRecurring: [
|
|
359
|
-
{ weekday: 3, nth: 3, name: "
|
|
360
|
-
{ weekday: 3, nth: 3, months: [8], name: "
|
|
361
|
-
{ weekday: 0, name: "
|
|
396
|
+
{ weekday: 3, nth: 3, name: "Closed on the 3rd Wednesday" },
|
|
397
|
+
{ weekday: 3, nth: 3, months: [8], name: "3rd Wednesday in August" }, // covered by the previous rule
|
|
398
|
+
{ weekday: 0, name: "Closed on Sundays" }, // same as the closed weekday
|
|
362
399
|
],
|
|
363
|
-
closedDates: ["2100-01-05"], //
|
|
364
|
-
closeAnnual: ["12-31"], //
|
|
400
|
+
closedDates: ["2100-01-05"], // outside the supported years of Japanese holidays (1955–2099)
|
|
401
|
+
closeAnnual: ["12-31"], // a typo
|
|
365
402
|
});
|
|
366
403
|
// [
|
|
367
404
|
// { severity: "warning", code: "unknown-option", path: "closeAnnual", message: '… (did you mean "closedAnnual"?)' },
|
|
@@ -371,25 +408,25 @@ diagnoseCalendar({
|
|
|
371
408
|
// ]
|
|
372
409
|
```
|
|
373
410
|
|
|
374
|
-
|
|
|
375
|
-
|
|
|
376
|
-
| `error`
|
|
377
|
-
| `error`
|
|
378
|
-
| `warning`
|
|
379
|
-
| `warning`
|
|
380
|
-
| `warning`
|
|
381
|
-
| `warning`
|
|
382
|
-
| `warning`
|
|
383
|
-
| `info`
|
|
384
|
-
|
|
385
|
-
-
|
|
386
|
-
- `error`
|
|
387
|
-
- `shadowed`
|
|
388
|
-
-
|
|
389
|
-
- `redundant`
|
|
390
|
-
-
|
|
391
|
-
|
|
392
|
-
|
|
411
|
+
| `severity` | `code` | Meaning |
|
|
412
|
+
| ---------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
413
|
+
| `error` | The same codes as exceptions | Errors that make `createCalendar` or `createSchedule` throw. Creation stops at the first one; diagnostics return them all |
|
|
414
|
+
| `error` | `invalid-provider-data` | Errors in the data returned by a holiday provider. Creation checks only the years used; diagnostics check every supported year |
|
|
415
|
+
| `warning` | `unknown-option` | Unknown keys (ignored at creation). If a close known key exists, `message` suggests it |
|
|
416
|
+
| `warning` | `shadowed` | A rule fully covered by an earlier rule of the same type (`relatedPath`) and never used |
|
|
417
|
+
| `warning` | `never-matches` | A recurring rule that matches no day (such as `{ day: 30, months: [2] }`) |
|
|
418
|
+
| `warning` | `out-of-range` | A specific date outside the supported years of the holiday provider (`relatedPath`) |
|
|
419
|
+
| `warning` | `no-business-day` | No business day at all (calendars only; searching for a business day throws) |
|
|
420
|
+
| `info` | `redundant` | A rule whose removal changes nothing (covered by a combination of earlier rules, same as a closed weekday, the same value as the weekday in a schedule, and so on) |
|
|
421
|
+
|
|
422
|
+
- Branch on `code`. `message` is in English and its wording may change. Future versions may add codes
|
|
423
|
+
- When there is an `error`, the rule relation checks (`shadowed`, `redundant` and so on) are skipped
|
|
424
|
+
- `shadowed` compares only rules of the same type (specific dates, annual dates, recurring rules). The order of evaluation is fixed, so rules unused across types are found as `redundant`. Specific open days (`openDates`) and specific closed days (`closedDates`) are of the same type, and a closed day covered by an earlier open day is `shadowed`
|
|
425
|
+
- `shadowed` for recurring rules also considers `onHolidayRule`. For example, an `"under-holiday"` rule does not cover an `"over-holiday"` rule on the same days (the later rule applies on holidays)
|
|
426
|
+
- `redundant` compares the results with and without the rule day by day. Specific dates are checked over their range; annual dates and recurring rules are checked, with specific dates removed, over the holiday provider's supported years (1955–2099 for Japan), or 2001–2028 without a provider (28 years cover every combination of weekday and date). Schedule values are compared by content for arrays and plain objects, and by identity otherwise (such as `Date`)
|
|
427
|
+
- With Japanese holidays, checking every day takes a few hundred milliseconds. Use it in tests or at startup, not per request. As a development and test tool, it lives in a subpath so it stays out of your app's main bundle
|
|
428
|
+
|
|
429
|
+
Keeping options safe with a test:
|
|
393
430
|
|
|
394
431
|
```ts
|
|
395
432
|
import assert from "node:assert/strict";
|
|
@@ -397,54 +434,29 @@ import { test } from "node:test";
|
|
|
397
434
|
import { diagnoseCalendar } from "bizdays-toolkit/diagnose";
|
|
398
435
|
import { shippingOptions } from "../src/calendar.ts";
|
|
399
436
|
|
|
400
|
-
test("
|
|
437
|
+
test("the shipping calendar options have no problems", () => {
|
|
401
438
|
assert.deepEqual(diagnoseCalendar(shippingOptions), []);
|
|
402
439
|
});
|
|
403
440
|
```
|
|
404
441
|
|
|
405
|
-
##
|
|
442
|
+
## Versioning and data updates
|
|
406
443
|
|
|
407
|
-
[Semantic Versioning](https://semver.org/
|
|
444
|
+
Versioning follows [Semantic Versioning](https://semver.org/). See [RELEASING.md](https://github.com/inohan77/bizdays-toolkit/blob/main/RELEASING.md) (Japanese) for details.
|
|
408
445
|
|
|
409
|
-
-
|
|
410
|
-
-
|
|
411
|
-
-
|
|
412
|
-
- 1.0.0
|
|
413
|
-
-
|
|
446
|
+
- Official data updates (new, moved and one-off holidays by law, Official Gazette or proclamation, fixed equinox days in Japan, newly published years on GOV.UK) are released as **patch** versions, so they reach you with either `^` or `~`
|
|
447
|
+
- To freeze the holiday results, pin an exact version (for example `"bizdays-toolkit": "0.8.0"`)
|
|
448
|
+
- The reflected data is shown by `dataVersion` (for Japan, the year and month of the Official Gazette reflected; for the UK, the year and month of the GOV.UK data reflected) and `confirmedThrough`
|
|
449
|
+
- Before 1.0.0, minor versions may contain breaking changes
|
|
450
|
+
- Decision reasons (`reason`), holiday kinds (`kind`), error codes and diagnostic codes may gain values in future versions. When you branch on their values, handle "anything else" too (see "互換性の約束", the compatibility promise, in [RELEASING.md](https://github.com/inohan77/bizdays-toolkit/blob/main/RELEASING.md))
|
|
414
451
|
|
|
415
|
-
##
|
|
452
|
+
## Data sources
|
|
416
453
|
|
|
417
|
-
|
|
454
|
+
Japanese holidays are verified with the "National Holidays" CSV published on the Cabinet Office website (<https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv>) (as a test fixture only; not included in the package).
|
|
418
455
|
|
|
419
|
-
|
|
456
|
+
UK bank holidays are verified with the GOV.UK bank holidays data (<https://www.gov.uk/bank-holidays.json>, and for earlier years the history of the official GOV.UK repositories [alphagov/calendars](https://github.com/alphagov/calendars) and [alphagov/frontend](https://github.com/alphagov/frontend)), and the Welsh names of the Wales holidays with the Welsh version of GOV.UK (the verification data is a test fixture only; not included in the package).
|
|
420
457
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
---
|
|
458
|
+
Contains public sector information licensed under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).
|
|
424
459
|
|
|
425
|
-
##
|
|
426
|
-
|
|
427
|
-
A business-day calendar that combines public holidays with company or store closures.
|
|
460
|
+
## License
|
|
428
461
|
|
|
429
|
-
|
|
430
|
-
- **Day-by-day schedules**: `createSchedule` returns any value per day (e.g. store hours) from weekly values, a holiday rule (`"weekday"`, `{ sameAs: 0 }`, or `{ value }`), annual dates and specific dates
|
|
431
|
-
- **Recurring rules**: weekly, nth-weekday-of-month (`{ weekday: 3, nth: 3 }`) and day-of-month (`{ day: -1 }`) rules, with per-rule holiday handling (`onHolidayRule`: `"under-holiday"`, `"over-holiday"`, or `"after-holiday"` to move the rule to the next non-holiday, e.g. "closed on Mondays; if Monday is a holiday, closed on the next weekday")
|
|
432
|
-
- **Option diagnostics**: `diagnoseCalendar` / `diagnoseSchedule` from `bizdays-toolkit/diagnose` return a list of problems without throwing: rules that can never apply (`shadowed`, `never-matches`), rules whose removal changes nothing (`redundant`), typos in option keys with a suggestion (`unknown-option`), dates outside the holiday provider's years (`out-of-range`), and calendars with no business day. Use `code` to branch; `message` is for humans
|
|
433
|
-
- **Month-based due dates**: `adjust` with `following` / `preceding` / `modified-following` / `modified-preceding`, the nth / first / last business day of a month, `dueDate(month, { day, convention })`, and the calendar helpers `closingMonth` and `monthOf`
|
|
434
|
-
- **Business-day arithmetic**: closed weekdays, annual closures (including ranges across the new year), specific closed/open dates, holiday policy, `addBusinessDays`, cut-off times, and more
|
|
435
|
-
- **Time-zone safe**: instants (`Date`, ISO strings with offsets, Temporal) are converted to a civil date in the calendar's time zone before evaluation
|
|
436
|
-
- **Lightweight, zero dependencies**: about 7.4 KB min+gzip (about 8.5 KB with Japanese holidays), ESM, bundled types, `sideEffects: false`
|
|
437
|
-
- **Extensible** through the `HolidayProvider` interface (only Japan is bundled for now). Holidays can carry names in other languages via the optional `names` field (keys are canonical BCP 47 language tags); the bundled Japanese data includes only the officially sourced Japanese and English names
|
|
438
|
-
|
|
439
|
-
```ts
|
|
440
|
-
import { createCalendar } from "bizdays-toolkit";
|
|
441
|
-
import { jp, isHoliday } from "bizdays-toolkit/jp";
|
|
442
|
-
|
|
443
|
-
isHoliday("2026-11-23"); // true
|
|
444
|
-
|
|
445
|
-
const office = createCalendar({ holidays: jp, closedWeekdays: [0, 6] });
|
|
446
|
-
office.addBusinessDays("2026-09-18", 1); // "2026-09-24"
|
|
447
|
-
office.businessDayOnOrAfter(new Date(), { cutoff: "14:00" });
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
Versioning follows SemVer. Updates that reflect law amendments or official announcements are released as **minor** versions; pin an exact version if you need frozen results. Before 1.0.0, minor versions may contain breaking changes. See [RELEASING.md](./RELEASING.md) (Japanese) for details.
|
|
462
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 祝日と、会社・店舗の休業日を組み合わせた営業日カレンダー(createCalendar)と、
|
|
3
3
|
* 曜日・祝日・特定日で変わる値(営業時間など)のスケジュール(createSchedule)。
|
|
4
4
|
*
|
|
5
|
-
* 国ごとの祝日はサブパスから読み込む(例: "bizdays-toolkit/jp")。
|
|
5
|
+
* 国ごとの祝日はサブパスから読み込む(例: "bizdays-toolkit/jp"・"bizdays-toolkit/uk")。
|
|
6
6
|
*
|
|
7
7
|
* 公開する API には TSDoc の @public(安定)または @beta(マイナー版で変わりうる)を付ける。
|
|
8
8
|
* 公開する名前と型は etc/api.md に記録し、pnpm check で意図しない変化を検出する。
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 祝日と、会社・店舗の休業日を組み合わせた営業日カレンダー(createCalendar)と、
|
|
3
3
|
* 曜日・祝日・特定日で変わる値(営業時間など)のスケジュール(createSchedule)。
|
|
4
4
|
*
|
|
5
|
-
* 国ごとの祝日はサブパスから読み込む(例: "bizdays-toolkit/jp")。
|
|
5
|
+
* 国ごとの祝日はサブパスから読み込む(例: "bizdays-toolkit/jp"・"bizdays-toolkit/uk")。
|
|
6
6
|
*
|
|
7
7
|
* 公開する API には TSDoc の @public(安定)または @beta(マイナー版で変わりうる)を付ける。
|
|
8
8
|
* 公開する名前と型は etc/api.md に記録し、pnpm check で意図しない変化を検出する。
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 英国の bank holidays(England・Wales・Scotland・Northern Ireland)。
|
|
3
|
+
*
|
|
4
|
+
* 地域ごとに提供元が分かれている。GOV.UK では England と Wales が 1 つの区分(England and Wales)だが、
|
|
5
|
+
* このライブラリでは別の提供元にしている(日付は現在同じ。将来分かれても利用者のコードを変えずに済むように)。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { createCalendar } from "bizdays-toolkit";
|
|
9
|
+
* import { scotland } from "bizdays-toolkit/uk";
|
|
10
|
+
* const calendar = createCalendar({ holidays: scotland, closedWeekdays: [0, 6] });
|
|
11
|
+
*/
|
|
12
|
+
import type { Holiday, HolidayProvider } from "../core/types.ts";
|
|
13
|
+
import { type UkHolidayKind } from "./rules.ts";
|
|
14
|
+
export type { KnownUkHolidayKind, UkHolidayKind } from "./rules.ts";
|
|
15
|
+
/**
|
|
16
|
+
* 英国の祝日。
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export type UkHoliday = Holiday<UkHolidayKind>;
|
|
20
|
+
/**
|
|
21
|
+
* England の bank holidays の提供元(GOV.UK の England and Wales)。
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export declare const england: HolidayProvider<UkHolidayKind>;
|
|
25
|
+
/**
|
|
26
|
+
* Wales の bank holidays の提供元(GOV.UK の England and Wales)。名称にウェールズ語(names.cy)が付く。
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export declare const wales: HolidayProvider<UkHolidayKind>;
|
|
30
|
+
/**
|
|
31
|
+
* Scotland の bank holidays の提供元。
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
export declare const scotland: HolidayProvider<UkHolidayKind>;
|
|
35
|
+
/**
|
|
36
|
+
* Northern Ireland の bank holidays の提供元。
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export declare const northernIreland: HolidayProvider<UkHolidayKind>;
|
|
40
|
+
/**
|
|
41
|
+
* 反映済みの GOV.UK のデータの年月。
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
export declare const dataVersion: string;
|
|
45
|
+
/**
|
|
46
|
+
* GOV.UK が日付を公表している最後の年(それより後は規則による予測で confirmed: false)。
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
export declare const confirmedThrough: number;
|
package/dist/uk/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 英国の bank holidays(England・Wales・Scotland・Northern Ireland)。
|
|
3
|
+
*
|
|
4
|
+
* 地域ごとに提供元が分かれている。GOV.UK では England と Wales が 1 つの区分(England and Wales)だが、
|
|
5
|
+
* このライブラリでは別の提供元にしている(日付は現在同じ。将来分かれても利用者のコードを変えずに済むように)。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { createCalendar } from "bizdays-toolkit";
|
|
9
|
+
* import { scotland } from "bizdays-toolkit/uk";
|
|
10
|
+
* const calendar = createCalendar({ holidays: scotland, closedWeekdays: [0, 6] });
|
|
11
|
+
*/
|
|
12
|
+
import { CONFIRMED_THROUGH, DATA_VERSION, SUPPORTED_YEARS, computeUkHolidays, } from "./rules.js";
|
|
13
|
+
/** Wales の祝日のウェールズ語の名称(GOV.UK のウェールズ語版の表記) */
|
|
14
|
+
const WELSH_NAMES = {
|
|
15
|
+
"New Year’s Day": "Dydd Calan",
|
|
16
|
+
"Good Friday": "Gwener y Groglith",
|
|
17
|
+
"Easter Monday": "Llun y Pasg",
|
|
18
|
+
"Early May bank holiday": "Gŵyl banc dechrau Mai",
|
|
19
|
+
"Early May bank holiday (VE day)": "Gŵyl banc dechrau Mai (diwrnod VE)",
|
|
20
|
+
"Spring bank holiday": "Gŵyl banc y gwanwyn",
|
|
21
|
+
"Summer bank holiday": "Gŵyl banc yr haf",
|
|
22
|
+
"Christmas Day": "Dydd Nadolig",
|
|
23
|
+
"Boxing Day": "Dydd San Steffan",
|
|
24
|
+
"Queen’s Diamond Jubilee": "Jiwbilî Ddiemwnt y Frenhines",
|
|
25
|
+
"Platinum Jubilee bank holiday": "Gŵyl banc Jiwbilî Blatinwm",
|
|
26
|
+
"Bank Holiday for the State Funeral of Queen Elizabeth II": "Gŵyl y Banc ar gyfer Angladd Gwladol y Frenhines Elizabeth II",
|
|
27
|
+
"Bank holiday for the coronation of King Charles III": "Gŵyl y banc ar gyfer coroni Brenin Siarl III",
|
|
28
|
+
};
|
|
29
|
+
function withWelsh(list) {
|
|
30
|
+
return list.map((h) => {
|
|
31
|
+
const cy = WELSH_NAMES[h.name];
|
|
32
|
+
return Object.freeze(cy === undefined ? h : { ...h, names: Object.freeze({ cy }) });
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function provider(id, division, transform) {
|
|
36
|
+
const cache = new Map();
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
id,
|
|
39
|
+
timeZone: "Europe/London",
|
|
40
|
+
supportedYears: SUPPORTED_YEARS,
|
|
41
|
+
dataVersion: DATA_VERSION,
|
|
42
|
+
confirmedThrough: CONFIRMED_THROUGH,
|
|
43
|
+
holidaysInYear(year) {
|
|
44
|
+
let list = cache.get(year);
|
|
45
|
+
if (!list) {
|
|
46
|
+
const computed = computeUkHolidays(division, year);
|
|
47
|
+
list = Object.freeze(transform ? transform(computed) : computed);
|
|
48
|
+
cache.set(year, list);
|
|
49
|
+
}
|
|
50
|
+
return list;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* England の bank holidays の提供元(GOV.UK の England and Wales)。
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
export const england = provider("GB-ENG", "england-and-wales");
|
|
59
|
+
/**
|
|
60
|
+
* Wales の bank holidays の提供元(GOV.UK の England and Wales)。名称にウェールズ語(names.cy)が付く。
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
export const wales = provider("GB-WLS", "england-and-wales", withWelsh);
|
|
64
|
+
/**
|
|
65
|
+
* Scotland の bank holidays の提供元。
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export const scotland = provider("GB-SCT", "scotland");
|
|
69
|
+
/**
|
|
70
|
+
* Northern Ireland の bank holidays の提供元。
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
export const northernIreland = provider("GB-NIR", "northern-ireland");
|
|
74
|
+
/**
|
|
75
|
+
* 反映済みの GOV.UK のデータの年月。
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
78
|
+
export const dataVersion = DATA_VERSION;
|
|
79
|
+
/**
|
|
80
|
+
* GOV.UK が日付を公表している最後の年(それより後は規則による予測で confirmed: false)。
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
export const confirmedThrough = CONFIRMED_THROUGH;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 英国の bank holidays(GOV.UK の区分: England and Wales・Scotland・Northern Ireland)の計算。
|
|
3
|
+
*
|
|
4
|
+
* - 毎年の祝日: 1971 年の Banking and Financial Dealings Act の附則と、毎年の布告で定まる日を規則で計算する
|
|
5
|
+
* (England and Wales・Northern Ireland の Good Friday と Christmas Day は慣習法上の休日だが、GOV.UK の一覧と同じく含める)
|
|
6
|
+
* - 振替: 土曜日・日曜日に当たる固定日の祝日は、その後で最も近い、祝日でない平日に振り替える(日付順に割り当てる)
|
|
7
|
+
* - 臨時の祝日・移動: 布告ごとの日付をテーブルで持つ
|
|
8
|
+
*
|
|
9
|
+
* 対応年のすべての日付と名称が GOV.UK のデータと一致することを、テスト用フィクスチャ
|
|
10
|
+
* (test/fixtures/govuk-bank-holidays.json)で検証している。CONFIRMED_THROUGH より後の年は、
|
|
11
|
+
* 規則による予測(confirmed: false)。
|
|
12
|
+
*/
|
|
13
|
+
import { type DateString } from "../core/date.ts";
|
|
14
|
+
import type { Holiday } from "../core/types.ts";
|
|
15
|
+
/**
|
|
16
|
+
* 英国の祝日の種別(既知の値)。
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export type KnownUkHolidayKind =
|
|
20
|
+
/** 毎年の祝日(布告で日付が移動した年を含む) */
|
|
21
|
+
"regular"
|
|
22
|
+
/** 土曜日・日曜日に当たった祝日の振替 */
|
|
23
|
+
| "substitute"
|
|
24
|
+
/** 布告による臨時の祝日(記念式典・国葬など) */
|
|
25
|
+
| "special";
|
|
26
|
+
/**
|
|
27
|
+
* 英国の祝日の種別。将来の版で値が増えることがあるので、分岐では「その他」も扱うこと。
|
|
28
|
+
* すべての値を網羅したことを型で確かめたい場合は KnownUkHolidayKind を使う。
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export type UkHolidayKind = KnownUkHolidayKind | (string & {});
|
|
32
|
+
/** @internal GOV.UK の区分 */
|
|
33
|
+
export type UkDivision = "england-and-wales" | "scotland" | "northern-ireland";
|
|
34
|
+
/** @internal 対応年 */
|
|
35
|
+
export declare const SUPPORTED_YEARS: Readonly<{
|
|
36
|
+
from: 2012;
|
|
37
|
+
to: 2099;
|
|
38
|
+
}>;
|
|
39
|
+
/** @internal GOV.UK が日付を公表している最後の年 */
|
|
40
|
+
export declare const CONFIRMED_THROUGH = 2028;
|
|
41
|
+
/** @internal 反映済みの GOV.UK のデータの年月(2026 年の Scotland の World Cup bank holiday の追加) */
|
|
42
|
+
export declare const DATA_VERSION = "2026-02";
|
|
43
|
+
/** @internal 復活祭の日曜日(グレゴリオ暦。Anonymous Gregorian algorithm) */
|
|
44
|
+
export declare function easterSunday(year: number): DateString;
|
|
45
|
+
/** @internal その年・区分の祝日(日付順) */
|
|
46
|
+
export declare function computeUkHolidays(division: UkDivision, year: number): Holiday<UkHolidayKind>[];
|
package/dist/uk/rules.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 英国の bank holidays(GOV.UK の区分: England and Wales・Scotland・Northern Ireland)の計算。
|
|
3
|
+
*
|
|
4
|
+
* - 毎年の祝日: 1971 年の Banking and Financial Dealings Act の附則と、毎年の布告で定まる日を規則で計算する
|
|
5
|
+
* (England and Wales・Northern Ireland の Good Friday と Christmas Day は慣習法上の休日だが、GOV.UK の一覧と同じく含める)
|
|
6
|
+
* - 振替: 土曜日・日曜日に当たる固定日の祝日は、その後で最も近い、祝日でない平日に振り替える(日付順に割り当てる)
|
|
7
|
+
* - 臨時の祝日・移動: 布告ごとの日付をテーブルで持つ
|
|
8
|
+
*
|
|
9
|
+
* 対応年のすべての日付と名称が GOV.UK のデータと一致することを、テスト用フィクスチャ
|
|
10
|
+
* (test/fixtures/govuk-bank-holidays.json)で検証している。CONFIRMED_THROUGH より後の年は、
|
|
11
|
+
* 規則による予測(confirmed: false)。
|
|
12
|
+
*/
|
|
13
|
+
import { addDays, daysInMonth, formatDate, nthWeekdayOfMonth, weekdayOf, } from "../core/date.js";
|
|
14
|
+
import { rangeError } from "../core/errors.js";
|
|
15
|
+
/** @internal 対応年 */
|
|
16
|
+
export const SUPPORTED_YEARS = Object.freeze({ from: 2012, to: 2099 });
|
|
17
|
+
/** @internal GOV.UK が日付を公表している最後の年 */
|
|
18
|
+
export const CONFIRMED_THROUGH = 2028;
|
|
19
|
+
/** @internal 反映済みの GOV.UK のデータの年月(2026 年の Scotland の World Cup bank holiday の追加) */
|
|
20
|
+
export const DATA_VERSION = "2026-02";
|
|
21
|
+
const NEW_YEAR = "New Year’s Day";
|
|
22
|
+
const SECOND_JANUARY = "2nd January";
|
|
23
|
+
const ST_PATRICK = "St Patrick’s Day";
|
|
24
|
+
const GOOD_FRIDAY = "Good Friday";
|
|
25
|
+
const EASTER_MONDAY = "Easter Monday";
|
|
26
|
+
const EARLY_MAY = "Early May bank holiday";
|
|
27
|
+
const SPRING = "Spring bank holiday";
|
|
28
|
+
const BOYNE = "Battle of the Boyne (Orangemen’s Day)";
|
|
29
|
+
const SUMMER = "Summer bank holiday";
|
|
30
|
+
const ST_ANDREW = "St Andrew’s Day";
|
|
31
|
+
const CHRISTMAS = "Christmas Day";
|
|
32
|
+
const BOXING = "Boxing Day";
|
|
33
|
+
const ALL = ["england-and-wales", "scotland", "northern-ireland"];
|
|
34
|
+
/** 布告で毎年の祝日の日付を移した年(名称が変わる場合は name) */
|
|
35
|
+
const MOVED = [
|
|
36
|
+
{ year: 2012, from: SPRING, date: "2012-06-04" },
|
|
37
|
+
{ year: 2020, from: EARLY_MAY, date: "2020-05-08", name: "Early May bank holiday (VE day)" },
|
|
38
|
+
{ year: 2022, from: SPRING, date: "2022-06-02" },
|
|
39
|
+
];
|
|
40
|
+
/** 布告による臨時の祝日 */
|
|
41
|
+
const SPECIAL_DAYS = [
|
|
42
|
+
{ date: "2012-06-05", name: "Queen’s Diamond Jubilee", divisions: ALL },
|
|
43
|
+
{ date: "2022-06-03", name: "Platinum Jubilee bank holiday", divisions: ALL },
|
|
44
|
+
{
|
|
45
|
+
date: "2022-09-19",
|
|
46
|
+
name: "Bank Holiday for the State Funeral of Queen Elizabeth II",
|
|
47
|
+
divisions: ALL,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
date: "2023-05-08",
|
|
51
|
+
name: "Bank holiday for the coronation of King Charles III",
|
|
52
|
+
divisions: ALL,
|
|
53
|
+
},
|
|
54
|
+
{ date: "2026-06-15", name: "World Cup bank holiday", divisions: ["scotland"] },
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* 振替の日付は規則どおりで、どちらの祝日の振替かの割り当てだけが規則と異なる年。
|
|
58
|
+
* 2023 年の Scotland(1 月 1 日が日曜日)は、GOV.UK では 1/2 が New Year’s Day の振替、1/3 が 2nd January の振替。
|
|
59
|
+
* 同じ曜日の並びの 2012・2017 年は、附則どおり 1/2 が 2nd January、1/3 が New Year’s Day の振替
|
|
60
|
+
*/
|
|
61
|
+
const RENAMED = [
|
|
62
|
+
{ division: "scotland", date: "2023-01-02", name: NEW_YEAR, kind: "substitute" },
|
|
63
|
+
{ division: "scotland", date: "2023-01-03", name: SECOND_JANUARY, kind: "substitute" },
|
|
64
|
+
];
|
|
65
|
+
/** @internal 復活祭の日曜日(グレゴリオ暦。Anonymous Gregorian algorithm) */
|
|
66
|
+
export function easterSunday(year) {
|
|
67
|
+
const a = year % 19;
|
|
68
|
+
const b = Math.floor(year / 100);
|
|
69
|
+
const c = year % 100;
|
|
70
|
+
const d = Math.floor(b / 4);
|
|
71
|
+
const e = b % 4;
|
|
72
|
+
const f = Math.floor((b + 8) / 25);
|
|
73
|
+
const g = Math.floor((b - f + 1) / 3);
|
|
74
|
+
const h = (19 * a + b - d - g + 15) % 30;
|
|
75
|
+
const i = Math.floor(c / 4);
|
|
76
|
+
const k = c % 4;
|
|
77
|
+
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
|
78
|
+
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
|
79
|
+
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
|
80
|
+
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
|
81
|
+
return formatDate(year, month, day);
|
|
82
|
+
}
|
|
83
|
+
function lastMonday(year, month) {
|
|
84
|
+
const last = formatDate(year, month, daysInMonth(year, month));
|
|
85
|
+
return addDays(last, -((weekdayOf(last) - 1 + 7) % 7));
|
|
86
|
+
}
|
|
87
|
+
function isWeekend(date) {
|
|
88
|
+
const w = weekdayOf(date);
|
|
89
|
+
return w === 0 || w === 6;
|
|
90
|
+
}
|
|
91
|
+
/** @internal その年・区分の祝日(日付順) */
|
|
92
|
+
export function computeUkHolidays(division, year) {
|
|
93
|
+
if (!Number.isInteger(year) || year < SUPPORTED_YEARS.from || year > SUPPORTED_YEARS.to) {
|
|
94
|
+
throw rangeError("out-of-range", `UK bank holidays are supported for ${SUPPORTED_YEARS.from}-${SUPPORTED_YEARS.to}; requested ${year}`);
|
|
95
|
+
}
|
|
96
|
+
const scotland = division === "scotland";
|
|
97
|
+
const ni = division === "northern-ireland";
|
|
98
|
+
const fixed = [{ month: 1, day: 1, name: NEW_YEAR }];
|
|
99
|
+
if (scotland)
|
|
100
|
+
fixed.push({ month: 1, day: 2, name: SECOND_JANUARY });
|
|
101
|
+
if (ni)
|
|
102
|
+
fixed.push({ month: 3, day: 17, name: ST_PATRICK });
|
|
103
|
+
if (ni)
|
|
104
|
+
fixed.push({ month: 7, day: 12, name: BOYNE });
|
|
105
|
+
if (scotland)
|
|
106
|
+
fixed.push({ month: 11, day: 30, name: ST_ANDREW });
|
|
107
|
+
fixed.push({ month: 12, day: 25, name: CHRISTMAS }, { month: 12, day: 26, name: BOXING });
|
|
108
|
+
const easter = easterSunday(year);
|
|
109
|
+
const movable = [
|
|
110
|
+
{ date: addDays(easter, -2), name: GOOD_FRIDAY, kind: "regular" },
|
|
111
|
+
{ date: nthWeekdayOfMonth(year, 5, 1, 1), name: EARLY_MAY, kind: "regular" },
|
|
112
|
+
{ date: lastMonday(year, 5), name: SPRING, kind: "regular" },
|
|
113
|
+
{
|
|
114
|
+
date: scotland ? nthWeekdayOfMonth(year, 8, 1, 1) : lastMonday(year, 8),
|
|
115
|
+
name: SUMMER,
|
|
116
|
+
kind: "regular",
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
if (!scotland)
|
|
120
|
+
movable.push({ date: addDays(easter, 1), name: EASTER_MONDAY, kind: "regular" });
|
|
121
|
+
const entries = [];
|
|
122
|
+
for (const e of movable) {
|
|
123
|
+
const moved = MOVED.find((m) => m.year === year && m.from === e.name);
|
|
124
|
+
entries.push(moved ? { date: moved.date, name: moved.name ?? e.name, kind: e.kind } : e);
|
|
125
|
+
}
|
|
126
|
+
for (const s of SPECIAL_DAYS) {
|
|
127
|
+
if (s.date.startsWith(`${year}-`) && s.divisions.includes(division)) {
|
|
128
|
+
entries.push({ date: s.date, name: s.name, kind: "special" });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// 平日に当たる固定日の祝日を先に置き、土曜日・日曜日に当たるものを日付順に振り替える
|
|
132
|
+
const taken = new Set(entries.map((e) => e.date));
|
|
133
|
+
const fixedDates = fixed.map((f) => ({ date: formatDate(year, f.month, f.day), name: f.name }));
|
|
134
|
+
for (const f of fixedDates) {
|
|
135
|
+
if (!isWeekend(f.date)) {
|
|
136
|
+
entries.push({ date: f.date, name: f.name, kind: "regular" });
|
|
137
|
+
taken.add(f.date);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (const f of fixedDates) {
|
|
141
|
+
if (!isWeekend(f.date))
|
|
142
|
+
continue;
|
|
143
|
+
let day = addDays(f.date, 1);
|
|
144
|
+
while (isWeekend(day) || taken.has(day))
|
|
145
|
+
day = addDays(day, 1);
|
|
146
|
+
entries.push({ date: day, name: f.name, kind: "substitute" });
|
|
147
|
+
taken.add(day);
|
|
148
|
+
}
|
|
149
|
+
for (const r of RENAMED) {
|
|
150
|
+
if (r.division !== division || !r.date.startsWith(`${year}-`))
|
|
151
|
+
continue;
|
|
152
|
+
const i = entries.findIndex((e) => e.date === r.date);
|
|
153
|
+
if (i >= 0)
|
|
154
|
+
entries[i] = { date: r.date, name: r.name, kind: r.kind };
|
|
155
|
+
}
|
|
156
|
+
return entries
|
|
157
|
+
.sort((a, b) => (a.date < b.date ? -1 : 1))
|
|
158
|
+
.map((e) => Object.freeze({
|
|
159
|
+
date: e.date,
|
|
160
|
+
name: e.name,
|
|
161
|
+
nameEn: e.name,
|
|
162
|
+
kind: e.kind,
|
|
163
|
+
confirmed: year <= CONFIRMED_THROUGH,
|
|
164
|
+
}));
|
|
165
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bizdays-toolkit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Holidays, business-day calendar and day-by-day schedules (e.g. store hours) with time zone support. Japanese holidays verified against the Cabinet Office data. /
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Holidays, business-day calendar and day-by-day schedules (e.g. store hours) with time zone support. Japanese holidays verified against the Cabinet Office data; UK bank holidays (England, Wales, Scotland, Northern Ireland) verified against GOV.UK. / 祝日と営業日のカレンダー(タイムゾーン対応・日本の祝日は内閣府、英国の bank holidays は GOV.UK のデータで全日照合)",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"bank-holidays",
|
|
6
7
|
"bizdays",
|
|
7
8
|
"business-day",
|
|
8
9
|
"business-days",
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
"holiday",
|
|
11
12
|
"holidays",
|
|
12
13
|
"japan",
|
|
14
|
+
"uk",
|
|
13
15
|
"workday",
|
|
14
16
|
"営業日",
|
|
15
17
|
"祝日"
|
|
@@ -40,6 +42,10 @@
|
|
|
40
42
|
"types": "./dist/jp/index.d.ts",
|
|
41
43
|
"default": "./dist/jp/index.js"
|
|
42
44
|
},
|
|
45
|
+
"./uk": {
|
|
46
|
+
"types": "./dist/uk/index.d.ts",
|
|
47
|
+
"default": "./dist/uk/index.js"
|
|
48
|
+
},
|
|
43
49
|
"./diagnose": {
|
|
44
50
|
"types": "./dist/diagnose/index.d.ts",
|
|
45
51
|
"default": "./dist/diagnose/index.js"
|
|
@@ -57,6 +63,7 @@
|
|
|
57
63
|
"fmt:check": "oxfmt --check",
|
|
58
64
|
"check": "pnpm typecheck && pnpm lint && pnpm test && pnpm test:tz && pnpm build && pnpm fmt:check && node scripts/check-package.ts && node scripts/check-size.ts && node scripts/api-report.ts",
|
|
59
65
|
"verify:cao": "node scripts/verify-cao.ts",
|
|
66
|
+
"verify:govuk": "node scripts/verify-govuk.ts",
|
|
60
67
|
"prepublishOnly": "pnpm check",
|
|
61
68
|
"release-notes": "node scripts/release-notes.ts",
|
|
62
69
|
"api:update": "pnpm build && node scripts/api-report.ts --update"
|