bizdays-toolkit 0.14.1 → 0.16.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,55 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.16.0] - 2026-10-23
8
+
9
+ ### 移行
10
+
11
+ 日時の文字列の区切りは `T` だけになり、空白で区切った文字列(`"2026-11-23 10:00"`)は `invalid-time` の Error になる(これまでは実行時だけ受け付けていた。型 `DateTimeInput` は以前から `T` 区切りだけ)。合成した `dayInfo` に `decidedByPath` が加わるので、`deepEqual` で比べているテストは期待値に足す。ほかは動作が変わらない(祝前日の規則・サブパス・合成の項目の追加)。版ごとの作業は [docs/migration.md](./docs/migration.md) にまとめた。
12
+
13
+ ### Added
14
+
15
+ - 繰り返しの規則に祝前日の形 `{ dayBeforeHoliday: true }` を追加した(型 `DayBeforeHolidayPattern`)。翌日が提供元の祝日でその日自身は祝日でない日に当てはまり、連休なら連休の初日の前日だけ。`{ dayAfterHoliday: true }` の鏡像で、`weekdays`・`months`・有効期間を付けられ、`onHolidayRule` は `invalid-option`。宿泊の祝前日料金・飲食店の祝前日の営業時間・祝前日の出荷の締めの繰り上げを規則として書ける。診断の `never-matches`・`shadowed` も扱う
16
+ - 祝日の提供元を実装する人向けのサブパス `bizdays-toolkit/provider` を追加した(`@beta`)。内部で使っている暦の計算 `easterSunday(year)`(グレゴリオ暦の復活祭。英国の祝日で GOV.UK のデータと照合済み)、`nthWeekdayOfMonth(year, month, weekday, n)`(`n` は 1〜5 と -1(最後))、`daysInMonth`、`isLeapYear` を、入力の検証を付けて公開する。米国の連邦祝日やドイツ・豪州の州の祝日のように、第 n 曜日と復活祭で決まる提供元が書ける
17
+ - 合成したカレンダーの `dayInfo` に `decidedByPath`(判定を決めたカレンダーまでの番号の列。外側から順)を追加した。合成したカレンダーをさらに合成したとき、`rulePath` がどのカレンダーの設定の位置かを結果だけから辿れる。`decidedBy` は外側の番号のまま
18
+
19
+ ### Changed
20
+
21
+ - 日時の文字列の区切りを `T` だけにし、空白で区切った文字列は `invalid-time` にした(上の「移行」)
22
+ - README: 繰り返しの規則の節に祝前日、他の国・地域の祝日の節に `bizdays-toolkit/provider` と復活祭で決まる提供元の例、合成の節に `decidedByPath`、日付・日時の入力の節に区切りの条件、確かめている環境に Bun・Deno を足した。サイズの値を測り直した(ルート約 9.8KB・日本の祝日込み約 11.6KB・英国の祝日込み約 11.5KB。0.16.0 で測定)
23
+ - `docs/decisions.md`: 14 番に年ごとの列挙で足りる理由、26 番に Bun・Deno、29 番に空白区切りの廃止、33 番に祝前日を足した経緯、36 番に曜日の日に `rulePath` を付けない理由の書き直し(位置は書けるが冗長)、41 番に次の月に併合する側を選んだ理由を書き、43 番(祝前日・provider・`decidedByPath`・空白区切りの廃止・Bun と Deno の確認)を追加した
24
+ - `RELEASING.md`: 確認の表と固定する版の一覧に `bun`・`deno` ジョブを足した
25
+
26
+ ### 開発
27
+
28
+ - 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 の確認も同じものを呼ぶ)
29
+ - `easterSunday` を `src/uk/rules.ts` から `src/core/date.ts` に移した(結果は変わらない。provider のサブパスが英国のデータを引き込まないため)
30
+ - 規則の診断の性質のテストの生成器に祝前日を加えた。`test/day-before-holiday.test.ts`・`test/provider.test.ts` を新規に足し、利用者の型チェックに祝前日・`decidedByPath`・provider を足した
31
+ - サイズの上限(`size-limits.json`)を上げ、`./provider` のエントリーを足した。ルート 23,000 → 24,000、`./jp` 26,000 → 27,000、`./diagnose` 29,500 → 30,500(gzip)。祝前日の検証・判定・診断と `decidedByPath` のため。`./provider` は 4,500(日付とエラーの共通の実装を含む)
32
+
33
+ ## [0.15.0] - 2026-10-21
34
+
35
+ ### 移行
36
+
37
+ `businessDayOnOrAfter` に `cutoff` を指定するときは、時刻を持つ入力(`Date`・日時の文字列・時刻を持つ Temporal の値)を渡す。時刻のない入力(暦日の文字列・`PlainDate`)との組み合わせは、これまで暦日を 0 時として扱い締め時刻を黙って無視していたが、`invalid-argument` の Error になる。ほかは動作が変わらない(規則の絞り込み・カレンダーのメソッド・型の追加)。版ごとの作業は [docs/migration.md](./docs/migration.md) にまとめた。
38
+
39
+ ### Added
40
+
41
+ - 祝日の規則(`{ holiday: true }`)と祝日の翌日の規則(`{ dayAfterHoliday: true }`)に、曜日の絞り込み `weekdays`(`months`・`kinds` と同じ絞り込みの配列)を追加した。「祝日の翌日は休館(土日に当たる場合は開館)」は `{ dayAfterHoliday: true, weekdays: [1, 2, 3, 4, 5] }`、「土曜の祝日は午前診療」は `{ holiday: true, weekdays: [6], value: … }`。曜日・日の規則に `weekdays` を付けると `invalid-option`。診断の `never-matches`・`shadowed` も曜日の絞り込みを扱う
42
+ - 営業日カレンダーに、営業日を考慮した締め日 `closingMonth(input, closingDay, { convention })`・`closingPeriod(monthOrDate, closingDay, { convention })` を追加した。各月の締め日を `adjust` と同じ `convention`(必須)で営業日に調整し、日付は調整後の締め日がその日以降になる最初の月に属する。前月の締め日から当月の締め日までに営業日がない月(長期休業)は固有の期間を持たず、その日は次の月に属し、その月の `closingPeriod` は `no-business-day`。合成したカレンダー・スケジュールからのカレンダーにも付く。ルートの `closingMonth`・`closingPeriod` は暦日のまま
43
+ - `HolidayPattern`・`RecurringPattern`・`RecurringRule`・`RecurringValue` に祝日の種別の型引数 `Kind`(既定 `string`)を追加し、`kinds` を `readonly Kind[]` にした。`holidays: jp` のカレンダー・スケジュールでは `kinds` に日本の祝日の種別の補完が効く(型は開いているので、ほかの文字列も通る)。型引数なしの利用はこれまでどおり
44
+
45
+ ### Changed
46
+
47
+ - `businessDayOnOrAfter` の `cutoff` を、時刻を持たない入力と組み合わせると `invalid-argument` にした(上の「移行」)
48
+ - README: `businessDaysBetween` が `addBusinessDays` の逆算になるのは起点が営業日のときで、休業日の起点では `addBusinessDays` が翌営業日を 1 と数える(`+0` と `+1` が同じ日)ことを明記した。`weekdays`、営業日を考慮した締め日、`cutoff` の入力の条件、バッジに npm のダウンロード数(直近 18 か月)を足した。サイズの値を測り直した(ルート約 9.7KB・日本の祝日込み約 11.5KB・英国の祝日込み約 11.4KB。0.15.0 で測定)
49
+ - `docs/decisions.md`: 31 番に「起点が営業日のとき」の条件と休業日の起点の定義を、37 番に「別名」と「文脈を持つ経路」の線引きを、30 番に `reason`・`rulePath`・診断の違いを足した。41 番(`weekdays`・営業日を考慮した締め日と空の月の扱い)・42 番(`kinds` の型・`cutoff` の例外)を追加した
50
+
51
+ ### 開発
52
+
53
+ - 性質のテストに、休業日を起点にした `addBusinessDays`(`addBusinessDays(a, n) = addBusinessDays(businessDayOnOrAfter(a), n − 1)`、n ≥ 1)と、営業日を考慮した締め日(複数のカレンダー × 4 つの方法 × 締め日 1〜31・−1 で、空でない期間が敷き詰められ `closingMonth` と往復する)を足した。規則の診断の性質のテストに `weekdays` を加えた
54
+ - サイズの上限(`size-limits.json`)を上げた。ルート 22,000 → 23,000、`./jp` 25,000 → 26,000、`./diagnose` 28,500 → 29,500(gzip)。`weekdays` の検証・判定と営業日を考慮した締め日のため
55
+
7
56
  ## [0.14.1] - 2026-10-20
