bizdays-toolkit 0.15.0 → 0.17.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 CHANGED
@@ -4,6 +4,59 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.17.0] - 2026-10-25
8
+
9
+ ### 移行
10
+
11
+ 動作が変わる既存の API はない(既定値はこれまでどおり)。`Calendar`・`Schedule`・`HolidayLookup` に項目 `unconfirmedHolidays` が加わり、`createCalendar`・`createSchedule` の設定と `diagnoseCalendar`・`diagnoseSchedule` の第 2 引数が増える。監視のワークフローの改名(`govuk-watch.yml` → `data-watch.yml`)により、cron-job.org の URL を直す(RELEASING.md)。版ごとの作業は [docs/migration.md](./docs/migration.md) にまとめた。
12
+
13
+ ### Added
14
+
15
+ - 未確定の祝日のガード `unconfirmedHolidays: "accept" | "throw"`(カレンダー・スケジュール共通。既定 `"accept"`)を追加した。`"throw"` では、評価した日(探索が途中で通る日、祝日の翌日・祝前日の規則が見る隣の日を含む)が未確定の祝日(`confirmed: false`)なら `code: "unconfirmed-holiday"` の `RangeError` を投げる。`holidaysInYear`・`holidaysBetween` は投げないので、影響する日を列挙できる。合成はどれかが `"throw"` なら `"throw"`。エラーコードに `unconfirmed-holiday` を追加した
16
+ - 診断の第 2 引数 `{ asOf: "YYYY-MM-DD" }`(型 `DiagnoseOptions`)を追加した。基準日より前に終わった規則・個別の日付を `info` の `expired` で返す。省略時の結果は変わらない。診断コードに `expired` を追加した
17
+ - シンガポールの祝日 `bizdays-toolkit/sg`(提供元 `sg`。ID `SG`、`Asia/Singapore`、種別 `regular`・`substitute`・`special`、型 `SgHoliday`・`SgHolidayKind`・`KnownSgHolidayKind`)を追加した。MOM が告示した 2020〜2027 年の日付を表で持ち、日曜日に当たった祝日の振替を Holidays Act の規則で計算する。それより後の年は予測せず `out-of-range`。data.gov.sg のコレクション 691(MOM)で全日照合し、2022 年の既知の差分(改定後の告示との食い違い)はフィクスチャに出典付きで記録した
18
+
19
+ ### Changed
20
+
21
+ - 診断: 判定に祝日を使わない設定(提供元がない、または `holidayPolicy: "ignore"` / `holiday: "weekday"` で祝日に従属する規則もない)の `unconfirmedHolidays: "throw"` を `info` の `redundant` にする。診断は未確定の祝日を `"accept"` で評価する
22
+ - 診断: 第 2 引数がオブジェクトでなければ `invalid-argument`、`asOf` が暦日の文字列でなければ `invalid-date` の例外を投げる(設定の誤りは診断結果で返す現状のまま)
23
+ - 振替元をさかのぼるときに祝日の例外を飲み込む範囲を `out-of-range` だけにした(`unconfirmed-holiday` などは投げ直す。これまでは提供元の例外も飲み込んでいた)
24
+ - README: シンガポールの節、「未確定の祝日」の節(レシピ付き)、診断の `asOf` と `expired`、出典にシンガポールと Singapore Open Data Licence を足した。サイズの値を測り直した(ルート約 10.0KB・日本の祝日込み約 11.8KB・英国の祝日込み約 11.7KB・シンガポールの祝日込み約 10.9KB。0.17.0 で測定)
25
+ - `RELEASING.md`: 「シンガポールのデータの更新」の節を足し、監視のワークフローの改名と cron-job.org の URL の修正を書いた
26
+ - `docs/decisions.md`: 44 番(未確定の祝日のガード・診断の基準日・シンガポールの祝日)を追加し、15・28 番に監視のワークフローの改名を書いた
27
+
28
+ ### 開発
29
+
30
+ - 監視のワークフローを `govuk-watch.yml` から `data-watch.yml` に改名し、英国とシンガポールの照合を 1 回の起動で順に行う(issue のラベルは `govuk-data`・`datagovsg-data`。ハートビートは両方が動いたときだけ)。`scripts/verify-datagovsg.ts`(`pnpm verify:datagovsg`。`--file`・`--write`・`--report`・`--simulate`)とフィクスチャ `test/fixtures/datagovsg-public-holidays.json`(既知の差分を含む)を追加した
31
+ - テスト: `test/unconfirmed-holidays.test.ts`・`test/diagnose-as-of.test.ts`・`test/sg-rules.test.ts`・`test/sg-datagovsg.test.ts` を追加し、`smoke.mjs`・CJS・利用者の型チェックにシンガポールと新しい設定を足した
32
+ - サイズの上限(`size-limits.json`)を上げ、`./sg` のエントリーを足した。ルート 24,000 → 25,000、`./jp` 27,000 → 28,000、`./uk` 6,700 → 7,500、`./diagnose` 30,500 → 32,000(gzip)。未確定の祝日のガードと診断の基準日のため。`./sg` は 6,000(日付とエラーの共通の実装を含む)
33
+
34
+ ## [0.16.0] - 2026-10-23
35
+
36
+ ### 移行
37
+
38
+ 日時の文字列の区切りは `T` だけになり、空白で区切った文字列(`"2026-11-23 10:00"`)は `invalid-time` の Error になる(これまでは実行時だけ受け付けていた。型 `DateTimeInput` は以前から `T` 区切りだけ)。合成した `dayInfo` に `decidedByPath` が加わるので、`deepEqual` で比べているテストは期待値に足す。ほかは動作が変わらない(祝前日の規則・サブパス・合成の項目の追加)。版ごとの作業は [docs/migration.md](./docs/migration.md) にまとめた。
39
+
40
+ ### Added
41
+
42
+ - 繰り返しの規則に祝前日の形 `{ dayBeforeHoliday: true }` を追加した(型 `DayBeforeHolidayPattern`)。翌日が提供元の祝日でその日自身は祝日でない日に当てはまり、連休なら連休の初日の前日だけ。`{ dayAfterHoliday: true }` の鏡像で、`weekdays`・`months`・有効期間を付けられ、`onHolidayRule` は `invalid-option`。宿泊の祝前日料金・飲食店の祝前日の営業時間・祝前日の出荷の締めの繰り上げを規則として書ける。診断の `never-matches`・`shadowed` も扱う
43
+ - 祝日の提供元を実装する人向けのサブパス `bizdays-toolkit/provider` を追加した(`@beta`)。内部で使っている暦の計算 `easterSunday(year)`(グレゴリオ暦の復活祭。英国の祝日で GOV.UK のデータと照合済み)、`nthWeekdayOfMonth(year, month, weekday, n)`(`n` は 1〜5 と -1(最後))、`daysInMonth`、`isLeapYear` を、入力の検証を付けて公開する。米国の連邦祝日やドイツ・豪州の州の祝日のように、第 n 曜日と復活祭で決まる提供元が書ける
44
+ - 合成したカレンダーの `dayInfo` に `decidedByPath`(判定を決めたカレンダーまでの番号の列。外側から順)を追加した。合成したカレンダーをさらに合成したとき、`rulePath` がどのカレンダーの設定の位置かを結果だけから辿れる。`decidedBy` は外側の番号のまま
45
+
46
+ ### Changed
47
+
48
+ - 日時の文字列の区切りを `T` だけにし、空白で区切った文字列は `invalid-time` にした(上の「移行」)
49
+ - README: 繰り返しの規則の節に祝前日、他の国・地域の祝日の節に `bizdays-toolkit/provider` と復活祭で決まる提供元の例、合成の節に `decidedByPath`、日付・日時の入力の節に区切りの条件、確かめている環境に Bun・Deno を足した。サイズの値を測り直した(ルート約 9.8KB・日本の祝日込み約 11.6KB・英国の祝日込み約 11.5KB。0.16.0 で測定)
50
+ - `docs/decisions.md`: 14 番に年ごとの列挙で足りる理由、26 番に Bun・Deno、29 番に空白区切りの廃止、33 番に祝前日を足した経緯、36 番に曜日の日に `rulePath` を付けない理由の書き直し(位置は書けるが冗長)、41 番に次の月に併合する側を選んだ理由を書き、43 番(祝前日・provider・`decidedByPath`・空白区切りの廃止・Bun と Deno の確認)を追加した
51
+ - `RELEASING.md`: 確認の表と固定する版の一覧に `bun`・`deno` ジョブを足した
52
+
53
+ ### 開発
54
+
55
+ - CI に `bun`・`deno` ジョブを追加した。公開する `dist` を Bun 1.4.3 と Deno 2.9.7(`--unstable-temporal`。本物の Temporal も確かめる)で、`test/compat/runtime.mjs` から `smoke.mjs` を実行して確かめる。どちらも版と SHA-256 を固定した公式のリリースを取得する(workerd と同じ方式)。`smoke.mjs` に `bizdays-toolkit/provider` の確認を足した(Node.js・ブラウザー・workerd・CJS の確認も同じものを呼ぶ)
56
+ - `easterSunday` を `src/uk/rules.ts` から `src/core/date.ts` に移した(結果は変わらない。provider のサブパスが英国のデータを引き込まないため)
57
+ - 規則の診断の性質のテストの生成器に祝前日を加えた。`test/day-before-holiday.test.ts`・`test/provider.test.ts` を新規に足し、利用者の型チェックに祝前日・`decidedByPath`・provider を足した
58
+ - サイズの上限(`size-limits.json`)を上げ、`./provider` のエントリーを足した。ルート 23,000 → 24,000、`./jp` 26,000 → 27,000、`./diagnose` 29,500 → 30,500(gzip)。祝前日の検証・判定・診断と `decidedByPath` のため。`./provider` は 4,500(日付とエラーの共通の実装を含む)
59
+
7
60
  ## [0.15.0] - 2026-10-21
