bizdays-toolkit 0.17.0 → 0.17.1
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 +13 -1
- package/README.md +54 -703
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.17.1] - 2026-10-25
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- README(英語・日本語)を入口に絞り、詳しい説明を `docs/guide.md`・`docs/guide.ja.md`(同梱の提供元・営業日カレンダー・繰り返しの規則・合成・よくある設定の例・月の期日・日ごとの値・スケジュールからのカレンダー・入力・対応年の外・未確定の祝日・エラー・設定の診断)と `docs/providers.md`・`docs/providers.ja.md`(他の国・地域の祝日の提供元の書き方)に移した。README には機能の箇条書き(7 項目)・インストール・クイックスタート・できることの一覧(提供元の表・規則の表・診断の例)・ドキュメントの索引を残した。本文の内容は変えていない(`docs/decisions.md` の 45 番)
|
|
12
|
+
|
|
13
|
+
### 開発
|
|
14
|
+
|
|
15
|
+
- `pnpm check`: 英語と日本語のドキュメントの対(README・`docs/guide`・`docs/providers`)の見出しの数とコードブロックの数の一致、互いへのリンク、`docs/*.md` の見出しへのリンク(`#…`)の実在を確かめる(`scripts/check-package.ts`)。コード例の実行(`scripts/check-readme-examples.ts`)も docs のガイドを対象に含めた
|
|
16
|
+
- `RELEASING.md`・`docs/decisions.md` の 22・27 番に、ドキュメントの対が増えたことを書いた
|
|
17
|
+
|
|
7
18
|
## [0.17.0] - 2026-10-25
|
|
8
19
|
|
|
9
20
|
### 移行
|
|
@@ -458,7 +469,8 @@
|
|
|
458
469
|
|
|
459
470
|
- 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
|
|
460
471
|
|
|
461
|
-
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.
|
|
472
|
+
[Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.1...HEAD
|
|
473
|
+
[0.17.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.0...v0.17.1
|
|
462
474
|
[0.17.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.16.0...v0.17.0
|
|
463
475
|
[0.16.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.15.0...v0.16.0
|
|
464
476
|
[0.15.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.14.1...v0.15.0
|
package/README.md
CHANGED
|
@@ -19,19 +19,13 @@
|
|
|
19
19
|
|
|
20
20
|
A business-day calendar that combines public holidays with company or store closures.
|
|
21
21
|
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **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
|
|
30
|
-
- **A calendar from a schedule**: `createCalendarFromSchedule` turns a schedule such as store hours into a business-day calendar, so the hours are the only configuration and business-day arithmetic works on them
|
|
31
|
-
- **Combining calendars**: `combineCalendars` builds a calendar of the days on which all of several calendars are open (your company and the bank, for a remittance date) or any of them is open (any of several stores), and business-day arithmetic works on the result
|
|
32
|
-
- **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, `Asia/Singapore` for Singapore) before evaluation. A server running in UTC does not treat 00:00–09:00 in Japan as the previous day
|
|
33
|
-
- **Lightweight, zero dependencies**: about 10.0 KB min+gzip (about 11.8 KB with Japanese holidays, about 11.7 KB with UK bank holidays, about 10.9 KB with Singapore holidays; measured for 0.17.0). ESM, bundled types and `sideEffects: false`. Written in standard JavaScript and `Intl` only (no Node.js APIs); CI tests it on Node.js, Chrome, Firefox, Edge, Safari, Cloudflare Workers, Bun and Deno
|
|
34
|
-
- **Extensible to other countries and regions**: holiday providers are pluggable through the `HolidayProvider` interface. Japan, the UK and Singapore are bundled
|
|
22
|
+
- **Holidays verified day by day against official data**: Japan (Cabinet Office, 1955 to the latest year in the Official Gazette), the UK (GOV.UK; England, Wales, Scotland and Northern Ireland, 2012 onwards) and Singapore (Ministry of Manpower on data.gov.sg, 2020 onwards). Every date is checked in the tests, differences are watched daily and shipped as patch versions
|
|
23
|
+
- **Business-day arithmetic**: closed weekdays, annual closures (across the new year too), specific closed and open dates and holidays combine into "N business days later", "next business day after the cutoff" and month-based due dates with the four adjustment conventions used in finance
|
|
24
|
+
- **Recurring rules**: "closed on the 3rd Wednesday", "every other Wednesday", "stocktaking on the last day of the month", "closed on Mondays, or the next weekday when Monday is a holiday", "the day after (or before) a holiday", each with its own behaviour on holidays and an optional effective period
|
|
25
|
+
- **Day-by-day values and combined calendars**: store hours that change by weekday, holiday and date; a business-day calendar made from such a schedule; and the days on which all (or any) of several calendars are open
|
|
26
|
+
- **Option diagnostics**: `bizdays-toolkit/diagnose` lists rules that never apply, rules that are shadowed or redundant, rules that have ended, typos in option names and options with no business day at all, without throwing. One line in a test keeps options honest
|
|
27
|
+
- **Lightweight, zero dependencies, time-zone safe**: about 10.0 KB min+gzip (about 11.8 KB with Japanese holidays, about 11.7 KB with UK bank holidays, about 10.9 KB with Singapore holidays; measured for 0.17.0). Standard JavaScript and `Intl` only, ESM with bundled types; tested on Node.js, Chrome, Firefox, Edge, Safari, Cloudflare Workers, Bun and Deno. Instants such as `Date` are converted to a civil date in the calendar's time zone before evaluation
|
|
28
|
+
- **Extensible to other countries and regions**: holiday providers are pluggable through the `HolidayProvider` interface, and `bizdays-toolkit/provider` exposes the calendar arithmetic (nth weekday of a month, Easter) that most countries' holidays are built from
|
|
35
29
|
|
|
36
30
|
## Installation
|
|
37
31
|
|
|
@@ -43,658 +37,64 @@ Node.js 22.12 or later. CI tests the package on Node.js 22.12, 22, 24 and 26, on
|
|
|
43
37
|
|
|
44
38
|
Types are bundled. CI type-checks a consumer project against the published type definitions with TypeScript 5.0, 5.5, 5.8, 6.0 and 7.0 (the native compiler), each with `moduleResolution` set to `node16`, `nodenext` and `bundler`, and confirms that an intentional type error is reported by each.
|
|
45
39
|
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
### Looking up holidays
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
import { getHoliday, holidaysInYear, isHoliday } from "bizdays-toolkit/jp";
|
|
52
|
-
|
|
53
|
-
isHoliday("2026-11-23"); // true
|
|
54
|
-
getHoliday("2026-09-22");
|
|
55
|
-
// { date: "2026-09-22", name: "国民の休日", nameEn: "Citizens' Holiday", kind: "citizens", confirmed: true }
|
|
56
|
-
holidaysInYear(2027);
|
|
57
|
-
|
|
58
|
-
// Instants (Date) are evaluated in Japan time
|
|
59
|
-
isHoliday(new Date("2026-11-22T15:30:00Z")); // true (Nov 23 in Japan time)
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
`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).
|
|
63
|
-
|
|
64
|
-
`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`).
|
|
65
|
-
|
|
66
|
-
### UK bank holidays
|
|
40
|
+
## Quick start
|
|
67
41
|
|
|
68
42
|
```ts
|
|
69
43
|
import { createCalendar } from "bizdays-toolkit";
|
|
70
|
-
import {
|
|
71
|
-
|
|
72
|
-
const edinburgh = createCalendar({ holidays: scotland, closedWeekdays: [0, 6] });
|
|
73
|
-
edinburgh.isBusinessDay("2026-01-02"); // false (2nd January)
|
|
74
|
-
edinburgh.addBusinessDays("2026-06-12", 1); // "2026-06-16" (June 15 is the World Cup bank holiday)
|
|
75
|
-
|
|
76
|
-
scotland.holidaysInYear(2026)[0];
|
|
77
|
-
// { date: "2026-01-01", name: "New Year’s Day", nameEn: "New Year’s Day", kind: "regular", confirmed: true }
|
|
78
|
-
wales.holidaysInYear(2026)[0]?.names; // { cy: "Dydd Calan" }
|
|
79
|
-
|
|
80
|
-
// Across several regions, use the union (a holiday in any of them is a holiday)
|
|
81
|
-
const ukWide = createCalendar({ holidays: [england, scotland, northernIreland] });
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
| Provider | ID | GOV.UK division | Notes |
|
|
85
|
-
| ----------------- | -------- | ----------------- | ------------------------------------------------ |
|
|
86
|
-
| `england` | `GB-ENG` | England and Wales | |
|
|
87
|
-
| `wales` | `GB-WLS` | England and Wales | Welsh names (`names.cy`) included |
|
|
88
|
-
| `scotland` | `GB-SCT` | Scotland | 2nd January, St Andrew’s Day and others |
|
|
89
|
-
| `northernIreland` | `GB-NIR` | Northern Ireland | St Patrick’s Day, Battle of the Boyne and others |
|
|
90
|
-
|
|
91
|
-
- `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
|
|
92
|
-
- 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
|
|
93
|
-
- 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
|
|
94
|
-
- 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
|
|
95
|
-
- One-off bank holidays may be announced only about ten days ahead. Differences from GOV.UK are watched daily and released as patch versions
|
|
96
|
-
|
|
97
|
-
### Singapore public holidays
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
import { createCalendar } from "bizdays-toolkit";
|
|
101
|
-
import { sg } from "bizdays-toolkit/sg";
|
|
102
|
-
|
|
103
|
-
const singapore = createCalendar({ holidays: sg, closedWeekdays: [0, 6] });
|
|
104
|
-
singapore.isBusinessDay("2026-08-10"); // false (National Day falls on a Sunday; Monday is the holiday in lieu)
|
|
105
|
-
singapore.addBusinessDays("2026-02-16", 1); // "2026-02-19" (Chinese New Year on February 17 and 18)
|
|
106
|
-
sg.holidaysInYear(2026).find((h) => h.kind === "substitute");
|
|
107
|
-
// { date: "2026-06-01", name: "Vesak Day", nameEn: "Vesak Day", kind: "substitute", confirmed: true }
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
- `sg` (ID `SG`, time zone `Asia/Singapore`) is one provider; Singapore has no regional holidays. `kind` is `regular` (an annual public holiday), `substitute` (the day after a public holiday that falls on a Sunday, which the Holidays Act makes a public holiday; it has the name of the original holiday) or `special` (a one-off holiday declared by the Government, such as Polling Day). A public holiday on a Saturday has no substitute day
|
|
111
|
-
- Chinese New Year, Hari Raya Puasa, Hari Raya Haji, Vesak Day and Deepavali follow the lunar, Islamic and Hindu calendars, so they cannot be computed from rules. The dates are the ones gazetted by the Ministry of Manpower (MOM), year by year, and the substitute days follow the rule of the Holidays Act. Supported years are the years MOM has gazetted (currently 2020–2027; `confirmedThrough` is the same as the last supported year). Later years are not predicted and are outside the supported years (`out-of-range`), so `sg` never returns `confirmed: false`
|
|
112
|
-
- The dates are verified against the "Singapore Public Holidays" collection on data.gov.sg (MOM). Where that data differs from the gazetted dates (2022: the revised Hari Raya dates and two missing substitute days), the gazetted dates are used. Differences from data.gov.sg are watched daily and released as patch versions
|
|
113
|
-
|
|
114
|
-
### Business-day calendars
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
import { createCalendar } from "bizdays-toolkit";
|
|
118
|
-
import { jp } from "bizdays-toolkit/jp";
|
|
119
|
-
|
|
120
|
-
const shipping = createCalendar({
|
|
121
|
-
holidays: jp,
|
|
122
|
-
closedWeekdays: [0], // Sunday (0 = Sunday … 6 = Saturday)
|
|
123
|
-
closedAnnual: [{ from: "12-31", to: "01-03", name: "New Year closure" }],
|
|
124
|
-
closedDates: [{ from: "2027-08-12", to: "2027-08-16", name: "Summer closure" }],
|
|
125
|
-
openDates: ["2026-12-27"], // a specific open day
|
|
126
|
-
holidayPolicy: "closed", // closed on holidays ("open": open / "ignore": not used)
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
shipping.isBusinessDay("2026-11-23"); // false (Labor Thanksgiving Day)
|
|
130
|
-
shipping.addBusinessDays("2026-12-30", 1); // "2027-01-04"
|
|
131
|
-
shipping.businessDaysBetween("2026-09-01", "2026-10-01"); // business days from (inclusive) to (exclusive)
|
|
132
|
-
|
|
133
|
-
// Orders before 14:00 ship today, later orders on the next business day
|
|
134
|
-
shipping.businessDayOnOrAfter(new Date(), { cutoff: "14:00" }); // the input must carry a time of day
|
|
135
|
-
|
|
136
|
-
shipping.dayInfo("2027-01-01");
|
|
137
|
-
// { date: "2027-01-01", weekday: 5, businessDay: false, reason: "closed-annual", name: "New Year closure", holiday: {…}, rulePath: "closedAnnual[0]" }
|
|
138
|
-
shipping.daysBetween("2026-12-30", "2027-01-04").map((d) => d.businessDay); // [true, false, false, false, false, true]
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
`daysBetween(from, to)` returns `dayInfo` for every day in the period (both ends included). `listBusinessDays` and `holidaysBetween` include both ends too; only `businessDaysBetween(from, to)` counts from `from` (inclusive) to `to` (exclusive), so that `businessDaysBetween(a, b) + businessDaysBetween(b, c)` equals `businessDaysBetween(a, c)` and, when `a` is a business day, `businessDaysBetween(a, addBusinessDays(a, n))` is `n` (the inverse of `addBusinessDays`). From a closed day, `addBusinessDays` counts the next business day as 1 (so `+0` and `+1` give the same day, as `n = 0` is `businessDayOnOrAfter`), while `businessDaysBetween` does not count the closed start, so the two differ by one there.
|
|
142
|
-
|
|
143
|
-
`createCalendar` and `createSchedule` validate the options when they are created, so the usual pattern is to create them once and reuse them. Creating is also cheap (about 0.02 ms for a calendar and 0.04 ms for a schedule with Japanese holidays on a development machine), so creating one per request is not a problem in practice.
|
|
144
|
-
|
|
145
|
-
Each day is decided in this order of priority (top first).
|
|
146
|
-
|
|
147
|
-
1. `openDates` (specific open days)
|
|
148
|
-
2. `closedDates` (specific closed days)
|
|
149
|
-
3. `closedAnnual` (annual closures)
|
|
150
|
-
4. `openRecurring` and `closedRecurring` rules with `onHolidayRule: "over-holiday"` (applied even on holidays), and holiday rules (`{ holiday: true }`)
|
|
151
|
-
5. Holidays (according to `holidayPolicy`; `"open"` makes them business days even on closed weekdays)
|
|
152
|
-
6. `openRecurring` and `closedRecurring` (the default `"under-holiday"`, and the days moved to by `"after-holiday"`)
|
|
153
|
-
7. `closedWeekdays` (closed weekdays)
|
|
154
|
-
8. Otherwise a business day
|
|
155
|
-
|
|
156
|
-
This order is fixed. Only the relation between a recurring rule and holidays can be chosen per rule with `onHolidayRule` (next section).
|
|
157
|
-
|
|
158
|
-
### Recurring rules (weekly, every other week, nth weekday, day of month, holidays, day after or before a holiday)
|
|
159
|
-
|
|
160
|
-
Recurring closed and open days such as "closed on the 3rd Wednesday", "closed on the 2nd and 4th Saturdays", "closed every other Wednesday", "stocktaking on the last day of the month", "closed on Mondays (on the next weekday if Monday is a holiday)", "open on substitute holidays", "closed on the day after a holiday" or "late opening on the day before a holiday" go in `closedRecurring` and `openRecurring`.
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
const museum = createCalendar({
|
|
164
|
-
holidays: jp,
|
|
165
|
-
holidayPolicy: "open", // open on holidays
|
|
166
|
-
closedRecurring: [
|
|
167
|
-
{ weekday: 1, name: "Closed on Mondays", onHolidayRule: "after-holiday" }, // closed on the next weekday instead
|
|
168
|
-
{
|
|
169
|
-
weekday: 3,
|
|
170
|
-
nth: 3,
|
|
171
|
-
months: [2, 8],
|
|
172
|
-
name: "Closed on the 3rd Wednesday in February and August",
|
|
173
|
-
},
|
|
174
|
-
{ day: -1, name: "Stocktaking", onHolidayRule: "over-holiday" }, // closed even on holidays
|
|
175
|
-
{ dayAfterHoliday: true, name: "Closed on the day after a holiday" },
|
|
176
|
-
],
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
museum.isBusinessDay("2026-10-12"); // true (open on Monday, Sports Day)
|
|
180
|
-
museum.dayInfo("2026-10-13");
|
|
181
|
-
// { date: "2026-10-13", weekday: 2, businessDay: false, reason: "closed-recurring",
|
|
182
|
-
// name: "Closed on Mondays", shiftedFrom: "2026-10-12", rulePath: "closedRecurring[0]" }
|
|
183
|
-
museum.dayInfo("2026-02-12").name; // "Closed on the day after a holiday" (National Foundation Day is February 11)
|
|
184
|
-
museum.isBusinessDay("2026-05-07"); // false (the day after the May 3–6 run of holidays)
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
| Pattern | Meaning |
|
|
188
|
-
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
189
|
-
| `{ weekday: 1 }` | Every Monday |
|
|
190
|
-
| `{ weekday: 3, nth: 3 }` | The 3rd Wednesday. `nth` is 1–5 or -1 (last); an array such as `[2, 4]` gives several |
|
|
191
|
-
| `{ weekday: 3, interval: 2, anchor: "2026-01-07" }` | Every other Wednesday: the Wednesdays an even number of weeks from `anchor` (2 or more weeks; see below) |
|
|
192
|
-
| `{ day: 20 }` | The 20th of every month. `day` is 1–31 or -1 (last day); an array gives several |
|
|
193
|
-
| `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed to the given kinds (`Holiday.kind`); judged before `holidayPolicy` (see below) |
|
|
194
|
-
| `{ dayAfterHoliday: true }` | The day after a holiday: the first non-holiday after a holiday or a run of holidays (holidays of the provider only) |
|
|
195
|
-
| `{ dayBeforeHoliday: true }` | The day before a holiday: the non-holiday just before a holiday or a run of holidays (holidays of the provider only) |
|
|
196
|
-
| `months: [8, 12]` | Narrows any pattern to these months |
|
|
197
|
-
| `effectiveFrom: "2027-04-01"`, `effectiveTo: "…"` | The period the rule is in effect (both ends included). Outside it, the rule is as if it were not written (see below) |
|
|
198
|
-
|
|
199
|
-
- **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
|
|
200
|
-
- **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
|
|
201
|
-
- **Every other week** (`interval: 2`) is not the same as the 1st and 3rd weekdays (`nth: [1, 3]`): a month with five Wednesdays shifts the latter by a week. `interval` is the number of weeks (an integer of 2 or more) and `anchor` is a date on the same weekday that the cycle passes through; the rule matches that date and every `interval` weeks before and after it. Stores announce "every other Wednesday from January 7", so copy that date. To start at the anchor rather than also matching earlier dates, add `effectiveFrom`. `interval` cannot be combined with `nth`
|
|
202
|
-
- **Holiday rules** (`{ holiday: true }`) write exceptions to the default holiday handling: "open on holidays from April 2027" is `openRecurring: [{ holiday: true, effectiveFrom: "2027-04-01" }]` with `holidayPolicy: "closed"` kept for the past, "open on substitute holidays and citizens' holidays" is `openRecurring: [{ holiday: true, kinds: ["substitute", "citizens"] }]`, and "special hours on the May holidays" in a schedule is `recurring: [{ holiday: true, months: [5], value: … }]`. `kinds` narrows to those `Holiday.kind` values (see the provider's list; with `holidays: jp` the editor completes them), `weekdays` to those weekdays ("morning hours on a Saturday holiday" is `{ holiday: true, weekdays: [6], value: … }`), `months` to those months; omit them for all holidays. The rule is judged at the "over the holiday" step, before `holidayPolicy` or a schedule's `holiday`, and also with `holidayPolicy: "ignore"`. Its days are holidays, so `onHolidayRule` cannot be set on it (`invalid-option`); `holiday` still shows the holiday
|
|
203
|
-
- **The day after a holiday** is never itself a holiday, so `onHolidayRule` cannot be set on such a rule (it is an `invalid-option`). "Holidays" are those of the holiday provider, not annual closures. A rule of this kind matches no day without a holiday provider. `weekdays` narrows it to those weekdays: a museum "closed on the day after a holiday, except when that day is a Saturday or Sunday" is `{ dayAfterHoliday: true, weekdays: [1, 2, 3, 4, 5] }` (opening the weekend with `openRecurring` instead would override every other weekend closure)
|
|
204
|
-
- **The day before a holiday** (`{ dayBeforeHoliday: true }`) is the mirror image: the day whose next day is a holiday and which is not a holiday itself, so before a run of holidays it is only the day before the run (in May 2026 it is Saturday May 2; May 4 and 5 are holidays themselves). The same rules apply (`weekdays`, `months`, effective periods, no `onHolidayRule`, no match without a provider). It writes "eve-of-holiday rates" in a schedule, "open until 2 a.m. on the day before a holiday" (`weekdays: [1, 2, 3, 4, 5]` keeps weekends as usual), or an earlier shipping cutoff on the day before a holiday (put the cutoff time in a schedule and pass `cutoffs.valueOn(date)` as `cutoff`)
|
|
205
|
-
- Among recurring rules, the one written first in the array wins, and `openRecurring` wins over `closedRecurring`. The day after Labor Thanksgiving Day on a Monday (2026-11-23) is both the day the Monday rule moves to and the day after a holiday; the rule written first decides, and `dayInfo` shows which
|
|
206
|
-
|
|
207
|
-
**Effective periods** (`effectiveFrom`, `effectiveTo`) let one calendar represent a change of rules and still reproduce past dates. They go on recurring rules and on annual closures (`closedAnnual` in object form).
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
const shop = createCalendar({
|
|
211
|
-
holidays: jp,
|
|
212
|
-
closedRecurring: [
|
|
213
|
-
{ weekday: 1, name: "Closed on Mondays", effectiveTo: "2027-03-31" },
|
|
214
|
-
{ weekday: 2, name: "Closed on Tuesdays", effectiveFrom: "2027-04-01" },
|
|
215
|
-
{ weekday: 3, interval: 2, anchor: "2026-01-07", name: "Closed every other Wednesday" },
|
|
216
|
-
],
|
|
217
|
-
closedAnnual: [{ from: "08-13", to: "08-16", name: "Summer closure", effectiveTo: "2026-12-31" }],
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
shop.isBusinessDay("2027-03-29"); // false (Monday, before the change)
|
|
221
|
-
shop.isBusinessDay("2027-04-06"); // false (Tuesday, after the change)
|
|
222
|
-
shop.addBusinessDays("2027-03-30", 3); // "2027-04-03" (counts across the change; March 31 is an every-other-week Wednesday)
|
|
223
|
-
shop.isBusinessDay("2027-08-13"); // true (the summer closure ended in 2026)
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
- Outside its period a rule is simply not applied; the order of priority does not change. A rule with only `effectiveFrom` or only `effectiveTo` is open-ended on the other side
|
|
227
|
-
- An `"after-holiday"` rule is judged by the date it would originally apply to: if that date is within the period, the rule is moved to the next non-holiday even when that day is outside the period
|
|
228
|
-
- Changing the closed weekday is written as two weekly rules with periods, as above, rather than with `closedWeekdays` (which has no period)
|
|
229
|
-
|
|
230
|
-
**What happens on a holiday** (`onHolidayRule`) is chosen per rule.
|
|
231
|
-
|
|
232
|
-
| Value | Meaning | How a store would announce it |
|
|
233
|
-
| --------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
234
|
-
| `"under-holiday"` (default) | Under the holiday: follows the holiday handling (`holidayPolicy`) | "Closed on the 3rd Wednesday (open if it is a holiday)" |
|
|
235
|
-
| `"over-holiday"` | Over the holiday: applies even on holidays | "Closed on the 3rd Wednesday (holidays included)" |
|
|
236
|
-
| `"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)" |
|
|
237
|
-
|
|
238
|
-
- With `holidayPolicy: "ignore"` (holidays not used), `"under-holiday"` rules also apply on holidays
|
|
239
|
-
- 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`
|
|
240
|
-
- The "holidays" used for moving are only those of the holiday provider (such as `jp`), not annual closures
|
|
241
|
-
- `dayInfo()` for a day decided by `openDates`, `closedDates`, `closedAnnual`, `openRecurring` or `closedRecurring` (including the day a rule moved to) includes `rulePath`, the position of that rule in the options in the same form as the diagnostics' `path` (such as `"closedRecurring[0]"`), so rules with the same `name` can be told apart. Days decided by holidays or `closedWeekdays` have no `rulePath`
|
|
242
|
-
|
|
243
|
-
In a schedule, the same patterns with a `value` go in `recurring`.
|
|
244
|
-
|
|
245
|
-
```ts
|
|
246
|
-
const hours = createSchedule<Hours>({
|
|
247
|
-
holidays: jp,
|
|
248
|
-
weekly: [sunday, weekday, weekday, weekday, weekday, weekday, weekday],
|
|
249
|
-
recurring: [
|
|
250
|
-
{ weekday: 1, value: null, name: "Closed on Mondays", onHolidayRule: "after-holiday" },
|
|
251
|
-
{ day: -1, value: { open: "10:00", close: "17:00" }, name: "Short hours at month end" },
|
|
252
|
-
{
|
|
253
|
-
dayBeforeHoliday: true,
|
|
254
|
-
value: { open: "10:00", close: "23:00" },
|
|
255
|
-
name: "Late on the eve of a holiday",
|
|
256
|
-
},
|
|
257
|
-
],
|
|
258
|
-
});
|
|
259
|
-
hours.valueOn("2026-05-02"); // { open: "10:00", close: "23:00" } (the day before the May 3–6 holidays)
|
|
260
|
-
hours.dayInfo("2026-05-04").reason; // "weekday" (a holiday in the run is not the day before a holiday)
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
### Combining calendars
|
|
264
|
-
|
|
265
|
-
`combineCalendars` makes one calendar out of several: the days on which **all** of them are open (`businessDay: "all"`), or on which **any** of them is open (`"any"`). The result is an ordinary calendar, so business-day arithmetic, `adjust`, `dueDate` and the rest work on the combination.
|
|
266
|
-
|
|
267
|
-
```ts
|
|
268
|
-
import { combineCalendars, createCalendar } from "bizdays-toolkit";
|
|
269
|
-
import { jp } from "bizdays-toolkit/jp";
|
|
270
|
-
|
|
271
|
-
const company = createCalendar({
|
|
272
|
-
holidays: jp,
|
|
273
|
-
closedWeekdays: [0, 6],
|
|
274
|
-
closedAnnual: [{ from: "12-29", to: "01-03", name: "New Year closure" }],
|
|
275
|
-
});
|
|
276
|
-
const bank = createCalendar({
|
|
277
|
-
holidays: jp,
|
|
278
|
-
closedWeekdays: [0, 6],
|
|
279
|
-
closedAnnual: [{ from: "12-31", to: "01-03", name: "Bank holiday period" }],
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
// A remittance is processed only on days when both the company and the bank are open
|
|
283
|
-
const remittance = combineCalendars([company, bank], { businessDay: "all" });
|
|
284
|
-
remittance.addBusinessDays("2026-12-28", 2); // "2027-01-05"
|
|
285
|
-
remittance.dayInfo("2026-12-29");
|
|
286
|
-
// { date: "2026-12-29", weekday: 2, businessDay: false, reason: "closed-annual", name: "New Year closure", rulePath: "closedAnnual[0]", decidedBy: 0, decidedByPath: [0] }
|
|
287
|
-
|
|
288
|
-
// Any of several stores is open
|
|
289
|
-
const anyStore = combineCalendars([company, bank], { businessDay: "any" });
|
|
290
|
-
anyStore.isBusinessDay("2026-12-29"); // true
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
- `dayInfo` reports `reason`, `name`, `shiftedFrom` and `rulePath` from the first calendar whose result equals the combined result, and the index of that calendar in `decidedBy` (`rulePath` is a position in that calendar's options). With `"all"`, a closed day is explained by the first calendar that is closed, and an open day by calendar 0 (all are open). With `"any"` it is the other way round
|
|
294
|
-
- `decidedByPath` is the list of indexes down to the calendar that decided, outermost first, so that `rulePath` can be traced from the result alone when combined calendars are combined again: `[0]` for a plain combination (the same as `decidedBy`), `[0, 1]` for calendar 1 inside calendar 0. `decidedBy` stays the outer index
|
|
295
|
-
- `holiday`, `isHoliday`, `holidaysInYear` and so on use the union of the calendars' holiday providers: the same provider object counts once, a wrapped provider (such as `jp` with some kinds filtered out) counts as a different one, and when two providers have a holiday on the same date, the provider of the earlier calendar decides the holiday shown. Combining a calendar with a wrapped `jp` and one with `jp` itself therefore keeps the business-day results but still shows the filtered-out holidays in `holiday`. `outOfRange` is `"throw"` if any calendar throws outside its supported years
|
|
296
|
-
- All calendars must share a time zone (`time-zone-conflict` otherwise). Passing fewer than two calendars, or the same calendar twice, is an `invalid-argument`. `maxSearchDays` (default 3660) can be given in the options
|
|
297
|
-
- A combined calendar can be combined again. Option diagnostics work on options, not on calendars, so diagnose each calendar's options separately
|
|
298
|
-
|
|
299
|
-
Combining options instead would not work: a specific open day (`openDates`) in one set of options would override closures in the other, and wrapping one calendar as a holiday provider of the other would turn its closures into "holidays" (`isHoliday` would be true, and `holidayPolicy: "open"` would open on them).
|
|
300
|
-
|
|
301
|
-
### Common recipes
|
|
302
|
-
|
|
303
|
-
**Treat citizens' holidays (days between two holidays) as business days**: wrap the holiday provider and filter by kind.
|
|
304
|
-
|
|
305
|
-
```ts
|
|
306
|
-
import { jp } from "bizdays-toolkit/jp";
|
|
307
|
-
|
|
308
|
-
const jpWithoutCitizensHoliday = {
|
|
309
|
-
...jp,
|
|
310
|
-
holidaysInYear: (year: number) => jp.holidaysInYear(year).filter((h) => h.kind !== "citizens"),
|
|
311
|
-
};
|
|
312
|
-
const calendar = createCalendar({ holidays: jpWithoutCitizensHoliday, closedWeekdays: [0, 6] });
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
**Split special days kept in one list**: dates (`"YYYY-MM-DD"`) go in `dates` and annual dates (`"MM-DD"`) in `annual`.
|
|
316
|
-
|
|
317
|
-
```ts
|
|
318
|
-
type Hours = { open: string; close: string } | null;
|
|
319
|
-
const regular: Hours = { open: "10:00", close: "19:00" };
|
|
320
|
-
const specialDays: { date: string; value: Hours }[] = [
|
|
321
|
-
{ date: "2026-12-31", value: { open: "10:00", close: "17:00" } },
|
|
322
|
-
{ date: "01-01", value: null },
|
|
323
|
-
];
|
|
324
|
-
const special = createSchedule<Hours>({
|
|
325
|
-
weekly: [regular, regular, regular, regular, regular, regular, regular],
|
|
326
|
-
dates: specialDays.filter((d) => d.date.length === 10),
|
|
327
|
-
annual: specialDays.filter((d) => d.date.length === 5),
|
|
328
|
-
});
|
|
329
|
-
special.valueOn("2027-01-01"); // null
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
**Which business day of the month a date is**: count the business days from the first of the month up to (and excluding) the date, then add one. `businessDaysBetween` excludes `to`, so the date itself is the `+ 1`. This is the inverse of `nthBusinessDayOfMonth`.
|
|
333
|
-
|
|
334
|
-
```ts
|
|
335
|
-
import { monthOf } from "bizdays-toolkit";
|
|
336
|
-
|
|
337
|
-
const calendar = createCalendar({ holidays: jp, closedWeekdays: [0, 6] });
|
|
338
|
-
const businessDayIndex = (date: string): number =>
|
|
339
|
-
calendar.isBusinessDay(date) ? calendar.businessDaysBetween(`${monthOf(date)}-01`, date) + 1 : 0;
|
|
340
|
-
businessDayIndex("2026-10-07"); // 5 (the 5th business day of October)
|
|
341
|
-
businessDayIndex("2026-10-03"); // 0 (Saturday: not a business day)
|
|
342
|
-
calendar.nthBusinessDayOfMonth("2026-10", 5); // "2026-10-07"
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
**The next closed day and why**: there is no `nextClosedDay`, but `daysBetween` returns `dayInfo` for every day, so take the first one that is not a business day.
|
|
346
|
-
|
|
347
|
-
```ts
|
|
348
|
-
import { addDays } from "bizdays-toolkit";
|
|
349
|
-
|
|
350
|
-
const calendar = createCalendar({ holidays: jp, closedWeekdays: [0, 6] });
|
|
351
|
-
const nextClosedDay = (date: string) =>
|
|
352
|
-
calendar.daysBetween(addDays(date, 1), addDays(date, 60)).find((d) => !d.businessDay);
|
|
353
|
-
nextClosedDay("2026-10-05")?.date; // "2026-10-10"
|
|
354
|
-
nextClosedDay("2026-10-05")?.reason; // "closed-weekday"
|
|
355
|
-
nextClosedDay("2026-10-11")?.reason; // "holiday" (Sports Day, October 12)
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
**Business days of Japanese banks**: banks are closed on weekends, holidays and December 31 to January 3, so a bank calendar is three lines. Combine it with your own calendar for a remittance date (see "Combining calendars").
|
|
359
|
-
|
|
360
|
-
```ts
|
|
361
|
-
const bank = createCalendar({
|
|
362
|
-
holidays: jp,
|
|
363
|
-
closedWeekdays: [0, 6],
|
|
364
|
-
closedAnnual: [{ from: "12-31", to: "01-03", name: "Bank holiday period" }],
|
|
365
|
-
});
|
|
366
|
-
bank.isBusinessDay("2026-12-30"); // true
|
|
367
|
-
bank.nextBusinessDay("2026-12-30"); // "2027-01-04"
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
**Build per-store calendars from shared options**: options are plain objects, so spread and extend them.
|
|
371
|
-
|
|
372
|
-
```ts
|
|
373
|
-
const base = {
|
|
374
|
-
holidays: jp,
|
|
375
|
-
closedWeekdays: [0],
|
|
376
|
-
closedAnnual: [{ from: "12-31", to: "01-03" }],
|
|
377
|
-
} as const;
|
|
378
|
-
const shibuya = createCalendar({ ...base, openDates: ["2026-12-27"] });
|
|
379
|
-
const yokohama = createCalendar({ ...base, closedRecurring: [{ weekday: 3, nth: 3 }] });
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
### Month-based due dates and date adjustment
|
|
44
|
+
import { isHoliday, jp } from "bizdays-toolkit/jp";
|
|
383
45
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
```ts
|
|
387
|
-
import { closingMonth, createCalendar, monthOf } from "bizdays-toolkit";
|
|
388
|
-
import { jp } from "bizdays-toolkit/jp";
|
|
46
|
+
isHoliday("2026-11-23"); // true (Labour Thanksgiving Day)
|
|
389
47
|
|
|
390
48
|
const calendar = createCalendar({
|
|
391
49
|
holidays: jp,
|
|
392
50
|
closedWeekdays: [0, 6],
|
|
393
|
-
closedAnnual: [{ from: "12-
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
calendar.adjust("2026-10-31", { convention: "following" }); // "2026-11-02" (Oct 31 is a Saturday)
|
|
397
|
-
calendar.adjust("2026-10-31", { convention: "modified-following" }); // "2026-10-30" (the month would change, so backwards)
|
|
398
|
-
|
|
399
|
-
calendar.firstBusinessDayOfMonth("2027-01"); // "2027-01-04"
|
|
400
|
-
calendar.lastBusinessDayOfMonth("2026-12"); // "2026-12-30"
|
|
401
|
-
calendar.nthBusinessDayOfMonth("2026-10", 5); // "2026-10-07" (-1 is the last, -2 the second to last)
|
|
402
|
-
|
|
403
|
-
// Paid on the 10th of every month (next business day if closed)
|
|
404
|
-
calendar.dueDate("2026-10", { day: 10, convention: "following" }); // "2026-10-13" (Oct 12 is Sports Day)
|
|
405
|
-
|
|
406
|
-
// Closing on the 20th, paid at the end of the next month (previous business day if closed)
|
|
407
|
-
const billing = closingMonth("2026-10-21", 20); // "2026-11" (Oct 21 to Nov 20 belongs to November)
|
|
408
|
-
calendar.dueDate(monthOf(billing, 1), { day: -1, convention: "preceding" }); // "2026-12-30"
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
**Adjustment conventions** (`convention`, required)
|
|
412
|
-
|
|
413
|
-
| Convention | On a closed day |
|
|
414
|
-
| ---------------------- | -------------------------------------------------------------------- |
|
|
415
|
-
| `"following"` | The next business day |
|
|
416
|
-
| `"preceding"` | The previous business day |
|
|
417
|
-
| `"modified-following"` | The next business day, or the previous one if the month would change |
|
|
418
|
-
| `"modified-preceding"` | The previous business day, or the next one if the month would change |
|
|
419
|
-
|
|
420
|
-
- On a business day, every convention returns the same day
|
|
421
|
-
- 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)
|
|
422
|
-
- 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
|
|
423
|
-
- There is no "nearest business day" (nearest), because the tie-breaking rule for equal distances differs by industry
|
|
424
|
-
|
|
425
|
-
**Month helpers** (root functions; business days are not considered)
|
|
426
|
-
|
|
427
|
-
- `monthOf(dateOrMonth, offset?)`: the month `offset` months after (before, if negative) a date or a month `"YYYY-MM"`
|
|
428
|
-
- `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
|
|
429
|
-
- `closingPeriod(monthOrDate, closingDay)`: the inverse: the period `{ from, to }` (both ends included) that belongs to the month, from the day after the previous closing day to the closing day. Given a date, the period that date belongs to. The result has the shape of a date range, so it can go straight into `closedDates` or `listBusinessDays`
|
|
430
|
-
|
|
431
|
-
```ts
|
|
432
|
-
closingPeriod("2026-11", 20); // { from: "2026-10-21", to: "2026-11-20" }
|
|
433
|
-
closingPeriod("2026-03", 31); // { from: "2026-03-01", to: "2026-03-31" } (closing on the 31st in February means the 28th)
|
|
434
|
-
const { from, to } = closingPeriod("2026-11-05", 20); // the period "2026-11-05" belongs to
|
|
435
|
-
const office = createCalendar({ holidays: jp, closedWeekdays: [0, 6] });
|
|
436
|
-
office.listBusinessDays(from, to).length; // 22 (business days in the November period)
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
**Closing days that move to a business day**: when the contract says "closed on the 20th, or the previous business day if the 20th is not one", use the calendar's `closingMonth(date, closingDay, { convention })` and `closingPeriod(monthOrDate, closingDay, { convention })`. Each month's closing day (the last day when it does not exist) is adjusted with the `convention` of `adjust` (required, no default), and a date belongs to the first month whose adjusted closing day is on or after it. The root functions keep closing on calendar days.
|
|
440
|
-
|
|
441
|
-
```ts
|
|
442
|
-
const office = createCalendar({ holidays: jp, closedWeekdays: [0, 6] });
|
|
443
|
-
// September 20, 2026 is a Sunday: with "preceding" the September closing day is Friday the 18th
|
|
444
|
-
office.closingMonth("2026-09-19", 20, { convention: "preceding" }); // "2026-10" (the 19th is after the September closing)
|
|
445
|
-
closingMonth("2026-09-19", 20); // "2026-09" (closing on calendar days)
|
|
446
|
-
office.closingPeriod("2026-10", 20, { convention: "preceding" }); // { from: "2026-09-19", to: "2026-10-20" }
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
A month whose closing day is not preceded by any business day since the previous closing day (a long closure for renovation, say) has no period of its own: its days belong to the next month, `closingMonth` never returns that month, and `closingPeriod` for that month throws `no-business-day`. With `"following"` a closing day can move past a long closure into a later month; the period is then longer, not empty.
|
|
450
|
-
|
|
451
|
-
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.
|
|
452
|
-
|
|
453
|
-
### Day-by-day values (e.g. store hours)
|
|
454
|
-
|
|
455
|
-
`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.
|
|
456
|
-
|
|
457
|
-
```ts
|
|
458
|
-
import { createSchedule } from "bizdays-toolkit";
|
|
459
|
-
import { jp } from "bizdays-toolkit/jp";
|
|
460
|
-
|
|
461
|
-
type Hours = { open: string; close: string } | null; // null means closed
|
|
462
|
-
const weekday = { open: "10:00", close: "21:00" };
|
|
463
|
-
const sunday = { open: "10:00", close: "19:00" };
|
|
464
|
-
|
|
465
|
-
const storeHours = createSchedule<Hours>({
|
|
466
|
-
holidays: jp,
|
|
467
|
-
weekly: [sunday, weekday, null, weekday, weekday, weekday, weekday], // [Sun, Mon, …, Sat]; closed on Tuesdays
|
|
468
|
-
holiday: { sameAs: 0 }, // Sunday hours on holidays ("weekday": by weekday / { value: … }: a fixed value)
|
|
469
|
-
annual: [{ date: "01-01", value: null, name: "Closed on New Year's Day" }], // annual dates (ranges too)
|
|
470
|
-
dates: [{ date: "2026-12-31", value: { open: "10:00", close: "17:00" }, name: "New Year's Eve" }],
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
storeHours.valueOn("2026-11-03"); // { open: "10:00", close: "19:00" } (Culture Day uses Sunday hours)
|
|
474
|
-
storeHours.dayInfo("2027-01-01");
|
|
475
|
-
// { date: "2027-01-01", weekday: 5, value: null, reason: "annual", name: "Closed on New Year's Day", holiday: {…}, rulePath: "annual[0]" }
|
|
476
|
-
|
|
477
|
-
// Holidays in a period with their hours: filter daysBetween by holiday
|
|
478
|
-
storeHours
|
|
479
|
-
.daysBetween("2026-11-01", "2026-11-30")
|
|
480
|
-
.filter((d) => d.holiday)
|
|
481
|
-
.map((d) => d.date); // ["2026-11-03", "2026-11-23"]
|
|
482
|
-
|
|
483
|
-
// Holidays not overridden by specific or annual dates (New Year's Day is closed by the annual rule)
|
|
484
|
-
storeHours
|
|
485
|
-
.daysBetween("2026-12-28", "2027-01-11")
|
|
486
|
-
.filter((d) => d.holiday && d.reason !== "date" && d.reason !== "annual")
|
|
487
|
-
.map((d) => d.date); // ["2027-01-11"]
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
`reason` is the rule that decided the value, and `rulePath` (for `dates`, `annual` and `recurring`) its position in the options. With `holiday: "weekday"` (holidays follow the weekday rule), a holiday has `reason: "weekday"`, so check `holiday` to know whether a day is a holiday. Filtering by `holiday` alone also keeps holidays whose value is set by `dates` or `annual` (such as special hours on New Year's Day). To list only holidays that follow the holiday handling, also exclude `reason` `"date"` and `"annual"`, as in the last example: `holiday` tells whether the day is a holiday, and `reason` tells which rule decided the value.
|
|
491
|
-
|
|
492
|
-
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.
|
|
493
|
-
|
|
494
|
-
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.
|
|
495
|
-
|
|
496
|
-
```ts
|
|
497
|
-
const { date, time } = storeHours.civilOf(new Date()); // { date: "2026-10-14", time: "16:30" } (Japan time)
|
|
498
|
-
const hours = storeHours.valueOn(date);
|
|
499
|
-
const isOpen = hours !== null && hours.open <= time && time < hours.close;
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
For inputs that always carry a time (a `Date`, Temporal `Instant`, `ZonedDateTime` or `PlainDateTime`, or a `"YYYY-MM-DDTHH:mm"` string), `time` is typed as `string`, so no `null` check is needed. `civilOf` is also on business-day calendars, and the same conversion is available as `toDateTimeParts(input, timeZone)` in the root. (`toJapanDate` and `toJapanDateTime` in `bizdays-toolkit/jp` are deprecated since 0.10.0: use `toDateString(input, "Asia/Tokyo")`, `toDateTimeParts(input, "Asia/Tokyo")` or `dateOf`/`civilOf` on a calendar instead. They will be removed in 1.0.0.)
|
|
503
|
-
|
|
504
|
-
### A business-day calendar from a schedule
|
|
505
|
-
|
|
506
|
-
When the schedule (store hours, say) is the real configuration, `createCalendarFromSchedule(schedule, isOpen)` makes a business-day calendar from it, so you do not write a second set of rules to keep in sync. `isOpen` decides from the value alone whether the day is a business day; the result is an ordinary calendar with business-day arithmetic, `adjust`, `dueDate` and the rest.
|
|
507
|
-
|
|
508
|
-
```ts
|
|
509
|
-
import { createCalendarFromSchedule } from "bizdays-toolkit";
|
|
510
|
-
|
|
511
|
-
const hours = createSchedule<Hours>({
|
|
512
|
-
holidays: jp,
|
|
513
|
-
weekly: [null, weekday, weekday, weekday, weekday, weekday, { open: "10:00", close: "17:00" }],
|
|
514
|
-
holiday: { sameAs: 0 },
|
|
515
|
-
annual: [
|
|
516
|
-
{ from: "12-31", to: "01-03", value: null, name: "New Year closure" },
|
|
517
|
-
{ date: "01-04", value: { open: "11:00", close: "15:00" }, name: "First day of the year" },
|
|
518
|
-
],
|
|
51
|
+
closedAnnual: [{ from: "12-29", to: "01-03", name: "New Year closure" }],
|
|
519
52
|
});
|
|
520
|
-
|
|
521
|
-
calendar.addBusinessDays("2026-12-
|
|
522
|
-
calendar.
|
|
523
|
-
|
|
53
|
+
calendar.isBusinessDay("2026-12-30"); // false
|
|
54
|
+
calendar.addBusinessDays("2026-12-28", 1); // "2027-01-04"
|
|
55
|
+
calendar.businessDayOnOrAfter("2026-12-28T15:30", { cutoff: "14:00" }); // "2027-01-04" (after the cutoff, so the next business day)
|
|
56
|
+
calendar.dueDate("2026-11", { day: 23, convention: "following" }); // "2026-11-24" (the 23rd is a holiday)
|
|
524
57
|
```
|
|
525
58
|
|
|
526
|
-
|
|
527
|
-
- `dayInfo` maps the schedule's `reason` to the calendar's: `date` → `"open-date"`/`"closed-date"`, `annual` → `"open-annual"`/`"closed-annual"`, `recurring` → `"open-recurring"`/`"closed-recurring"`, `holiday` → `"holiday"` (whether or not the holiday is open, as with `holidayPolicy: "open"`), `weekday` → `"regular"`/`"closed-weekday"`. `"open-annual"` exists only for calendars made this way, since `closedAnnual` of `createCalendar` only closes. `name`, `holiday`, `shiftedFrom` and `rulePath` are the schedule's
|
|
528
|
-
- Holidays, the time zone and `outOfRange` are the schedule's. Diagnose the schedule's options with `diagnoseSchedule`; the calendar itself is not diagnosed. `maxSearchDays` (default 3660) can be given in the options
|
|
529
|
-
- Use `createCalendar` when you only need open or closed; use this when you have a schedule anyway
|
|
530
|
-
|
|
531
|
-
### Date and date-time inputs
|
|
532
|
-
|
|
533
|
-
Every method accepts the following inputs. Returned dates are always `"YYYY-MM-DD"` strings.
|
|
534
|
-
|
|
535
|
-
| Input | Treated as |
|
|
536
|
-
| ---------------------------------------------------- | ----------------------------------------------------------------- |
|
|
537
|
-
| `"2026-11-23"` | A civil date |
|
|
538
|
-
| `"2026-11-23T10:00"` | A date and time in the calendar's time zone (wall clock) |
|
|
539
|
-
| `"2026-11-22T15:30:00Z"`, `"2026-11-23T00:30+09:00"` | An instant, converted to a civil date in the calendar's time zone |
|
|
540
|
-
| `Date` | An instant |
|
|
541
|
-
| Temporal `PlainDate`, `PlainDateTime` | A civil date or date and time (no dependency on Temporal) |
|
|
542
|
-
| Temporal `Instant`, `ZonedDateTime` | An instant |
|
|
543
|
-
|
|
544
|
-
The separator between the date and the time is `T` (upper or lower case), as in the `DateTimeInput` type; a space (`"2026-11-23 10:00"`) is an `invalid-time` error. Temporal objects are recognized by their shape, so the package does not depend on Temporal. CI also checks real Temporal objects in the runtimes that provide Temporal (the CI log shows whether each runtime had it).
|
|
59
|
+
The [guide](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md) explains everything below in full, with examples that are run in CI. This README is the short version.
|
|
545
60
|
|
|
546
|
-
|
|
61
|
+
## What you can do
|
|
547
62
|
|
|
548
|
-
|
|
63
|
+
### Holidays of Japan, the UK and Singapore
|
|
549
64
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const opensAt = store.instantOf(store.nextBusinessDay(store.today()), "10:00"); // the next opening, as a Date
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
Around daylight-saving changes the result follows Temporal's default (`disambiguation: "compatible"`): a time that does not exist (in the UK, 01:00–01:59 on the last Sunday of March) is moved forward by the length of the gap, and a time that occurs twice (01:00–01:59 on the last Sunday of October) is the earlier one.
|
|
65
|
+
| Provider | Subpath | Time zone | Supported years | Verified against |
|
|
66
|
+
| ------------------------------------------------- | -------------------- | ---------------- | --------------- | -------------------------------------- |
|
|
67
|
+
| `jp` | `bizdays-toolkit/jp` | `Asia/Tokyo` | 1955–2099 | Cabinet Office "National Holidays" CSV |
|
|
68
|
+
| `england`, `wales`, `scotland`, `northernIreland` | `bizdays-toolkit/uk` | `Europe/London` | 2012–2099 | GOV.UK bank holidays |
|
|
69
|
+
| `sg` | `bizdays-toolkit/sg` | `Asia/Singapore` | 2020–2027 | data.gov.sg (Ministry of Manpower) |
|
|
559
70
|
|
|
560
|
-
|
|
561
|
-
import { england } from "bizdays-toolkit/uk";
|
|
562
|
-
|
|
563
|
-
const uk = createCalendar({ holidays: england });
|
|
564
|
-
uk.instantOf("2026-03-29", "01:30").toISOString(); // "2026-03-29T01:30:00.000Z" (02:30 BST; 01:30 does not exist)
|
|
565
|
-
uk.instantOf("2026-10-25", "01:30").toISOString(); // "2026-10-25T00:30:00.000Z" (the first 01:30, still BST)
|
|
566
|
-
```
|
|
71
|
+
Each holiday carries its date, name (and English name), `kind` (national, substitute, citizens' holiday, bank holiday and so on, by country) and `confirmed` (whether the date is officially fixed; the Japanese equinox days and UK bank holidays after the last published year are predictions). Years after the supported range are an error unless `outOfRange: "ignore"` is set, and `unconfirmedHolidays: "throw"` stops any result that would depend on a predicted holiday. Details: [Bundled holiday providers](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#bundled-holiday-providers), [Unconfirmed holidays](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#unconfirmed-holidays).
|
|
567
72
|
|
|
568
|
-
###
|
|
73
|
+
### Business-day calendars and recurring rules
|
|
569
74
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
```ts
|
|
573
|
-
import { createCalendar, type HolidayProvider } from "bizdays-toolkit";
|
|
574
|
-
|
|
575
|
-
const myCountry: HolidayProvider = {
|
|
576
|
-
id: "XX",
|
|
577
|
-
timeZone: "Europe/Paris",
|
|
578
|
-
supportedYears: { from: 2000, to: 2100 },
|
|
579
|
-
dataVersion: "2026-10",
|
|
580
|
-
holidaysInYear: (year) => [
|
|
581
|
-
{
|
|
582
|
-
date: `${year}-12-25`,
|
|
583
|
-
name: "Noël",
|
|
584
|
-
nameEn: "Christmas Day",
|
|
585
|
-
kind: "public",
|
|
586
|
-
confirmed: true,
|
|
587
|
-
},
|
|
588
|
-
],
|
|
589
|
-
};
|
|
590
|
-
const calendar = createCalendar({ holidays: myCountry, closedWeekdays: [0, 6] });
|
|
591
|
-
```
|
|
592
|
-
|
|
593
|
-
Most holidays of other countries are an nth weekday of a month (the US federal holidays) or a fixed number of days from Easter Sunday (much of Europe). `bizdays-toolkit/provider` exposes the calendar arithmetic the bundled providers use for these, so a provider does not have to reimplement and verify it: `nthWeekdayOfMonth(year, month, weekday, n)` (`n` is 1–5 or -1 for the last), `easterSunday(year)` (Gregorian; verified against GOV.UK for the UK providers), `daysInMonth(year, month)` and `isLeapYear(year)`. Combine them with `addDays` and `weekdayOf` from the root. These functions are `@beta` (they may change in a minor version while their use settles).
|
|
594
|
-
|
|
595
|
-
```ts
|
|
596
|
-
import { addDays, createCalendar, type HolidayProvider } from "bizdays-toolkit";
|
|
597
|
-
import { easterSunday, nthWeekdayOfMonth } from "bizdays-toolkit/provider";
|
|
598
|
-
|
|
599
|
-
const bavaria: HolidayProvider<"public"> = {
|
|
600
|
-
id: "DE-BY",
|
|
601
|
-
timeZone: "Europe/Berlin",
|
|
602
|
-
supportedYears: { from: 2000, to: 2100 },
|
|
603
|
-
dataVersion: "example",
|
|
604
|
-
holidaysInYear: (year) => {
|
|
605
|
-
const easter = easterSunday(year);
|
|
606
|
-
const day = (date: string, name: string, nameEn: string) => ({
|
|
607
|
-
date,
|
|
608
|
-
name,
|
|
609
|
-
nameEn,
|
|
610
|
-
kind: "public" as const,
|
|
611
|
-
confirmed: true,
|
|
612
|
-
});
|
|
613
|
-
return [
|
|
614
|
-
day(addDays(easter, -2), "Karfreitag", "Good Friday"),
|
|
615
|
-
day(addDays(easter, 1), "Ostermontag", "Easter Monday"),
|
|
616
|
-
day(addDays(easter, 60), "Fronleichnam", "Corpus Christi"),
|
|
617
|
-
];
|
|
618
|
-
},
|
|
619
|
-
};
|
|
620
|
-
createCalendar({ holidays: bavaria }).isHoliday("2026-06-04"); // true (Corpus Christi)
|
|
621
|
-
nthWeekdayOfMonth(2026, 11, 4, 4); // "2026-11-26" (the 4th Thursday of November: Thanksgiving Day)
|
|
622
|
-
nthWeekdayOfMonth(2026, 5, 1, -1); // "2026-05-25" (the last Monday of May: Memorial Day)
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
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.
|
|
626
|
-
|
|
627
|
-
```ts
|
|
628
|
-
import { jp } from "bizdays-toolkit/jp";
|
|
629
|
-
|
|
630
|
-
const zhHant: Record<string, string> = { "2026-01-01": "元日" /* … your own translations */ };
|
|
631
|
-
const jpWithNames = {
|
|
632
|
-
...jp,
|
|
633
|
-
holidaysInYear: (year: number) =>
|
|
634
|
-
jp.holidaysInYear(year).map((h) => {
|
|
635
|
-
const name = zhHant[h.date];
|
|
636
|
-
return name === undefined ? h : { ...h, names: { "zh-Hant": name } };
|
|
637
|
-
}),
|
|
638
|
-
};
|
|
639
|
-
createCalendar({ holidays: jpWithNames }).getHoliday("2026-01-01")?.names; // { "zh-Hant": "元日" }
|
|
640
|
-
```
|
|
75
|
+
A calendar is built from the holiday provider and your closures: `closedWeekdays`, `closedAnnual` (month-day ranges, across the new year too), `closedDates` and `openDates`, and recurring rules in `closedRecurring` / `openRecurring`:
|
|
641
76
|
|
|
642
|
-
|
|
77
|
+
| Rule | Meaning |
|
|
78
|
+
| --------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
79
|
+
| `{ weekday: 3, nth: 3 }` | The 3rd Wednesday (`nth` is 1–5 or -1 for the last; an array for several) |
|
|
80
|
+
| `{ weekday: 3, interval: 2, anchor: "2026-01-07" }` | Every other Wednesday, counted from the anchor |
|
|
81
|
+
| `{ day: 20 }`, `{ day: -1 }` | The 20th, the last day of the month |
|
|
82
|
+
| `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed by kind |
|
|
83
|
+
| `{ dayAfterHoliday: true }`, `{ dayBeforeHoliday: true }` | The day after (or before) a holiday or a run of holidays |
|
|
84
|
+
| `onHolidayRule: "after-holiday"` | "Closed on Mondays, or the next weekday when Monday is a holiday" (museum rule) |
|
|
85
|
+
| `effectiveFrom`, `effectiveTo` | The period the rule is in effect |
|
|
643
86
|
|
|
644
|
-
|
|
87
|
+
`dayInfo` tells you why a day is open or closed (`reason`, `name`, `rulePath`). Details: [Business-day calendars](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#business-day-calendars), [Recurring rules](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#recurring-rules), [Common recipes](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#common-recipes).
|
|
645
88
|
|
|
646
|
-
###
|
|
89
|
+
### Due dates, schedules and combined calendars
|
|
647
90
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
import { jp } from "bizdays-toolkit/jp";
|
|
653
|
-
|
|
654
|
-
const strict = createCalendar({
|
|
655
|
-
holidays: jp,
|
|
656
|
-
closedWeekdays: [0, 6],
|
|
657
|
-
unconfirmedHolidays: "throw",
|
|
658
|
-
});
|
|
659
|
-
try {
|
|
660
|
-
strict.dueDate("2028-03", { day: 20, convention: "following" });
|
|
661
|
-
} catch (e) {
|
|
662
|
-
if (isBizdaysError(e) && e.code === "unconfirmed-holiday") {
|
|
663
|
-
// Recompute after the Official Gazette of February 2028; these are the days involved
|
|
664
|
-
strict
|
|
665
|
-
.holidaysBetween("2028-01-01", "2028-12-31")
|
|
666
|
-
.filter((h) => !h.confirmed)
|
|
667
|
-
.map((h) => h.date); // ["2028-03-20", "2028-09-22"]
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
- It matters only when the results depend on holidays. With `holidayPolicy: "ignore"` (or `holiday: "weekday"` in a schedule) and no holiday-based rule (`holiday`, `dayAfterHoliday`, `dayBeforeHoliday`, `"after-holiday"`), the results do not change, but `dayInfo` still looks up the holiday it reports and throws. Option diagnostics flag such options as `redundant`
|
|
673
|
-
- Combined calendars throw if any of their calendars throws; a calendar made from a schedule follows the schedule
|
|
674
|
-
- The option catches holidays that are predicted, not holidays that are unknown: a one-off UK bank holiday announced later is absent from the predictions, and Singapore has no predictions at all (later years are `out-of-range`)
|
|
675
|
-
|
|
676
|
-
### Errors
|
|
677
|
-
|
|
678
|
-
Invalid options and inputs throw ordinary exceptions such as `RangeError` and `TypeError`, with a `code`. Messages may change, so branch on `code`.
|
|
679
|
-
|
|
680
|
-
```ts
|
|
681
|
-
import { createCalendar, isBizdaysError } from "bizdays-toolkit";
|
|
682
|
-
|
|
683
|
-
try {
|
|
684
|
-
createCalendar({ closedDates: ["2026-12-32"] });
|
|
685
|
-
} catch (error) {
|
|
686
|
-
if (isBizdaysError(error) && error.code === "invalid-date") {
|
|
687
|
-
// An invalid date (the message includes the location in the options, "closedDates[0].date")
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
Main codes: `invalid-date`, `invalid-input`, `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.
|
|
91
|
+
- `dueDate("2026-11", { day: 23, convention: "following" })`, `adjust`, `closingMonth` and `closingPeriod` compute month-based due dates and closing periods with the `following`, `preceding`, `modified-following` and `modified-preceding` conventions ([Month-based due dates](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#month-based-due-dates-and-date-adjustment))
|
|
92
|
+
- `createSchedule` gives a value per day (store hours, a shipping cutoff, a price band) from the same kinds of rules, and `createCalendarFromSchedule` turns such a schedule into a calendar so the hours are the only configuration ([Day-by-day values](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#day-by-day-values-eg-store-hours), [A calendar from a schedule](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#a-business-day-calendar-from-a-schedule))
|
|
93
|
+
- `combineCalendars` builds the days on which all (or any) of several calendars are open, such as your company and the bank for a remittance date ([Combining calendars](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#combining-calendars))
|
|
94
|
+
- Dates can be given as `"YYYY-MM-DD"`, `"YYYY-MM-DDTHH:mm"`, `Date` or Temporal objects; instants are converted in the calendar's time zone ([Date and date-time inputs](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#date-and-date-time-inputs))
|
|
693
95
|
|
|
694
96
|
### Option diagnostics
|
|
695
97
|
|
|
696
|
-
`diagnoseCalendar` and `diagnoseSchedule` from `bizdays-toolkit/diagnose` examine options and return a list of problems. They never throw for a problem in the options (an empty array means no problems); only a wrong second argument (see `asOf` below) throws.
|
|
697
|
-
|
|
698
98
|
```ts
|
|
699
99
|
import { diagnoseCalendar } from "bizdays-toolkit/diagnose";
|
|
700
100
|
import { jp } from "bizdays-toolkit/jp";
|
|
@@ -702,74 +102,25 @@ import { jp } from "bizdays-toolkit/jp";
|
|
|
702
102
|
diagnoseCalendar({
|
|
703
103
|
holidays: jp,
|
|
704
104
|
closedWeekdays: [0],
|
|
705
|
-
closedRecurring: [
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
],
|
|
710
|
-
closedDates: ["2100-01-05"], // outside the supported years of Japanese holidays (1955–2099)
|
|
711
|
-
closeAnnual: ["12-31"], // a typo
|
|
712
|
-
});
|
|
713
|
-
// [
|
|
714
|
-
// { severity: "warning", code: "unknown-option", path: "closeAnnual", message: '… (did you mean "closedAnnual"?)' },
|
|
715
|
-
// { severity: "warning", code: "out-of-range", path: "closedDates[0]", relatedPath: "holidays", message: "…" },
|
|
716
|
-
// { severity: "warning", code: "shadowed", path: "closedRecurring[1]", relatedPath: "closedRecurring[0]", message: "…" },
|
|
717
|
-
// { severity: "info", code: "redundant", path: "closedRecurring[2]", message: "…" },
|
|
718
|
-
// ]
|
|
105
|
+
closedRecurring: [{ weekday: 0 }],
|
|
106
|
+
closeAnnual: ["12-31"],
|
|
107
|
+
}).map((d) => [d.severity, d.code, d.path]);
|
|
108
|
+
// [["warning", "unknown-option", "closeAnnual"], ["info", "redundant", "closedRecurring[0]"]]
|
|
719
109
|
```
|
|
720
110
|
|
|
721
|
-
|
|
722
|
-
| ---------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
723
|
-
| `error` | The same codes as exceptions | Errors that make `createCalendar` or `createSchedule` throw. Creation stops at the first one; diagnostics return them all |
|
|
724
|
-
| `error` | `invalid-provider-data` | Errors in the data returned by a holiday provider. Creation checks only the years used; diagnostics check every supported year |
|
|
725
|
-
| `warning` | `unknown-option` | Unknown keys (ignored at creation). If a close known key exists, `message` suggests it |
|
|
726
|
-
| `warning` | `shadowed` | A rule fully covered by an earlier rule of the same type (`relatedPath`) and never used |
|
|
727
|
-
| `warning` | `never-matches` | A recurring rule that matches no day (such as `{ day: 30, months: [2] }`, a 5th Wednesday with an effective period that has none, or `dayAfterHoliday` without a holiday provider) |
|
|
728
|
-
| `warning` | `out-of-range` | A specific date, or the effective period of a rule, outside the supported years of the holiday provider (`relatedPath`) |
|
|
729
|
-
| `warning` | `no-business-day` | No business day at all (calendars only; searching for a business day throws) |
|
|
730
|
-
| `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), or `unconfirmedHolidays: "throw"` in options whose results do not use holidays |
|
|
731
|
-
| `info` | `expired` | Only with `asOf` (below): a rule or date that ended before the given day |
|
|
732
|
-
|
|
733
|
-
- Branch on `code`. `message` is in English and its wording may change. Future versions may add codes
|
|
734
|
-
- When there is an `error`, the rule relation checks (`shadowed`, `redundant` and so on) are skipped
|
|
735
|
-
- `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`
|
|
736
|
-
- `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)
|
|
737
|
-
- `shadowed` for `dayAfterHoliday` rules is checked only against other `dayAfterHoliday` rules (by `months` and period), because which days they match depend on the holidays of each year; overlap with weekday rules is found as `redundant`
|
|
738
|
-
- `shadowed` for rules with effective periods also requires the earlier rule's period to contain the later rule's period, and for `interval` rules the earlier rule's cycle to contain the later one (a weekly rule covers an every-other-week rule; two every-other-week rules with opposite phases do not cover each other)
|
|
739
|
-
- `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; with `interval` rules the range is extended to the length of their cycle, such as 56 years for every other week). Effective periods outside that range are checked as well (with `outOfRange: "throw"`, only within the supported years). `message` names the years checked. Schedule values are compared by content for arrays and plain objects, and by identity otherwise (such as `Date`)
|
|
740
|
-
- 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
|
|
741
|
-
|
|
742
|
-
**Finding rules that have ended** (`asOf`): options accumulate over the years, for example in a multi-store service that stores each store's options as JSON. Pass `{ asOf: "YYYY-MM-DD" }` as the second argument and every rule with an `effectiveTo` before that day, and every specific date or range that ends before it, is returned as `info` with code `expired` (independent of `redundant`: these rules did affect the past). Without `asOf` nothing is returned as `expired`, so a test such as `assert.deepEqual(diagnoseCalendar(options), [])` does not drift with the date. `asOf` takes only a date string (a `Date` would need a time zone); other values throw `invalid-date`, a second argument that is not an object throws `invalid-argument`, and unknown keys in it are ignored.
|
|
111
|
+
`diagnoseCalendar` and `diagnoseSchedule` return every problem in the options without throwing: typos (with a suggestion), rules that never match, rules shadowed by earlier ones, rules whose removal changes nothing, options with no business day, and, with `{ asOf }`, rules that have ended. `assert.deepEqual(diagnoseCalendar(options), [])` in a test keeps options honest. Details: [Option diagnostics](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#option-diagnostics), [Errors](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#errors).
|
|
743
112
|
|
|
744
|
-
|
|
745
|
-
import { diagnoseCalendar } from "bizdays-toolkit/diagnose";
|
|
746
|
-
import { jp } from "bizdays-toolkit/jp";
|
|
113
|
+
### Other countries and regions
|
|
747
114
|
|
|
748
|
-
|
|
749
|
-
{
|
|
750
|
-
holidays: jp,
|
|
751
|
-
closedRecurring: [
|
|
752
|
-
{ weekday: 1, name: "Closed on Mondays", effectiveTo: "2027-03-31" },
|
|
753
|
-
{ weekday: 2, name: "Closed on Tuesdays", effectiveFrom: "2027-04-01" },
|
|
754
|
-
],
|
|
755
|
-
closedDates: ["2026-02-10"],
|
|
756
|
-
},
|
|
757
|
-
{ asOf: "2028-01-01" },
|
|
758
|
-
).map((d) => [d.code, d.path]); // [["expired", "closedDates[0]"], ["expired", "closedRecurring[0]"]]
|
|
759
|
-
```
|
|
760
|
-
|
|
761
|
-
Keeping options safe with a test:
|
|
115
|
+
A holiday provider is an object with `id`, `timeZone`, `supportedYears` and `holidaysInYear(year)`. `bizdays-toolkit/provider` exposes `nthWeekdayOfMonth` and `easterSunday`, which most countries' holidays are built from. See [providers.md](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/providers.md).
|
|
762
116
|
|
|
763
|
-
|
|
764
|
-
import assert from "node:assert/strict";
|
|
765
|
-
import { test } from "node:test";
|
|
766
|
-
import { diagnoseCalendar } from "bizdays-toolkit/diagnose";
|
|
767
|
-
import { shippingOptions } from "../src/calendar.ts";
|
|
117
|
+
## Documentation
|
|
768
118
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
119
|
+
- [Guide](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md): the full description of every feature, with examples run in CI
|
|
120
|
+
- [Writing a holiday provider](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/providers.md)
|
|
121
|
+
- [Migration guide](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/migration.md) (Japanese) and [CHANGELOG](https://github.com/inohan77/bizdays-toolkit/blob/main/CHANGELOG.md)
|
|
122
|
+
- [Design decisions](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/decisions.md) (Japanese): why things are the way they are
|
|
123
|
+
- [RELEASING.md](https://github.com/inohan77/bizdays-toolkit/blob/main/RELEASING.md) (Japanese): versioning, compatibility promise, data updates and monitoring
|
|
773
124
|
|
|
774
125
|
## Versioning and data updates
|
|
775
126
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bizdays-toolkit",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
4
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; Singapore public holidays verified against data.gov.sg. / 祝日と営業日のカレンダー(タイムゾーン対応・日本の祝日は内閣府、英国の bank holidays は GOV.UK、シンガポールの祝日は data.gov.sg のデータで全日照合)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bank-holidays",
|