8
57
 
9
58
  ライブラリの結果は 0.14.0 と同じ(内部の改善と文書だけ)。
@@ -382,7 +431,9 @@
382
431
 
383
432
  - 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
384
433
 
385
- [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.14.1...HEAD
434
+ [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.16.0...HEAD
435
+ [0.16.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.15.0...v0.16.0
436
+ [0.15.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.14.1...v0.15.0
386
437
  [0.14.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.14.0...v0.14.1
387
438
  [0.14.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.13.0...v0.14.0
388
439
  [0.13.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.12.0...v0.13.0
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  <p align="center">
9
9
  <a href="https://www.npmjs.com/package/bizdays-toolkit"><img src="https://img.shields.io/npm/v/bizdays-toolkit" alt="npm version"></a>
10
+ <a href="https://www.npmjs.com/package/bizdays-toolkit"><img src="https://img.shields.io/npm/d18m/bizdays-toolkit" alt="npm downloads"></a>
10
11
  <a href="https://github.com/inohan77/bizdays-toolkit/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/inohan77/bizdays-toolkit/ci.yml?branch=main&amp;label=CI" alt="CI status"></a>
11
12
  <a href="https://github.com/inohan77/bizdays-toolkit/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/bizdays-toolkit" alt="license"></a>
12
13
  <img src="https://img.shields.io/node/v/bizdays-toolkit" alt="Node.js version">
@@ -28,7 +29,7 @@ A business-day calendar that combines public holidays with company or store clos
28
29
  - **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
29
30
  - **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
30
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
31
- - **Lightweight, zero dependencies**: about 9.3 KB min+gzip (about 11.1 KB with Japanese holidays, about 11.1 KB with UK bank holidays; measured for 0.14.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
32
+ - **Lightweight, zero dependencies**: about 9.8 KB min+gzip (about 11.6 KB with Japanese holidays, about 11.5 KB with UK bank holidays; measured for 0.16.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
32
33
  - **Extensible to other countries and regions**: holiday providers are pluggable through the `HolidayProvider` interface. Japan and the UK are bundled
33
34
 
34
35
  ## Installation
@@ -37,7 +38,7 @@ A business-day calendar that combines public holidays with company or store clos
37
38
  pnpm add bizdays-toolkit
38
39
  ```
39
40
 
40
- 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.
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), 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.
41
42
 
42
43
  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.
43
44
 
@@ -112,14 +113,14 @@ shipping.addBusinessDays("2026-12-30", 1); // "2027-01-04"
112
113
  shipping.businessDaysBetween("2026-09-01", "2026-10-01"); // business days from (inclusive) to (exclusive)
113
114
 
114
115
  // Orders before 14:00 ship today, later orders on the next business day
115
- shipping.businessDayOnOrAfter(new Date(), { cutoff: "14:00" });
116
+ shipping.businessDayOnOrAfter(new Date(), { cutoff: "14:00" }); // the input must carry a time of day
116
117
 
117
118
  shipping.dayInfo("2027-01-01");
118
119
  // { date: "2027-01-01", weekday: 5, businessDay: false, reason: "closed-annual", name: "New Year closure", holiday: {…}, rulePath: "closedAnnual[0]" }
119
120
  shipping.daysBetween("2026-12-30", "2027-01-04").map((d) => d.businessDay); // [true, false, false, false, false, true]
120
121
  ```
121
122
 
122
- `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 it is the inverse of `addBusinessDays`.
123
+ `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.
123
124
 
124
125
  `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.
125
126
 
@@ -136,9 +137,9 @@ Each day is decided in this order of priority (top first).
136
137
 
137
138
  This order is fixed. Only the relation between a recurring rule and holidays can be chosen per rule with `onHolidayRule` (next section).
138
139
 
139
- ### Recurring rules (weekly, every other week, nth weekday, day of month, holidays, day after a holiday)
140
+ ### Recurring rules (weekly, every other week, nth weekday, day of month, holidays, day after or before a holiday)
140
141
 
141
- 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`.
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", "closed on the day after a holiday" or "late opening on the day before a holiday" go in `closedRecurring` and `openRecurring`.
142
143
 
143
144
  ```ts
144
145
  const museum = createCalendar({
@@ -173,14 +174,16 @@ museum.isBusinessDay("2026-05-07"); // false (the day after the May 3–6 run of
173
174
  | `{ day: 20 }` | The 20th of every month. `day` is 1–31 or -1 (last day); an array gives several |
174
175
  | `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed to the given kinds (`Holiday.kind`); judged before `holidayPolicy` (see below) |
175
176
  | `{ dayAfterHoliday: true }` | The day after a holiday: the first non-holiday after a holiday or a run of holidays (holidays of the provider only) |
177
+ | `{ dayBeforeHoliday: true }` | The day before a holiday: the non-holiday just before a holiday or a run of holidays (holidays of the provider only) |
176
178
  | `months: [8, 12]` | Narrows any pattern to these months |
177
179
  | `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) |
178
180
 
179
181
  - **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
180
182
  - **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
181
183
  - **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`
182
- - **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), `months` to those months; omit both 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
183
- - **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
184
+ - **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
185
+ - **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)
186
+ - **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`)
184
187
  - 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
185
188
 
186
189
  **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).
@@ -228,8 +231,15 @@ const hours = createSchedule<Hours>({
228
231
  recurring: [
229
232
  { weekday: 1, value: null, name: "Closed on Mondays", onHolidayRule: "after-holiday" },
230
233
  { day: -1, value: { open: "10:00", close: "17:00" }, name: "Short hours at month end" },
234
+ {
235
+ dayBeforeHoliday: true,
236
+ value: { open: "10:00", close: "23:00" },
237
+ name: "Late on the eve of a holiday",
238
+ },
231
239
  ],
232
240
  });
241
+ hours.valueOn("2026-05-02"); // { open: "10:00", close: "23:00" } (the day before the May 3–6 holidays)
242
+ hours.dayInfo("2026-05-04").reason; // "weekday" (a holiday in the run is not the day before a holiday)
233
243
  ```
234
244
 
235
245
  ### Combining calendars
@@ -255,7 +265,7 @@ const bank = createCalendar({
255
265
  const remittance = combineCalendars([company, bank], { businessDay: "all" });
256
266
  remittance.addBusinessDays("2026-12-28", 2); // "2027-01-05"
257
267
  remittance.dayInfo("2026-12-29");
258
- // { date: "2026-12-29", weekday: 2, businessDay: false, reason: "closed-annual", name: "New Year closure", rulePath: "closedAnnual[0]", decidedBy: 0 }
268
+ // { date: "2026-12-29", weekday: 2, businessDay: false, reason: "closed-annual", name: "New Year closure", rulePath: "closedAnnual[0]", decidedBy: 0, decidedByPath: [0] }
259
269
 
260
270
  // Any of several stores is open
261
271
  const anyStore = combineCalendars([company, bank], { businessDay: "any" });
@@ -263,6 +273,7 @@ anyStore.isBusinessDay("2026-12-29"); // true
263
273
  ```
264
274
 
265
275
  - `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
276
+ - `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
266
277
  - `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
267
278
  - 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
268
279
  - A combined calendar can be combined again. Option diagnostics work on options, not on calendars, so diagnose each calendar's options separately
@@ -407,6 +418,18 @@ const office = createCalendar({ holidays: jp, closedWeekdays: [0, 6] });
407
418
  office.listBusinessDays(from, to).length; // 22 (business days in the November period)
408
419
  ```
409
420
 
421
+ **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.
422
+
423
+ ```ts
424
+ const office = createCalendar({ holidays: jp, closedWeekdays: [0, 6] });
425
+ // September 20, 2026 is a Sunday: with "preceding" the September closing day is Friday the 18th
426
+ office.closingMonth("2026-09-19", 20, { convention: "preceding" }); // "2026-10" (the 19th is after the September closing)
427
+ closingMonth("2026-09-19", 20); // "2026-09" (closing on calendar days)
428
+ office.closingPeriod("2026-10", 20, { convention: "preceding" }); // { from: "2026-09-19", to: "2026-10-20" }
429
+ ```
430
+
431
+ 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.
432
+
410
433
  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.
411
434
 
412
435
  ### Day-by-day values (e.g. store hours)
@@ -500,7 +523,7 @@ Every method accepts the following inputs. Returned dates are always `"YYYY-MM-D
500
523
  | Temporal `PlainDate`, `PlainDateTime` | A civil date or date and time (no dependency on Temporal) |
501
524
  | Temporal `Instant`, `ZonedDateTime` | An instant |
502
525
 
503
- 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).
526
+ 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).
504
527
 
505
528
  The calendar's time zone is the `timeZone` option, then the holiday provider's time zone, then `"UTC"`.
506
529
 
@@ -549,6 +572,38 @@ const myCountry: HolidayProvider = {
549
572
  const calendar = createCalendar({ holidays: myCountry, closedWeekdays: [0, 6] });
550
573
  ```
551
574
 
575
+ 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).
576
+
577
+ ```ts
578
+ import { addDays, createCalendar, type HolidayProvider } from "bizdays-toolkit";
579
+ import { easterSunday, nthWeekdayOfMonth } from "bizdays-toolkit/provider";
580
+
581
+ const bavaria: HolidayProvider<"public"> = {
582
+ id: "DE-BY",
583
+ timeZone: "Europe/Berlin",
584
+ supportedYears: { from: 2000, to: 2100 },
585
+ dataVersion: "example",
586
+ holidaysInYear: (year) => {
587
+ const easter = easterSunday(year);
588
+ const day = (date: string, name: string, nameEn: string) => ({
589
+ date,
590
+ name,
591
+ nameEn,
592
+ kind: "public" as const,
593
+ confirmed: true,
594
+ });
595
+ return [
596
+ day(addDays(easter, -2), "Karfreitag", "Good Friday"),
597
+ day(addDays(easter, 1), "Ostermontag", "Easter Monday"),
598
+ day(addDays(easter, 60), "Fronleichnam", "Corpus Christi"),
599
+ ];
600
+ },
601
+ };
602
+ createCalendar({ holidays: bavaria }).isHoliday("2026-06-04"); // true (Corpus Christi)
603
+ nthWeekdayOfMonth(2026, 11, 4, 4); // "2026-11-26" (the 4th Thursday of November: Thanksgiving Day)
604
+ nthWeekdayOfMonth(2026, 5, 1, -1); // "2026-05-25" (the last Monday of May: Memorial Day)
605
+ ```
606
+
552
607
  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.
553
608
 
554
609
  ```ts
@@ -81,9 +81,9 @@ export interface CalendarOptions<Kind extends string = string> extends HolidaySo
81
81
  * 祝日に当たったときの扱いは規則ごとに onHolidayRule で指定する(既定: "under-holiday")。
82
82
  * 有効期間(effectiveFrom・effectiveTo)を付けると、期間の外では書かれていないのと同じに扱う
83
83
  */
84
- readonly closedRecurring?: readonly RecurringRule[];
84
+ readonly closedRecurring?: readonly RecurringRule<Kind>[];
85
85
  /** 繰り返しの営業日。closedRecurring と同じ日に当たる場合はこちらが優先 */
86
- readonly openRecurring?: readonly RecurringRule[];
86
+ readonly openRecurring?: readonly RecurringRule<Kind>[];
87
87
  /** 祝日の扱い(既定: "closed") */
88
88
  readonly holidayPolicy?: HolidayPolicy;
89
89
  /** 営業日を探すときに調べる最大日数(既定: 3660)。超えたら Error */
@@ -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
  * カレンダーの合成のオプション。
@@ -147,7 +153,8 @@ export interface CombineOptions {
147
153
  export interface BusinessDayOptions {
148
154
  /**
149
155
  * 締め時刻 "HH:mm"(カレンダーのタイムゾーン)。
150
- * 入力の時刻がこの時刻以降なら、その日は対象にしない。暦日だけの入力は 0 時として扱う
156
+ * 入力の時刻がこの時刻以降なら、その日は対象にしない。時刻を持たない入力(暦日の文字列・Temporal の PlainDate など)
157
+ * と組み合わせると code: "invalid-argument" の Error(締め時刻が黙って無視されるのを防ぐ。0.15.0 から)
151
158
  */
152
159
  readonly cutoff?: string;
153
160
  }
@@ -195,7 +202,10 @@ export interface Calendar<Kind extends string = string> extends HolidayLookup<Ki
195
202
  nextBusinessDay(input: DateInput): DateString;
196
203
  /** 前営業日(入力日の前日以前) */
197
204
  previousBusinessDay(input: DateInput): DateString;
198
- /** 入力日が営業日ならその日、そうでなければ翌営業日。cutoff で締め時刻を指定できる */
205
+ /**
206
+ * 入力日が営業日ならその日、そうでなければ翌営業日。cutoff で締め時刻を指定できる
207
+ * (cutoff を指定するときの入力は、時刻を持つもの。時刻のない入力と組み合わせると invalid-argument)
208
+ */
199
209
  businessDayOnOrAfter(input: DateInput, options?: BusinessDayOptions): DateString;
200
210
  /** 入力日が営業日ならその日、そうでなければ前営業日 */
201
211
  businessDayOnOrBefore(input: DateInput): DateString;
@@ -227,6 +237,23 @@ export interface Calendar<Kind extends string = string> extends HolidayLookup<Ki
227
237
  * その月にない日は末日に読み替えてから調整する(例: 2 月の 30 日払いは 2 月末日)
228
238
  */
229
239
  dueDate(month: MonthString, options: DueDateOptions): DateString;
240
+ /**
241
+ * 営業日を考慮した締め日で区切ったときに、日付が属する月。
242
+ * 各月の締め日(締め日がその月にない場合は末日)を convention で営業日に調整した日を D(m) とし、
243
+ * 日付が D(m) 以下となる最小の m を返す。前月の締め日から当月の締め日までに営業日がない月(長期休業)は
244
+ * 固有の期間を持たず、その日は次の月に属する
245
+ */
246
+ closingMonth(input: DateInput, closingDay: number, options: AdjustOptions): MonthString;
247
+ /**
248
+ * 営業日を考慮した締め日で区切ったときの、その月分の期間(両端を含む暦日。前月の調整後の締め日の翌日から
249
+ * 当月の調整後の締め日まで)。暦日を渡すと、その日が属する期間(closingMonth で求めた月の期間)。
250
+ * 月を渡して、その月に固有の期間がない(前月の締め日から当月の締め日までに営業日がない)ときは
251
+ * code: "no-business-day" の Error。返す値は変更できず、name は付かない
252
+ */
253
+ closingPeriod(input: DateInput | MonthString, closingDay: number, options: AdjustOptions): {
254
+ readonly from: DateString;
255
+ readonly to: DateString;
256
+ };
230
257
  }
231
258
  interface CompiledCalendar<Kind extends string> {
232
259
  readonly schedule: CompiledSchedule<boolean, Kind>;
@@ -247,7 +274,8 @@ export declare function createCalendar<Kind extends string = string>(options?: C
247
274
  * 営業している日を営業日とする。返すのは通常の営業日カレンダーで、営業日の加減算・調整・期日などが合成結果で動く。
248
275
  *
249
276
  * - dayInfo の reason・name・shiftedFrom・rulePath は、合成結果と同じ判定をした最初のカレンダーの結果で、
250
- * その番号を decidedBy で返す。holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
277
+ * その番号を decidedBy で返す。入れ子の合成では、内側の番号まで含めた列を decidedByPath で返す。
278
+ * holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
251
279
  * - タイムゾーンはすべてのカレンダーで同じでなければならない(time-zone-conflict)。
252
280
  * 2 つ未満、同じカレンダーを 2 回渡した場合は invalid-argument
253
281
  * - 合成したカレンダーをさらに合成することもできる。規則の診断(diagnoseCalendar)の対象にはならないので、
@@ -21,7 +21,7 @@
21
21
  import { addDays, daysInMonth, formatDate, fromDayNumber, toDayNumber, } from "./date.js";
22
22
  import { checkUnknownKeys, IssueCollector, plainError, rangeError } from "./errors.js";
23
23
  import { parseTimeOfDay, toCivil } from "./input.js";
24
- import { dayOfMonth, isDayOfMonthValue, parseMonth } from "./month.js";
24
+ import { checkClosingDay, closingDayIn, closingMonth as calendarClosingMonth, dayOfMonth, formatMonth, isDayOfMonthValue, parseMonth, shiftMonth, } from "./month.js";
25
25
  import { buildSchedule, compileAnnualRules, compileDateRules, compileHolidaySource, compileRecurringRules, createHolidayLookup, infoOfSchedule, isWeekday, mapDays, RECURRING_RULE_KEYS, } from "./schedule.js";
26
26
  const CALENDAR_OPTION_KEYS = [
27
27
  "holidays",
@@ -181,6 +181,23 @@ function buildCalendar(lookup, isOpen, info, maxSearchDays) {
181
181
  "modified-following",
182
182
  "modified-preceding",
183
183
  ];
184
+ /** 月 m の、営業日に調整した締め日 D(m)(decisions.md の 41 番) */
185
+ function closingDate(month, closingDay, options) {
186
+ return adjustDate(dayOfMonth(month, closingDayIn(month, closingDay)), options);
187
+ }
188
+ /** 営業日を考慮した締め日で日付が属する月(date ≤ D(m) となる最小の m)。暦日の締めの月の前月から順に調べる */
189
+ function closingMonthOf(date, closingDay, options) {
190
+ checkClosingDay(closingDay);
191
+ let month = parseMonth(calendarClosingMonth(date, closingDay));
192
+ // D は月とともに単調に増える(減らない)ので、暦日の締めの月から、後ろへ(following で前月の締め日が
193
+ // 長期休業を越えて当月以降に来た場合)、前へ(preceding で当月の締め日が date より前に来た場合)順に探す
194
+ while (date <= closingDate(shiftMonth(month, -1), closingDay, options)) {
195
+ month = shiftMonth(month, -1);
196
+ }
197
+ while (date > closingDate(month, closingDay, options))
198
+ month = shiftMonth(month, 1);
199
+ return month;
200
+ }
184
201
  function adjustDate(date, options) {
185
202
  const convention = options?.convention;
186
203
  if (!CONVENTIONS.includes(convention)) {
@@ -265,7 +282,10 @@ function buildCalendar(lookup, isOpen, info, maxSearchDays) {
265
282
  let start = civil.date;
266
283
  if (opts.cutoff !== undefined) {
267
284
  const cutoff = parseTimeOfDay(opts.cutoff);
268
- if (civil.minutes !== null && civil.minutes >= cutoff)
285
+ if (civil.minutes === null) {
286
+ throw rangeError("invalid-argument", "cutoff requires an input with a time of day (a Date, a date-time string or a Temporal value with a time); a date-only input would silently ignore the cutoff");
287
+ }
288
+ if (civil.minutes >= cutoff)
269
289
  start = addDays(start, 1);
270
290
  }
271
291
  return search(start, 1);
@@ -305,6 +325,21 @@ function buildCalendar(lookup, isOpen, info, maxSearchDays) {
305
325
  nthBusinessDayOfMonth: nthOfMonth,
306
326
  firstBusinessDayOfMonth: (month) => nthOfMonth(month, 1),
307
327
  lastBusinessDayOfMonth: (month) => nthOfMonth(month, -1),
328
+ closingMonth: (input, closingDay, options) => formatMonth(closingMonthOf(dateOf(input), closingDay, options)),
329
+ closingPeriod(input, closingDay, options) {
330
+ checkClosingDay(closingDay);
331
+ // "YYYY-MM" だけを月として扱い、それ以外(暦日・日時の文字列・Date・Temporal の値)は dateOf で暦日にする
332
+ const isMonth = typeof input === "string" && /^\d{4}-\d{2}$/.test(input);
333
+ const month = isMonth
334
+ ? parseMonth(input)
335
+ : closingMonthOf(dateOf(input), closingDay, options);
336
+ const to = closingDate(month, closingDay, options);
337
+ const from = addDays(closingDate(shiftMonth(month, -1), closingDay, options), 1);
338
+ if (from > to) {
339
+ throw rangeError("no-business-day", `${formatMonth(month)} has no closing period of its own: there is no business day between the previous closing day and ${to} (its days belong to the next month)`);
340
+ }
341
+ return Object.freeze({ from, to });
342
+ },
308
343
  dueDate(month, options) {
309
344
  const day = options?.day;
310
345
  if (!isDayOfMonthValue(day)) {
@@ -326,7 +361,8 @@ const isCalendarLike = (v) => typeof v === "object" &&
326
361
  * 営業している日を営業日とする。返すのは通常の営業日カレンダーで、営業日の加減算・調整・期日などが合成結果で動く。
327
362
  *
328
363
  * - dayInfo の reason・name・shiftedFrom・rulePath は、合成結果と同じ判定をした最初のカレンダーの結果で、
329
- * その番号を decidedBy で返す。holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
364
+ * その番号を decidedBy で返す。入れ子の合成では、内側の番号まで含めた列を decidedByPath で返す。
365
+ * holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
330
366
  * - タイムゾーンはすべてのカレンダーで同じでなければならない(time-zone-conflict)。
331
367
  * 2 つ未満、同じカレンダーを 2 回渡した場合は invalid-argument
332
368
  * - 合成したカレンダーをさらに合成することもできる。規則の診断(diagnoseCalendar)の対象にはならないので、
@@ -391,6 +427,8 @@ export function combineCalendars(calendars, options) {
391
427
  if (base.rulePath !== undefined)
392
428
  result.rulePath = base.rulePath;
393
429
  result.decidedBy = decidedBy;
430
+ // 内側が合成したカレンダーなら、その経路の前に自分の番号を付ける(入れ子でなければ長さ 1)
431
+ result.decidedByPath = Object.freeze([decidedBy, ...(base.decidedByPath ?? [])]);
394
432
  return Object.freeze(result);
395
433
  }
396
434
  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,7 +8,7 @@
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
14
  * - info … redundant: 取り除いても結果が変わらない規則
@@ -8,7 +8,7 @@
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
14
  * - info … redundant: 取り除いても結果が変わらない規則
@@ -360,7 +360,7 @@ function analyze(c, compiled, isCalendar) {
360
360
  });
361
361
  // --- 繰り返しの規則: 当てはまる日がない・覆われている ---
362
362
  const recurringBits = recurring.map(patternBits);
363
- /** 祝日・祝日の翌日の規則の判定に使う(提供元の対応年の外は祝日でないとみなす) */
363
+ /** 祝日・祝日の翌日・祝前日の規則の判定に使う(提供元の対応年の外は祝日でないとみなす) */
364
364
  const holidayOfSafe = (date) => {
365
365
  try {
366
366
  return lookup.holidayOn(date);
@@ -369,7 +369,7 @@ function analyze(c, compiled, isCalendar) {
369
369
  return undefined;
370
370
  }
371
371
  };
372
- /** 祝日に従属する規則(祝日・祝日の翌日)が、調べる期間と有効期間の中で 1 日でも当てはまるか(祝日は年ごとに違うので、全日を調べる) */
372
+ /** 祝日に従属する規則(祝日・祝日の翌日・祝前日)が、調べる期間と有効期間の中で 1 日でも当てはまるか(祝日は年ごとに違うので、全日を調べる) */
373
373
  const holidayRuleMatches = (rule) => {
374
374
  if (source.providers.length === 0 || !window)
375
375
  return false;
@@ -381,7 +381,7 @@ function analyze(c, compiled, isCalendar) {
381
381
  const setCovers = (a, b) => a === null || (b !== null && [...b].every((v) => a.has(v)));
382
382
  recurring.forEach((rule, j) => {
383
383
  const bits = recurringBits[j];
384
- const holidayBased = rule.holiday || rule.dayAfterHoliday;
384
+ const holidayBased = rule.holiday || rule.dayAfterHoliday || rule.dayBeforeHoliday;
385
385
  const matches = holidayBased ? holidayRuleMatches(rule) : matchesAnyDay(rule, cycle);
386
386
  if (!matches) {
387
387
  unused.add(rule);
@@ -389,23 +389,32 @@ function analyze(c, compiled, isCalendar) {
389
389
  ? source.providers.length === 0
390
390
  ? "This rule matches no day: there are no holidays without a holiday provider"
391
391
  : rule.holiday
392
- ? "This rule matches no day: no holiday of the selected kinds within the selected months and effective period"
393
- : "This rule matches no day: no day after a holiday within its effective period"
392
+ ? "This rule matches no day: no holiday of the selected kinds, weekdays and months within its effective period"
393
+ : rule.dayAfterHoliday
394
+ ? "This rule matches no day: no day after a holiday on the selected weekdays and months within its effective period"
395
+ : "This rule matches no day: no day before a holiday on the selected weekdays and months within its effective period"
394
396
  : rule.effectiveFrom === null && rule.effectiveTo === null
395
397
  ? "This rule matches no day (for example, a day that does not exist in the selected months)"
396
398
  : "This rule matches no day within its effective period");
397
399
  return;
398
400
  }
399
- // 祝日・祝日の翌日の規則は祝日次第で年ごとに変わるので、形の包含は同じ形の規則どうし(種別・月の絞り込み)だけで調べる
401
+ // 祝日・祝日の翌日・祝前日の規則は祝日次第で年ごとに変わるので、形の包含は同じ形の規則どうし(種別・曜日・月の絞り込み)だけで調べる
400
402
  const k = recurring.findIndex((r, i) => i < j &&
401
403
  canCover(r, rule) &&
402
404
  periodCovers(r, rule) &&
403
405
  (rule.holiday
404
- ? r.holiday && setCovers(r.kinds, rule.kinds) && setCovers(r.months, rule.months)
405
- : rule.dayAfterHoliday
406
- ? r.dayAfterHoliday && setCovers(r.months, rule.months)
406
+ ? r.holiday &&
407
+ setCovers(r.kinds, rule.kinds) &&
408
+ setCovers(r.weekdays, rule.weekdays) &&
409
+ setCovers(r.months, rule.months)
410
+ : rule.dayAfterHoliday || rule.dayBeforeHoliday
411
+ ? r.dayAfterHoliday === rule.dayAfterHoliday &&
412
+ r.dayBeforeHoliday === rule.dayBeforeHoliday &&
413
+ setCovers(r.weekdays, rule.weekdays) &&
414
+ setCovers(r.months, rule.months)
407
415
  : !r.holiday &&
408
416
  !r.dayAfterHoliday &&
417
+ !r.dayBeforeHoliday &&
409
418
  intervalCovers(r, rule) &&
410
419
  isSubset(bits, recurringBits[i])));
411
420
  if (k < 0)
@@ -10,7 +10,8 @@
10
10
  */
11
11
  import { formatDate, parseDate } from "./date.js";
12
12
  import { rangeError, typeError } from "./errors.js";
13
- const DATETIME_RE = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(Z|[+-]\d{2}:?\d{2})?$/i;
13
+ // 日付と時刻の区切りは "T" だけ(型 DateTimeInput と同じ)。空白で区切った文字列は 0.16.0 から invalid-time
14
+ const DATETIME_RE = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(Z|[+-]\d{2}:?\d{2})?$/i;
14
15
  const formatters = new Map();
15
16
  function formatterFor(timeZone) {
16
17
  let f = formatters.get(timeZone);
@@ -48,6 +48,10 @@ export declare function monthOf(input: DateString | MonthString, offset?: number
48
48
  * @public
49
49
  */
50
50
  export declare function closingMonth(date: DateString, closingDay: number): MonthString;
51
+ /** @internal 締め日の検証(closingMonth・closingPeriod・営業日カレンダーの締め日で共用) */
52
+ export declare function checkClosingDay(closingDay: number): void;
53
+ /** @internal その月の締め日(日)。-1 またはその月の日数以上なら末日(decisions.md の 16 番の読み替えはここだけで行う) */
54
+ export declare function closingDayIn({ year, month }: YearMonth, closingDay: number): number;
51
55
  /**
52
56
  * 締め日で区切ったときの、その月分の期間(両端を含む暦日)。closingMonth の逆算。
53
57
  *
@@ -71,13 +71,14 @@ export function closingMonth(date, closingDay) {
71
71
  const { year, month, day } = parseDate(date);
72
72
  return formatMonth(shiftMonth({ year, month }, day <= closingDayIn({ year, month }, closingDay) ? 0 : 1));
73
73
  }
74
- function checkClosingDay(closingDay) {
74
+ /** @internal 締め日の検証(closingMonth・closingPeriod・営業日カレンダーの締め日で共用) */
75
+ export function checkClosingDay(closingDay) {
75
76
  if (!isDayOfMonthValue(closingDay)) {
76
77
  throw rangeError("invalid-argument", `closingDay must be an integer from 1 to 31, or -1 (last day): ${closingDay}`);
77
78
  }
78
79
  }
79
- /** その月の締め日(日)。-1 またはその月の日数以上なら末日(decisions.md の 16 番の読み替えはここだけで行う) */
80
- function closingDayIn({ year, month }, closingDay) {
80
+ /** @internal その月の締め日(日)。-1 またはその月の日数以上なら末日(decisions.md の 16 番の読み替えはここだけで行う) */
81
+ export function closingDayIn({ year, month }, closingDay) {
81
82
  const last = daysInMonth(year, month);
82
83
  return closingDay === -1 || closingDay > last ? last : closingDay;
83
84
  }
@@ -13,6 +13,7 @@
13
13
  * "after-holiday" の規則は、祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)。
14
14
  * 毎年の月日・繰り返しの規則の有効期間(effectiveFrom・effectiveTo)の外では、その規則は書かれていないのと同じ。
15
15
  * 祝日の翌日の規則(dayAfterHoliday)は、前日が祝日でその日自身は祝日でない日に当てはまる(提供元の祝日だけで判定)。
16
+ * 祝前日の規則(dayBeforeHoliday)は、翌日が祝日でその日自身は祝日でない日に当てはまる(同じく提供元の祝日だけ)。
16
17
  * 祝日の規則(holiday: true。kinds で種別を絞れる)は、「祝日の上」の段階で判定する
17
18
  *
18
19
  * 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
@@ -90,7 +91,10 @@ export interface WeekdayPattern {
90
91
  readonly months?: readonly Month[];
91
92
  readonly day?: never;
92
93
  readonly dayAfterHoliday?: never;
94
+ readonly dayBeforeHoliday?: never;
93
95
  readonly holiday?: never;
96
+ readonly weekdays?: never;
97
+ readonly kinds?: never;
94
98
  }
95
99
  /**
96
100
  * 毎月の日の繰り返し(例: 毎月 20 日、毎月末日)。その月にない日(2 月 30 日など)は当てはまらない。
@@ -106,7 +110,10 @@ export interface DayOfMonthPattern {
106
110
  readonly interval?: never;
107
111
  readonly anchor?: never;
108
112
  readonly dayAfterHoliday?: never;
113
+ readonly dayBeforeHoliday?: never;
109
114
  readonly holiday?: never;
115
+ readonly weekdays?: never;
116
+ readonly kinds?: never;
110
117
  }
111
118
  /**
112
119
  * 祝日の翌日(祝日が続く場合は連休が明けた日。その日自身は祝日でない)。「祝日」は提供元の祝日だけ。
@@ -115,6 +122,8 @@ export interface DayOfMonthPattern {
115
122
  */
116
123
  export interface DayAfterHolidayPattern {
117
124
  readonly dayAfterHoliday: true;
125
+ /** 当てはめる曜日(翌日の曜日。省略時はすべての曜日。例: [1, 2, 3, 4, 5] で土日に当たる翌日は除く) */
126
+ readonly weekdays?: readonly Weekday[];
118
127
  /** 当てはめる月(翌日の月。省略時はすべての月) */
119
128
  readonly months?: readonly Month[];
120
129
  readonly weekday?: never;
@@ -122,7 +131,30 @@ export interface DayAfterHolidayPattern {
122
131
  readonly interval?: never;
123
132
  readonly anchor?: never;
124
133
  readonly day?: never;
134
+ readonly dayBeforeHoliday?: never;
125
135
  readonly holiday?: never;
136
+ readonly kinds?: never;
137
+ }
138
+ /**
139
+ * 祝前日(翌日が祝日で、その日自身は祝日でない日。連休なら連休の初日の前日)。「祝日」は提供元の祝日だけ。
140
+ * 祝日の翌日(dayAfterHoliday)の鏡像で、祝前日料金・祝前日の営業時間の延長・締め時刻の繰り上げなどを規則として書く。
141
+ * 当てはまる日は祝日でないので、onHolidayRule は指定できない(指定すると invalid-option)。
142
+ * @public
143
+ */
144
+ export interface DayBeforeHolidayPattern {
145
+ readonly dayBeforeHoliday: true;
146
+ /** 当てはめる曜日(祝前日の曜日。省略時はすべての曜日。例: [1, 2, 3, 4, 5] で土日に当たる祝前日は除く) */
147
+ readonly weekdays?: readonly Weekday[];
148
+ /** 当てはめる月(祝前日の月。省略時はすべての月) */
149
+ readonly months?: readonly Month[];
150
+ readonly weekday?: never;
151
+ readonly nth?: never;
152
+ readonly interval?: never;
153
+ readonly anchor?: never;
154
+ readonly day?: never;
155
+ readonly dayAfterHoliday?: never;
156
+ readonly holiday?: never;
157
+ readonly kinds?: never;
126
158
  }
127
159
  /**
128
160
  * 祝日(提供元の祝日である日)。kinds で祝日の種別を絞れる(省略時はすべての祝日)。
@@ -131,10 +163,12 @@ export interface DayAfterHolidayPattern {
131
163
  * 当てはまる日は祝日なので、onHolidayRule は指定できない(指定すると invalid-option)。
132
164
  * @public
133
165
  */
134
- export interface HolidayPattern {
166
+ export interface HolidayPattern<Kind extends string = string> {
135
167
  readonly holiday: true;
136
- /** 当てはめる祝日の種別(Holiday.kind。省略時はすべて) */
137
- readonly kinds?: readonly string[];
168
+ /** 当てはめる祝日の種別(Holiday.kind。省略時はすべて)。提供元の種別の型で補完が効く(型は開いているので他の文字列も通る) */
169
+ readonly kinds?: readonly Kind[];
170
+ /** 当てはめる曜日(省略時はすべての曜日。例: [6] で土曜の祝日だけ) */
171
+ readonly weekdays?: readonly Weekday[];
138
172
  /** 当てはめる月(省略時はすべての月) */
139
173
  readonly months?: readonly Month[];
140
174
  readonly weekday?: never;
@@ -143,17 +177,18 @@ export interface HolidayPattern {
143
177
  readonly anchor?: never;
144
178
  readonly day?: never;
145
179
  readonly dayAfterHoliday?: never;
180
+ readonly dayBeforeHoliday?: never;
146
181
  }
147
182
  /**
148
183
  * 繰り返しのパターン。
149
184
  * @public
150
185
  */
151
- export type RecurringPattern = WeekdayPattern | DayOfMonthPattern | DayAfterHolidayPattern | HolidayPattern;
186
+ export type RecurringPattern<Kind extends string = string> = WeekdayPattern | DayOfMonthPattern | DayAfterHolidayPattern | DayBeforeHolidayPattern | HolidayPattern<Kind>;
152
187
  /**
153
188
  * 繰り返しの規則(営業日カレンダーの closedRecurring・openRecurring)。
154
189
  * @public
155
190
  */
156
- export type RecurringRule = RecurringPattern & EffectivePeriod & {
191
+ export type RecurringRule<Kind extends string = string> = RecurringPattern<Kind> & EffectivePeriod & {
157
192
  readonly name?: string;
158
193
  /** 祝日に当たったときの扱い(既定: "under-holiday") */
159
194
  readonly onHolidayRule?: OnHolidayRule;
@@ -162,7 +197,7 @@ export type RecurringRule = RecurringPattern & EffectivePeriod & {
162
197
  * 繰り返しの規則の値(スケジュールの recurring)。
163
198
  * @public
164
199
  */
165
- export type RecurringValue<T> = RecurringRule & {
200
+ export type RecurringValue<T, Kind extends string = string> = RecurringRule<Kind> & {
166
201
  readonly value: T;
167
202
  };
168
203
  /**
@@ -228,7 +263,7 @@ export interface ScheduleOptions<T, Kind extends string = string> extends Holida
228
263
  /** 祝日の値の決め方(既定: "weekday") */
229
264
  readonly holiday?: HolidayValueRule<T>;
230
265
  /** 繰り返しの規則(毎週・毎月の第 n 曜日・毎月の日)の値 */
231
- readonly recurring?: readonly RecurringValue<T>[];
266
+ readonly recurring?: readonly RecurringValue<T, Kind>[];
232
267
  /** 毎年の月日の値 */
233
268
  readonly annual?: readonly (AnnualValue<T> | AnnualRangeValue<T>)[];
234
269
  /** 個別の日付の値 */
@@ -366,10 +401,14 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
366
401
  readonly days: ReadonlySet<number> | null;
367
402
  /** 祝日の翌日の規則か */
368
403
  readonly dayAfterHoliday: boolean;
404
+ /** 祝前日の規則か */
405
+ readonly dayBeforeHoliday: boolean;
369
406
  /** 祝日の規則か */
370
407
  readonly holiday: boolean;
371
408
  /** 祝日の規則で絞る種別(null はすべての祝日) */
372
409
  readonly kinds: ReadonlySet<string> | null;
410
+ /** 祝日・祝日の翌日・祝前日の規則で絞る曜日(null はすべての曜日) */
411
+ readonly weekdays: ReadonlySet<number> | null;
373
412
  /** 月の集合(null はすべての月) */
374
413
  readonly months: ReadonlySet<number> | null;
375
414
  readonly onHolidayRule: OnHolidayRule;
@@ -378,12 +417,12 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
378
417
  }
379
418
  /** @internal 繰り返しの規則を検証して取り込む */
380
419
  export declare function compileRecurringRules<T>(c: IssueCollector, path: string, entries: unknown, valueOf: (entry: Record<string, unknown>, path: string) => T | undefined, keys: readonly string[]): RecurringRuleCompiled<T>[];
381
- /** @internal その日の祝日を返す関数(祝日・祝日の翌日の規則の判定に使う。提供元の対応年の外は祝日でないとみなすこと) */
420
+ /** @internal その日の祝日を返す関数(祝日・祝日の翌日・祝前日の規則の判定に使う。提供元の対応年の外は祝日でないとみなすこと) */
382
421
  export type HolidayOf = (date: DateString) => Holiday<string> | undefined;
383
422
  /**
384
- * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日の翌日・月)がその日に当てはまるか。
423
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
385
424
  * 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
386
- * 祝日の翌日の規則は isHoliday で判定し、isHoliday を渡さなければ当てはまらない
425
+ * 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない
387
426
  */
388
427
  export declare function matchPattern(rule: RecurringRuleCompiled<unknown>, date: DateString, holidayOf?: HolidayOf): boolean;
389
428
  /** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む) */
@@ -13,6 +13,7 @@
13
13
  * "after-holiday" の規則は、祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)。
14
14
  * 毎年の月日・繰り返しの規則の有効期間(effectiveFrom・effectiveTo)の外では、その規則は書かれていないのと同じ。
15
15
  * 祝日の翌日の規則(dayAfterHoliday)は、前日が祝日でその日自身は祝日でない日に当てはまる(提供元の祝日だけで判定)。
16
+ * 祝前日の規則(dayBeforeHoliday)は、翌日が祝日でその日自身は祝日でない日に当てはまる(同じく提供元の祝日だけ)。
16
17
  * 祝日の規則(holiday: true。kinds で種別を絞れる)は、「祝日の上」の段階で判定する
17
18
  *
18
19
  * 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
@@ -211,9 +212,15 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
211
212
  const hasWeekday = "weekday" in entry;
212
213
  const hasDay = "day" in entry;
213
214
  const hasAfter = "dayAfterHoliday" in entry;
215
+ const hasBefore = "dayBeforeHoliday" in entry;
214
216
  const hasHoliday = "holiday" in entry;
215
- if (Number(hasWeekday) + Number(hasDay) + Number(hasAfter) + Number(hasHoliday) !== 1) {
216
- c.error("invalid-option", p, 'must have exactly one of "weekday", "day", "dayAfterHoliday" or "holiday"');
217
+ if (Number(hasWeekday) +
218
+ Number(hasDay) +
219
+ Number(hasAfter) +
220
+ Number(hasBefore) +
221
+ Number(hasHoliday) !==
222
+ 1) {
223
+ c.error("invalid-option", p, 'must have exactly one of "weekday", "day", "dayAfterHoliday", "dayBeforeHoliday" or "holiday"');
217
224
  return;
218
225
  }
219
226
  let kinds = null;
@@ -281,7 +288,7 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
281
288
  ok = false;
282
289
  }
283
290
  else {
284
- const key = hasAfter ? "dayAfterHoliday" : "holiday";
291
+ const key = hasAfter ? "dayAfterHoliday" : hasBefore ? "dayBeforeHoliday" : "holiday";
285
292
  if (entry[key] !== true) {
286
293
  c.error("invalid-option", `${p}.${key}`, "must be true");
287
294
  ok = false;
@@ -289,7 +296,9 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
289
296
  if ("onHolidayRule" in entry) {
290
297
  c.error("invalid-option", `${p}.onHolidayRule`, hasAfter
291
298
  ? 'cannot be used with "dayAfterHoliday" (the day after a holiday is never a holiday)'
292
- : 'cannot be used with "holiday" (the rule applies on holidays; it is judged before holidayPolicy)');
299
+ : hasBefore
300
+ ? 'cannot be used with "dayBeforeHoliday" (the day before a holiday is never a holiday)'
301
+ : 'cannot be used with "holiday" (the rule applies on holidays; it is judged before holidayPolicy)');
293
302
  ok = false;
294
303
  }
295
304
  if (hasHoliday && entry["kinds"] !== undefined && !Array.isArray(entry["kinds"])) {
@@ -309,6 +318,24 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
309
318
  c.error("invalid-option", `${p}.kinds`, '"kinds" can be used only with "holiday"');
310
319
  ok = false;
311
320
  }
321
+ let weekdays = null;
322
+ if ("weekdays" in entry) {
323
+ if (!hasHoliday && !hasAfter && !hasBefore) {
324
+ c.error("invalid-option", `${p}.weekdays`, '"weekdays" can be used only with "holiday", "dayAfterHoliday" or "dayBeforeHoliday"');
325
+ ok = false;
326
+ }
327
+ else if (!Array.isArray(entry["weekdays"])) {
328
+ c.error("invalid-option", `${p}.weekdays`, "must be a non-empty array of weekdays (0 = Sunday to 6 = Saturday)");
329
+ ok = false;
330
+ }
331
+ else {
332
+ const set = checkSet(c, `${p}.weekdays`, entry["weekdays"], isWeekday, "a weekday (0 = Sunday to 6 = Saturday)");
333
+ if (set)
334
+ weekdays = set;
335
+ else
336
+ ok = false;
337
+ }
338
+ }
312
339
  let months = null;
313
340
  if (entry["months"] !== undefined) {
314
341
  if (!Array.isArray(entry["months"])) {
@@ -339,8 +366,10 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
339
366
  anchor,
340
367
  days,
341
368
  dayAfterHoliday: hasAfter,
369
+ dayBeforeHoliday: hasBefore,
342
370
  holiday: hasHoliday,
343
371
  kinds,
372
+ weekdays,
344
373
  months,
345
374
  onHolidayRule: onHoliday,
346
375
  value,
@@ -352,22 +381,27 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
352
381
  return rules;
353
382
  }
354
383
  /**
355
- * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日の翌日・月)がその日に当てはまるか。
384
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
356
385
  * 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
357
- * 祝日の翌日の規則は isHoliday で判定し、isHoliday を渡さなければ当てはまらない
386
+ * 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない
358
387
  */
359
388
  export function matchPattern(rule, date, holidayOf) {
360
389
  const { year, month, day } = parseDate(date);
361
390
  if (rule.months && !rule.months.has(month))
362
391
  return false;
363
- if (rule.holiday) {
364
- const h = holidayOf?.(date);
365
- return h !== undefined && (rule.kinds === null || rule.kinds.has(h.kind));
366
- }
367
- if (rule.dayAfterHoliday) {
368
- return (holidayOf !== undefined &&
369
- holidayOf(date) === undefined &&
370
- holidayOf(addDays(date, -1)) !== undefined);
392
+ if (rule.holiday || rule.dayAfterHoliday || rule.dayBeforeHoliday) {
393
+ if (holidayOf === undefined)
394
+ return false;
395
+ if (rule.weekdays && !rule.weekdays.has(weekdayOf(date)))
396
+ return false;
397
+ if (rule.holiday) {
398
+ const h = holidayOf(date);
399
+ return h !== undefined && (rule.kinds === null || rule.kinds.has(h.kind));
400
+ }
401
+ // 祝日の翌日は前日が祝日、祝前日は翌日が祝日(どちらもその日自身は祝日でない)
402
+ if (holidayOf(date) !== undefined)
403
+ return false;
404
+ return holidayOf(addDays(date, rule.dayAfterHoliday ? -1 : 1)) !== undefined;
371
405
  }
372
406
  const dim = daysInMonth(year, month);
373
407
  if (rule.weekday !== null) {
@@ -617,8 +651,10 @@ export const RECURRING_RULE_KEYS = [
617
651
  "anchor",
618
652
  "day",
619
653
  "dayAfterHoliday",
654
+ "dayBeforeHoliday",
620
655
  "holiday",
621
656
  "kinds",
657
+ "weekdays",
622
658
  "months",
623
659
  "name",
624
660
  "onHolidayRule",
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * 公開する名前と型は etc/api.md に記録し、pnpm check で意図しない変化を検出する。
9
9
  */
10
10
  export { combineCalendars, createCalendar, createCalendarFromSchedule, type AdjustOptions, type AnnualDate, type AnnualRange, type BusinessDayConvention, type BusinessDayOptions, type Calendar, type CalendarOptions, type CombineOptions, type DateRange, type DayInfo, type DayReason, type DueDateOptions, type HolidayPolicy, type KnownDayReason, type SpecificDate, } from "./core/calendar.ts";
11
- export { createSchedule, type AnnualRangeValue, type AnnualValue, type CivilDateTimeParts, type DateRangeValue, type DateValue, type DayAfterHolidayPattern, type DayOfMonth, type DayOfMonthPattern, type EffectivePeriod, type HolidayLookup, type HolidayPattern, type HolidaySourceOptions, type HolidayValueRule, type KnownScheduleReason, type Month, type Nth, type OnHolidayRule, type RecurringPattern, type RecurringRule, type RecurringValue, type Schedule, type ScheduleDay, type ScheduleOptions, type ScheduleReason, type WeekdayPattern, } from "./core/schedule.ts";
11
+ export { createSchedule, type AnnualRangeValue, type AnnualValue, type CivilDateTimeParts, type DateRangeValue, type DateValue, type DayAfterHolidayPattern, type DayBeforeHolidayPattern, type DayOfMonth, type DayOfMonthPattern, type EffectivePeriod, type HolidayLookup, type HolidayPattern, type HolidaySourceOptions, type HolidayValueRule, type KnownScheduleReason, type Month, type Nth, type OnHolidayRule, type RecurringPattern, type RecurringRule, type RecurringValue, type Schedule, type ScheduleDay, type ScheduleOptions, type ScheduleReason, type WeekdayPattern, } from "./core/schedule.ts";
12
12
  export { addDays, diffDays, isValidDate, weekdayOf, type DateString, type Weekday, } from "./core/date.ts";
13
13
  export { closingMonth, closingPeriod, monthOf, type MonthString } from "./core/month.ts";
14
14
  export { isBizdaysError, type ErrorCode, type KnownErrorCode } from "./core/errors.ts";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 祝日の提供元(HolidayProvider)を実装する人向けの暦の計算(bizdays-toolkit/provider)。
3
+ *
4
+ * 多くの国・地域の祝日は「月の第 n 何曜日」(米国の連邦祝日など)と「復活祭からの日数」(ヨーロッパの多くの国)で
5
+ * 書ける。この 2 種類の計算と、月の日数・うるう年の判定を、ライブラリ内部で使っている実装のまま公開する
6
+ * (復活祭の計算は英国の祝日で GOV.UK のデータと照合済み)。ルートの addDays・weekdayOf と組み合わせて使う。
7
+ * 同梱の提供元の実装とは別に、アプリの本体に不要なものが入らないようサブパスに分けている。
8
+ *
9
+ * 公開する関数は @beta(マイナー版で変わりうる)。使われ方が固まったら @public にする。
10
+ */
11
+ import { type DateString, type Weekday } from "../core/date.ts";
12
+ /**
13
+ * 復活祭の日曜日(グレゴリオ暦)。Good Friday は −2 日、Easter Monday は +1 日、Ascension は +39 日、
14
+ * Whit Monday は +50 日、Corpus Christi は +60 日(ルートの addDays で求める)。
15
+ * 年は 1000〜9999 の整数(それ以外は invalid-argument)。
16
+ * @beta
17
+ */
18
+ export declare function easterSunday(year: number): DateString;
19
+ /**
20
+ * その月の第 n 何曜日(n は 1〜5、-1 は最後。曜日は 0 = 日曜日〜6 = 土曜日)。
21
+ * 例: 1 月の第 3 月曜日は nthWeekdayOfMonth(2026, 1, 1, 3)、5 月の最終月曜日は nthWeekdayOfMonth(2026, 5, 1, -1)。
22
+ * その月に n 回目のその曜日がないとき(第 5 回目)は invalid-argument。
23
+ * @beta
24
+ */
25
+ export declare function nthWeekdayOfMonth(year: number, month: number, weekday: Weekday, n: number): DateString;
26
+ /**
27
+ * その月の日数(28〜31)。
28
+ * @beta
29
+ */
30
+ export declare function daysInMonth(year: number, month: number): number;
31
+ /**
32
+ * うるう年か(グレゴリオ暦)。
33
+ * @beta
34
+ */
35
+ export declare function isLeapYear(year: number): boolean;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * 祝日の提供元(HolidayProvider)を実装する人向けの暦の計算(bizdays-toolkit/provider)。
3
+ *
4
+ * 多くの国・地域の祝日は「月の第 n 何曜日」(米国の連邦祝日など)と「復活祭からの日数」(ヨーロッパの多くの国)で
5
+ * 書ける。この 2 種類の計算と、月の日数・うるう年の判定を、ライブラリ内部で使っている実装のまま公開する
6
+ * (復活祭の計算は英国の祝日で GOV.UK のデータと照合済み)。ルートの addDays・weekdayOf と組み合わせて使う。
7
+ * 同梱の提供元の実装とは別に、アプリの本体に不要なものが入らないようサブパスに分けている。
8
+ *
9
+ * 公開する関数は @beta(マイナー版で変わりうる)。使われ方が固まったら @public にする。
10
+ */
11
+ import { daysInMonth as daysInMonthOf, easterSunday as easterSundayOf, formatDate, isLeapYear as isLeapYearOf, nthWeekdayOfMonth as nthWeekdayOfMonthIn, weekdayOf, } from "../core/date.js";
12
+ import { rangeError } from "../core/errors.js";
13
+ function checkYear(year) {
14
+ if (!Number.isInteger(year) || year < 1000 || year > 9999) {
15
+ throw rangeError("invalid-argument", `year must be an integer from 1000 to 9999: ${String(year)}`);
16
+ }
17
+ }
18
+ function checkMonth(month) {
19
+ if (!Number.isInteger(month) || month < 1 || month > 12) {
20
+ throw rangeError("invalid-argument", `month must be an integer from 1 to 12: ${String(month)}`);
21
+ }
22
+ }
23
+ /**
24
+ * 復活祭の日曜日(グレゴリオ暦)。Good Friday は −2 日、Easter Monday は +1 日、Ascension は +39 日、
25
+ * Whit Monday は +50 日、Corpus Christi は +60 日(ルートの addDays で求める)。
26
+ * 年は 1000〜9999 の整数(それ以外は invalid-argument)。
27
+ * @beta
28
+ */
29
+ export function easterSunday(year) {
30
+ checkYear(year);
31
+ return easterSundayOf(year);
32
+ }
33
+ /**
34
+ * その月の第 n 何曜日(n は 1〜5、-1 は最後。曜日は 0 = 日曜日〜6 = 土曜日)。
35
+ * 例: 1 月の第 3 月曜日は nthWeekdayOfMonth(2026, 1, 1, 3)、5 月の最終月曜日は nthWeekdayOfMonth(2026, 5, 1, -1)。
36
+ * その月に n 回目のその曜日がないとき(第 5 回目)は invalid-argument。
37
+ * @beta
38
+ */
39
+ export function nthWeekdayOfMonth(year, month, weekday, n) {
40
+ checkYear(year);
41
+ checkMonth(month);
42
+ if (!Number.isInteger(weekday) || weekday < 0 || weekday > 6) {
43
+ throw rangeError("invalid-weekday", `weekday must be an integer from 0 (Sunday) to 6 (Saturday): ${String(weekday)}`);
44
+ }
45
+ if (!Number.isInteger(n) || n === 0 || n < -1 || n > 5) {
46
+ throw rangeError("invalid-argument", `n must be an integer from 1 to 5, or -1 (last): ${String(n)}`);
47
+ }
48
+ const days = daysInMonthOf(year, month);
49
+ if (n === -1) {
50
+ const last = formatDate(year, month, days);
51
+ const offset = (weekdayOf(last) - weekday + 7) % 7;
52
+ return formatDate(year, month, days - offset);
53
+ }
54
+ const first = ((weekday - weekdayOf(formatDate(year, month, 1)) + 7) % 7) + 1;
55
+ if (first + (n - 1) * 7 > days) {
56
+ throw rangeError("invalid-argument", `${year}-${String(month).padStart(2, "0")} has no ${n}th weekday ${weekday}`);
57
+ }
58
+ return nthWeekdayOfMonthIn(year, month, weekday, n);
59
+ }
60
+ /**
61
+ * その月の日数(28〜31)。
62
+ * @beta
63
+ */
64
+ export function daysInMonth(year, month) {
65
+ checkYear(year);
66
+ checkMonth(month);
67
+ return daysInMonthOf(year, month);
68
+ }
69
+ /**
70
+ * うるう年か(グレゴリオ暦)。
71
+ * @beta
72
+ */
73
+ export function isLeapYear(year) {
74
+ checkYear(year);
75
+ return isLeapYearOf(year);
76
+ }
@@ -10,7 +10,6 @@
10
10
  * (test/fixtures/govuk-bank-holidays.json)で検証している。CONFIRMED_THROUGH より後の年は、
11
11
  * 規則による予測(confirmed: false)。
12
12
  */
13
- import { type DateString } from "../core/date.ts";
14
13
  import type { Holiday } from "../core/types.ts";
15
14
  /**
16
15
  * 英国の祝日の種別(既知の値)。
@@ -40,7 +39,5 @@ export declare const SUPPORTED_YEARS: Readonly<{
40
39
  export declare const CONFIRMED_THROUGH = 2028;
41
40
  /** @internal 反映済みの GOV.UK のデータの年月(2026 年の Scotland の World Cup bank holiday の追加) */
42
41
  export declare const DATA_VERSION = "2026-02";
43
- /** @internal 復活祭の日曜日(グレゴリオ暦。Anonymous Gregorian algorithm) */
44
- export declare function easterSunday(year: number): DateString;
45
42
  /** @internal その年・区分の祝日(日付順) */
46
43
  export declare function computeUkHolidays(division: UkDivision, year: number): Holiday<UkHolidayKind>[];
package/dist/uk/rules.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * (test/fixtures/govuk-bank-holidays.json)で検証している。CONFIRMED_THROUGH より後の年は、
11
11
  * 規則による予測(confirmed: false)。
12
12
  */
13
- import { addDays, daysInMonth, formatDate, nthWeekdayOfMonth, weekdayOf, } from "../core/date.js";
13
+ import { addDays, daysInMonth, easterSunday, formatDate, nthWeekdayOfMonth, weekdayOf, } from "../core/date.js";
14
14
  import { rangeError } from "../core/errors.js";
15
15
  /** @internal 対応年 */
16
16
  export const SUPPORTED_YEARS = Object.freeze({ from: 2012, to: 2099 });
@@ -62,24 +62,6 @@ const RENAMED = [
62
62
  { division: "scotland", date: "2023-01-02", name: NEW_YEAR, kind: "substitute" },
63
63
  { division: "scotland", date: "2023-01-03", name: SECOND_JANUARY, kind: "substitute" },
64
64
  ];
65
- /** @internal 復活祭の日曜日(グレゴリオ暦。Anonymous Gregorian algorithm) */
66
- export function easterSunday(year) {
67
- const a = year % 19;
68
- const b = Math.floor(year / 100);
69
- const c = year % 100;
70
- const d = Math.floor(b / 4);
71
- const e = b % 4;
72
- const f = Math.floor((b + 8) / 25);
73
- const g = Math.floor((b - f + 1) / 3);
74
- const h = (19 * a + b - d - g + 15) % 30;
75
- const i = Math.floor(c / 4);
76
- const k = c % 4;
77
- const l = (32 + 2 * e + 2 * i - h - k) % 7;
78
- const m = Math.floor((a + 11 * h + 22 * l) / 451);
79
- const month = Math.floor((h + l - 7 * m + 114) / 31);
80
- const day = ((h + l - 7 * m + 114) % 31) + 1;
81
- return formatDate(year, month, day);
82
- }
83
65
  function lastMonday(year, month) {
84
66
  const last = formatDate(year, month, daysInMonth(year, month));
85
67
  return addDays(last, -((weekdayOf(last) - 1 + 7) % 7));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bizdays-toolkit",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
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. / 祝日と営業日のカレンダー(タイムゾーン対応・日本の祝日は内閣府、英国の bank holidays は GOV.UK のデータで全日照合)",
5
5
  "keywords": [
6
6
  "bank-holidays",
@@ -50,6 +50,10 @@
50
50
  "types": "./dist/diagnose/index.d.ts",
51
51
  "default": "./dist/diagnose/index.js"
52
52
  },
53
+ "./provider": {
54
+ "types": "./dist/provider/index.d.ts",
55
+ "default": "./dist/provider/index.js"
56
+ },
53
57
  "./package.json": "./package.json"
54
58
  },
55
59
  "scripts": {