8
61
 
9
62
  ### 移行
@@ -405,7 +458,9 @@
405
458
 
406
459
  - 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
407
460
 
408
- [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.15.0...HEAD
461
+ [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.0...HEAD
462
+ [0.17.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.16.0...v0.17.0
463
+ [0.16.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.15.0...v0.16.0
409
464
  [0.15.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.14.1...v0.15.0
410
465
  [0.14.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.14.0...v0.14.1
411
466
  [0.14.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.13.0...v0.14.0
package/README.md CHANGED
@@ -21,6 +21,7 @@ A business-day calendar that combines public holidays with company or store clos
21
21
 
22
22
  - **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)
23
23
  - **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
24
+ - **Singapore public holidays verified day by day against data.gov.sg**: the dates gazetted by the Ministry of Manpower (2020 through the last year gazetted), with the days in lieu for holidays on a Sunday, are checked against the MOM collection on data.gov.sg in the tests, and differences are watched daily
24
25
  - **Recurring rules**: rules such as "closed on the 3rd Wednesday", "closed every other Wednesday", "stocktaking on the last day of the month", "closed on Mondays (on the next weekday if Monday is a holiday)" or "closed on the day after a holiday", together with what happens when they fall on a holiday (follow the holiday, apply anyway, or move to the next day). Rules can have an effective period, so "closed on Tuesdays instead of Mondays from April 2027" is one calendar
25
26
  - **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
26
27
  - **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
@@ -28,9 +29,9 @@ A business-day calendar that combines public holidays with company or store clos
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
29
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
30
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
31
- - **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
32
- - **Lightweight, zero dependencies**: about 9.7 KB min+gzip (about 11.5 KB with Japanese holidays, about 11.4 KB with UK bank holidays; measured for 0.15.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 and Cloudflare Workers
33
- - **Extensible to other countries and regions**: holiday providers are pluggable through the `HolidayProvider` interface. Japan and the UK are bundled
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
34
35
 
35
36
  ## Installation
36
37
 
@@ -38,7 +39,7 @@ A business-day calendar that combines public holidays with company or store clos
38
39
  pnpm add bizdays-toolkit
39
40
  ```
40
41
 
41
- Node.js 22.12 or later. CI tests the package on Node.js 22.12, 22, 24 and 26, on Chrome, Firefox and Edge (Linux) and Safari (macOS), and on Cloudflare Workers (workerd, the open-source Workers runtime). The browsers are the latest stable versions on GitHub's runners, so their versions change as the runners are updated. The package is ESM only, but it can also be loaded from CommonJS with `require()` (tested in CI as well; on Node.js 22.12, `require()` prints an experimental-feature warning, which does not affect the result, and Node.js 26 prints none). Other runtimes (such as Deno, Bun or Vercel's Edge Runtime) are not tested.
42
+ Node.js 22.12 or later. CI tests the package on Node.js 22.12, 22, 24 and 26, on Chrome, Firefox and Edge (Linux) and Safari (macOS), on Cloudflare Workers (workerd, the open-source Workers runtime), and on Bun (1.4) and Deno (2.9). The browsers are the latest stable versions on GitHub's runners, so their versions change as the runners are updated. The package is ESM only, but it can also be loaded from CommonJS with `require()` (tested in CI as well; on Node.js 22.12, `require()` prints an experimental-feature warning, which does not affect the result, and Node.js 26 prints none). Vercel's Edge Runtime is not tested.
42
43
 
43
44
  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.
44
45
 
@@ -93,6 +94,23 @@ const ukWide = createCalendar({ holidays: [england, scotland, northernIreland] }
93
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
94
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
95
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
+
96
114
  ### Business-day calendars
97
115
 
98
116
  ```ts
@@ -137,9 +155,9 @@ Each day is decided in this order of priority (top first).
137
155
 
138
156
  This order is fixed. Only the relation between a recurring rule and holidays can be chosen per rule with `onHolidayRule` (next section).
139
157
 
140
- ### Recurring rules (weekly, every other week, nth weekday, day of month, holidays, day after a holiday)
158
+ ### Recurring rules (weekly, every other week, nth weekday, day of month, holidays, day after or before a holiday)
141
159
 
142
- 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" or "closed on the day after a holiday" go in `closedRecurring` and `openRecurring`.
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`.
143
161
 
144
162
  ```ts
145
163
  const museum = createCalendar({
@@ -174,6 +192,7 @@ museum.isBusinessDay("2026-05-07"); // false (the day after the May 3–6 run of
174
192
  | `{ day: 20 }` | The 20th of every month. `day` is 1–31 or -1 (last day); an array gives several |
175
193
  | `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed to the given kinds (`Holiday.kind`); judged before `holidayPolicy` (see below) |
176
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) |
177
196
  | `months: [8, 12]` | Narrows any pattern to these months |
178
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) |
179
198
 
@@ -182,6 +201,7 @@ museum.isBusinessDay("2026-05-07"); // false (the day after the May 3–6 run of
182
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`
183
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
184
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`)
185
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
186
206
 
187
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).
@@ -229,8 +249,15 @@ const hours = createSchedule<Hours>({
229
249
  recurring: [
230
250
  { weekday: 1, value: null, name: "Closed on Mondays", onHolidayRule: "after-holiday" },
231
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
+ },
232
257
  ],
233
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)
234
261
  ```
235
262
 
236
263
  ### Combining calendars
@@ -256,7 +283,7 @@ const bank = createCalendar({
256
283
  const remittance = combineCalendars([company, bank], { businessDay: "all" });
257
284
  remittance.addBusinessDays("2026-12-28", 2); // "2027-01-05"
258
285
  remittance.dayInfo("2026-12-29");
259
- // { date: "2026-12-29", weekday: 2, businessDay: false, reason: "closed-annual", name: "New Year closure", rulePath: "closedAnnual[0]", decidedBy: 0 }
286
+ // { date: "2026-12-29", weekday: 2, businessDay: false, reason: "closed-annual", name: "New Year closure", rulePath: "closedAnnual[0]", decidedBy: 0, decidedByPath: [0] }
260
287
 
261
288
  // Any of several stores is open
262
289
  const anyStore = combineCalendars([company, bank], { businessDay: "any" });
@@ -264,6 +291,7 @@ anyStore.isBusinessDay("2026-12-29"); // true
264
291
  ```
265
292
 
266
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
267
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
268
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
269
297
  - A combined calendar can be combined again. Option diagnostics work on options, not on calendars, so diagnose each calendar's options separately
@@ -513,7 +541,7 @@ Every method accepts the following inputs. Returned dates are always `"YYYY-MM-D
513
541
  | Temporal `PlainDate`, `PlainDateTime` | A civil date or date and time (no dependency on Temporal) |
514
542
  | Temporal `Instant`, `ZonedDateTime` | An instant |
515
543
 
516
- 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).
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).
517
545
 
518
546
  The calendar's time zone is the `timeZone` option, then the holiday provider's time zone, then `"UTC"`.
519
547
 
@@ -562,6 +590,38 @@ const myCountry: HolidayProvider = {
562
590
  const calendar = createCalendar({ holidays: myCountry, closedWeekdays: [0, 6] });
563
591
  ```
564
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
+
565
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.
566
626
 
567
627
  ```ts
@@ -581,7 +641,37 @@ createCalendar({ holidays: jpWithNames }).getHoliday("2026-01-01")?.names; // {
581
641
 
582
642
  ### Outside the supported years
583
643
 
584
- 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"`.
644
+ Evaluating a date outside the holiday provider's supported years (1955–2099 for Japan, 2012–2099 for the UK, 2020–2027 for Singapore) throws a `RangeError`. To treat such dates as having no holidays, set `outOfRange: "ignore"`.
645
+
646
+ ### Unconfirmed holidays
647
+
648
+ Years after the provider's `confirmedThrough` may contain predicted holidays (`confirmed: false`): the Japanese equinox days, whose dates are published in the Official Gazette every February for the next year, and UK bank holidays after the last year GOV.UK has published. By default the predictions are used. With `unconfirmedHolidays: "throw"` (next to `outOfRange`, on calendars and schedules), any evaluation that looks up an unconfirmed holiday throws a `RangeError` with `code: "unconfirmed-holiday"`, so that a result which would depend on a prediction, such as a due date in a five-year repayment schedule or a date written into a contract, is never produced silently. "Any evaluation" includes the days a search passes through (`addBusinessDays`, `adjust`, `dueDate`, `closingMonth` and so on) and the neighbouring day a `dayAfterHoliday` or `dayBeforeHoliday` rule looks at, not only the day asked about. `holidaysInYear` and `holidaysBetween` never throw, so they are the way out: after catching the error, list the days that are affected.
649
+
650
+ ```ts
651
+ import { createCalendar, isBizdaysError } from "bizdays-toolkit";
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`)
585
675
 
586
676
  ### Errors
587
677
 
@@ -603,7 +693,7 @@ Main codes: `invalid-date`, `invalid-input`, `invalid-month`, `invalid-month-day
603
693
 
604
694
  ### Option diagnostics
605
695
 
606
- `diagnoseCalendar` and `diagnoseSchedule` from `bizdays-toolkit/diagnose` examine options and return a list of problems. They never throw (an empty array means no problems).
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.
607
697
 
608
698
  ```ts
609
699
  import { diagnoseCalendar } from "bizdays-toolkit/diagnose";
@@ -628,16 +718,17 @@ diagnoseCalendar({
628
718
  // ]
629
719
  ```
630
720
 
631
- | `severity` | `code` | Meaning |
632
- | ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
633
- | `error` | The same codes as exceptions | Errors that make `createCalendar` or `createSchedule` throw. Creation stops at the first one; diagnostics return them all |
634
- | `error` | `invalid-provider-data` | Errors in the data returned by a holiday provider. Creation checks only the years used; diagnostics check every supported year |
635
- | `warning` | `unknown-option` | Unknown keys (ignored at creation). If a close known key exists, `message` suggests it |
636
- | `warning` | `shadowed` | A rule fully covered by an earlier rule of the same type (`relatedPath`) and never used |
637
- | `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) |
638
- | `warning` | `out-of-range` | A specific date, or the effective period of a rule, outside the supported years of the holiday provider (`relatedPath`) |
639
- | `warning` | `no-business-day` | No business day at all (calendars only; searching for a business day throws) |
640
- | `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) |
721
+ | `severity` | `code` | Meaning |
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 |
641
732
 
642
733
  - Branch on `code`. `message` is in English and its wording may change. Future versions may add codes
643
734
  - When there is an `error`, the rule relation checks (`shadowed`, `redundant` and so on) are skipped
@@ -648,6 +739,25 @@ diagnoseCalendar({
648
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`)
649
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
650
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.
743
+
744
+ ```ts
745
+ import { diagnoseCalendar } from "bizdays-toolkit/diagnose";
746
+ import { jp } from "bizdays-toolkit/jp";
747
+
748
+ diagnoseCalendar(
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
+
651
761
  Keeping options safe with a test:
652
762
 
653
763
  ```ts
@@ -665,9 +775,9 @@ test("the shipping calendar options have no problems", () => {
665
775
 
666
776
  Versioning follows [Semantic Versioning](https://semver.org/). See [RELEASING.md](https://github.com/inohan77/bizdays-toolkit/blob/main/RELEASING.md) (Japanese) for details.
667
777
 
668
- - 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 `~`
778
+ - 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, newly gazetted years and revised dates in Singapore) are released as **patch** versions, so they reach you with either `^` or `~`
669
779
  - To freeze the holiday results, pin an exact version (for example `"bizdays-toolkit": "0.8.0"`)
670
- - The reflected data is shown by the provider's `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` (`jp.dataVersion`, `england.confirmedThrough` and so on). The module-level constants `dataVersion` and `confirmedThrough` in `bizdays-toolkit/jp` and `bizdays-toolkit/uk` are deprecated since 0.12.0 and will be removed in 1.0.0
780
+ - The reflected data is shown by the provider's `dataVersion` (for Japan, the year and month of the Official Gazette reflected; for the UK and Singapore, the year and month of the GOV.UK or data.gov.sg data reflected) and `confirmedThrough` (`jp.dataVersion`, `england.confirmedThrough` and so on). The module-level constants `dataVersion` and `confirmedThrough` in `bizdays-toolkit/jp` and `bizdays-toolkit/uk` are deprecated since 0.12.0 and will be removed in 1.0.0
671
781
  - Before 1.0.0, minor versions may contain breaking changes. What you need to change when moving between minor versions is listed in the [migration guide](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/migration.md) (Japanese) and in the "移行" (migration) part of each version in the CHANGELOG
672
782
  - 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))
673
783
 
@@ -677,7 +787,9 @@ Japanese holidays are verified with the "National Holidays" CSV published on the
677
787
 
678
788
  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).
679
789
 
680
- Contains public sector information licensed under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).
790
+ Singapore public holidays are verified with the "Singapore Public Holidays" collection published by the Ministry of Manpower on data.gov.sg (<https://data.gov.sg/collections/691/view>), with the Ministry's press releases as the authority where the two differ (the verification data is a test fixture only; not included in the package).
791
+
792
+ Contains public sector information licensed under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/), and data from data.gov.sg made available under the [Singapore Open Data Licence](https://data.gov.sg/open-data-licence).
681
793
 
682
794
  ## License
683
795
 
@@ -126,6 +126,12 @@ export interface DayInfo<Kind extends string = string> {
126
126
  * reason・name・shiftedFrom・rulePath はそのカレンダーの結果。合成していないカレンダーでは付かない
127
127
  */
128
128
  readonly decidedBy?: number;
129
+ /**
130
+ * 合成したカレンダーで、判定を決めたカレンダーまでの番号の列(外側から順)。入れ子でない合成では長さ 1 で、
131
+ * 先頭は decidedBy と同じ。合成したカレンダーをさらに合成したとき、rulePath がどのカレンダーの設定の位置かを
132
+ * 結果だけから辿れる(例: [0, 1] は 0 番のカレンダーの中の 1 番)。合成していないカレンダーでは付かない
133
+ */
134
+ readonly decidedByPath?: readonly number[];
129
135
  }
130
136
  /**
131
137
  * カレンダーの合成のオプション。
@@ -268,7 +274,8 @@ export declare function createCalendar<Kind extends string = string>(options?: C
268
274
  * 営業している日を営業日とする。返すのは通常の営業日カレンダーで、営業日の加減算・調整・期日などが合成結果で動く。
269
275
  *
270
276
  * - dayInfo の reason・name・shiftedFrom・rulePath は、合成結果と同じ判定をした最初のカレンダーの結果で、
271
- * その番号を decidedBy で返す。holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
277
+ * その番号を decidedBy で返す。入れ子の合成では、内側の番号まで含めた列を decidedByPath で返す。
278
+ * holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
272
279
  * - タイムゾーンはすべてのカレンダーで同じでなければならない(time-zone-conflict)。
273
280
  * 2 つ未満、同じカレンダーを 2 回渡した場合は invalid-argument
274
281
  * - 合成したカレンダーをさらに合成することもできる。規則の診断(diagnoseCalendar)の対象にはならないので、
@@ -27,6 +27,7 @@ const CALENDAR_OPTION_KEYS = [
27
27
  "holidays",
28
28
  "timeZone",
29
29
  "outOfRange",
30
+ "unconfirmedHolidays",
30
31
  "closedWeekdays",
31
32
  "closedAnnual",
32
33
  "closedDates",
@@ -264,6 +265,7 @@ function buildCalendar(lookup, isOpen, info, maxSearchDays) {
264
265
  timeZone: lookup.timeZone,
265
266
  providers: lookup.providers,
266
267
  outOfRange: lookup.outOfRange,
268
+ unconfirmedHolidays: lookup.unconfirmedHolidays,
267
269
  dateOf,
268
270
  civilOf: lookup.civilOf,
269
271
  instantOf: lookup.instantOf,
@@ -361,7 +363,8 @@ const isCalendarLike = (v) => typeof v === "object" &&
361
363
  * 営業している日を営業日とする。返すのは通常の営業日カレンダーで、営業日の加減算・調整・期日などが合成結果で動く。
362
364
  *
363
365
  * - dayInfo の reason・name・shiftedFrom・rulePath は、合成結果と同じ判定をした最初のカレンダーの結果で、
364
- * その番号を decidedBy で返す。holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
366
+ * その番号を decidedBy で返す。入れ子の合成では、内側の番号まで含めた列を decidedByPath で返す。
367
+ * holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
365
368
  * - タイムゾーンはすべてのカレンダーで同じでなければならない(time-zone-conflict)。
366
369
  * 2 つ未満、同じカレンダーを 2 回渡した場合は invalid-argument
367
370
  * - 合成したカレンダーをさらに合成することもできる。規則の診断(diagnoseCalendar)の対象にはならないので、
@@ -400,6 +403,10 @@ export function combineCalendars(calendars, options) {
400
403
  providers: Object.freeze(providers),
401
404
  timeZone: zones[0],
402
405
  outOfRange: calendars.every((c) => c.outOfRange === "ignore") ? "ignore" : "throw",
406
+ // 未確定の祝日も同じ考え方: どれかが "throw" なら、そのカレンダーの判定が例外になるので合成の holiday も "throw"
407
+ unconfirmedHolidays: calendars.some((c) => c.unconfirmedHolidays === "throw")
408
+ ? "throw"
409
+ : "accept",
403
410
  });
404
411
  const isOpen = (date) => mode === "all"
405
412
  ? calendars.every((c) => c.isBusinessDay(date))
@@ -426,6 +433,8 @@ export function combineCalendars(calendars, options) {
426
433
  if (base.rulePath !== undefined)
427
434
  result.rulePath = base.rulePath;
428
435
  result.decidedBy = decidedBy;
436
+ // 内側が合成したカレンダーなら、その経路の前に自分の番号を付ける(入れ子でなければ長さ 1)
437
+ result.decidedByPath = Object.freeze([decidedBy, ...(base.decidedByPath ?? [])]);
429
438
  return Object.freeze(result);
430
439
  }
431
440
  return buildCalendar(lookup, isOpen, info, maxSearchDays);
@@ -52,3 +52,8 @@ export declare function diffDays(from: DateString, to: DateString): number;
52
52
  export declare function weekdayOf(date: DateString): Weekday;
53
53
  /** その月の第 n 何曜日(n は 1 始まり) */
54
54
  export declare function nthWeekdayOfMonth(year: number, month: number, weekday: Weekday, n: number): DateString;
55
+ /**
56
+ * 復活祭の日曜日(グレゴリオ暦。Anonymous Gregorian algorithm)。英国の祝日の計算に使い、提供元の実装者向けに
57
+ * bizdays-toolkit/provider からも公開する(入力の検証はそちらで行う)
58
+ */
59
+ export declare function easterSunday(year: number): DateString;
package/dist/core/date.js CHANGED
@@ -99,3 +99,24 @@ export function nthWeekdayOfMonth(year, month, weekday, n) {
99
99
  const offset = (weekday - weekdayOf(formatDate(year, month, 1)) + 7) % 7;
100
100
  return formatDate(year, month, 1 + offset + (n - 1) * 7);
101
101
  }
102
+ /**
103
+ * 復活祭の日曜日(グレゴリオ暦。Anonymous Gregorian algorithm)。英国の祝日の計算に使い、提供元の実装者向けに
104
+ * bizdays-toolkit/provider からも公開する(入力の検証はそちらで行う)
105
+ */
106
+ export function easterSunday(year) {
107
+ const a = year % 19;
108
+ const b = Math.floor(year / 100);
109
+ const c = year % 100;
110
+ const d = Math.floor(b / 4);
111
+ const e = b % 4;
112
+ const f = Math.floor((b + 8) / 25);
113
+ const g = Math.floor((b - f + 1) / 3);
114
+ const h = (19 * a + b - d - g + 15) % 30;
115
+ const i = Math.floor(c / 4);
116
+ const k = c % 4;
117
+ const l = (32 + 2 * e + 2 * i - h - k) % 7;
118
+ const m = Math.floor((a + 11 * h + 22 * l) / 451);
119
+ const month = Math.floor((h + l - 7 * m + 114) / 31);
120
+ const day = ((h + l - 7 * m + 114) % 31) + 1;
121
+ return formatDate(year, month, day);
122
+ }
@@ -8,10 +8,14 @@
8
8
  * - warning … 作れるが、意図と違う可能性が高いもの
9
9
  * unknown-option … 知らないキー(作成時は無視される)。近い既知のキーがあれば message に候補を挙げる
10
10
  * shadowed … 同じ種類の先の規則に完全に覆われ、決して使われない(relatedPath に先の規則)
11
- * never-matches … 当てはまる日がない繰り返しの規則(2 月 30 日、有効期間の中に第 5 水曜日がない、提供元のない設定の祝日の翌日、など)
11
+ * never-matches … 当てはまる日がない繰り返しの規則(2 月 30 日、有効期間の中に第 5 水曜日がない、提供元のない設定の祝日の翌日・祝前日、など)
12
12
  * out-of-range … 個別の日付、または規則の有効期間が祝日の提供元の対応年の外にある(relatedPath に提供元)
13
13
  * no-business-day … 営業日が 1 日もない設定(営業日カレンダーのみ)
14
- * - info … redundant: 取り除いても結果が変わらない規則
14
+ * - info … redundant: 取り除いても結果が変わらない規則・設定(判定に祝日を使わない設定の unconfirmedHolidays: "throw" を含む)
15
+ * expired: 第 2 引数の asOf より前に終わった規則・日付(asOf を渡したときだけ)
16
+ *
17
+ * 未確定の祝日の扱い(unconfirmedHolidays)は診断では常に "accept" で評価する。診断は設定の形を調べるもので、
18
+ * 祝日が確定しているかどうかは関係がなく、"throw" のまま評価すると例外を握りつぶして「当てはまらない」と誤判定するため
15
19
  *
16
20
  * 判定の順番(個別の日付 → 毎年の月日 → 繰り返し(over)→ 祝日 → 繰り返し(under・振替先)→ 曜日)は
17
21
  * 固定なので、shadowed は同じ種類の規則どうしに限って調べる。種類をまたいで使われない規則は
@@ -19,6 +23,7 @@
19
23
  * 有効期間(effectiveFrom・effectiveTo)のある規則は期間の包含も、周期(interval)のある規則は周期の包含も
20
24
  * shadowed の条件にする。redundant の走査の範囲は、周期がそろう長さと、範囲の外にある有効期間まで広げる
21
25
  */
26
+ import { type DateString } from "./date.ts";
22
27
  import { type CalendarOptions } from "./calendar.ts";
23
28
  import { type KnownErrorCode } from "./errors.ts";
24
29
  import { type ScheduleOptions } from "./schedule.ts";
@@ -44,7 +49,9 @@ export type KnownDiagnosticCode = KnownErrorCode
44
49
  /** 当てはまる日がない繰り返しの規則 */
45
50
  | "never-matches"
46
51
  /** 取り除いても結果が変わらない */
47
- | "redundant";
52
+ | "redundant"
53
+ /** 基準日(asOf)より前に終わった規則・日付(整理の候補) */
54
+ | "expired";
48
55
  /**
49
56
  * 診断コード。将来の版で値が増えることがあるので、分岐では「その他」も扱うこと。
50
57
  * @public
@@ -63,6 +70,18 @@ export interface Diagnostic {
63
70
  /** 関係する別の位置(shadowed では覆っている先の規則、out-of-range では祝日の提供元) */
64
71
  readonly relatedPath?: string;
65
72
  }
73
+ /** 診断を実行する。祝日の提供元の誤り(年ごとに初めて使うときに分かる)も error として返す */
74
+ /**
75
+ * 診断のオプション(第 2 引数)。
76
+ * @public
77
+ */
78
+ export interface DiagnoseOptions {
79
+ /**
80
+ * 基準日 "YYYY-MM-DD"。指定すると、この日より前に終わった規則・日付(有効期間の effectiveTo、個別の日付・期間の終わり)を
81
+ * info の expired として返す(整理の候補)。省略時は返さない(現在時刻は使わず、結果を決定的に保つ)
82
+ */
83
+ readonly asOf?: DateString;
84
+ }
66
85
  /**
67
86
  * 営業日カレンダーの設定を診断する。例外は投げず、見つけた問題を返す(問題がなければ空の配列)。
68
87
  *
@@ -70,7 +89,7 @@ export interface Diagnostic {
70
89
  * JavaScript から渡された型の合わない設定も受け付ける。
71
90
  * @public
72
91
  */
73
- export declare function diagnoseCalendar(options?: CalendarOptions | object): readonly Diagnostic[];
92
+ export declare function diagnoseCalendar(options?: CalendarOptions | object, diagnoseOptions?: DiagnoseOptions): readonly Diagnostic[];
74
93
  /**
75
94
  * スケジュールの設定を診断する。例外は投げず、見つけた問題を返す(問題がなければ空の配列)。
76
95
  *
@@ -78,4 +97,4 @@ export declare function diagnoseCalendar(options?: CalendarOptions | object): re
78
97
  * error があるときは、規則どうしの診断(shadowed・redundant など)は行わない。
79
98
  * @public
80
99
  */
81
- export declare function diagnoseSchedule(options: ScheduleOptions<unknown> | object): readonly Diagnostic[];
100
+ export declare function diagnoseSchedule(options: ScheduleOptions<unknown> | object, diagnoseOptions?: DiagnoseOptions): readonly Diagnostic[];