bizdays-toolkit 0.15.0 → 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,32 @@
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
+
7
33
  ## [0.15.0] - 2026-10-21
8
34
 
9
35
  ### 移行
@@ -405,7 +431,8 @@
405
431
 
406
432
  - 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
407
433
 
408
- [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.15.0...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
409
436
  [0.15.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.14.1...v0.15.0
410
437
  [0.14.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.14.0...v0.14.1
411
438
  [0.14.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.13.0...v0.14.0
package/README.md CHANGED
@@ -29,7 +29,7 @@ A business-day calendar that combines public holidays with company or store clos
29
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
30
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
31
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
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
33
33
  - **Extensible to other countries and regions**: holiday providers are pluggable through the `HolidayProvider` interface. Japan and the UK are bundled
34
34
 
35
35
  ## Installation
@@ -38,7 +38,7 @@ A business-day calendar that combines public holidays with company or store clos
38
38
  pnpm add bizdays-toolkit
39
39
  ```
40
40
 
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.
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.
42
42
 
43
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.
44
44
 
@@ -137,9 +137,9 @@ Each day is decided in this order of priority (top first).
137
137
 
138
138
  This order is fixed. Only the relation between a recurring rule and holidays can be chosen per rule with `onHolidayRule` (next section).
139
139
 
140
- ### 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)
141
141
 
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`.
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`.
143
143
 
144
144
  ```ts
145
145
  const museum = createCalendar({
@@ -174,6 +174,7 @@ museum.isBusinessDay("2026-05-07"); // false (the day after the May 3–6 run of
174
174
  | `{ day: 20 }` | The 20th of every month. `day` is 1–31 or -1 (last day); an array gives several |
175
175
  | `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed to the given kinds (`Holiday.kind`); judged before `holidayPolicy` (see below) |
176
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) |
177
178
  | `months: [8, 12]` | Narrows any pattern to these months |
178
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) |
179
180
 
@@ -182,6 +183,7 @@ museum.isBusinessDay("2026-05-07"); // false (the day after the May 3–6 run of
182
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`
183
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
184
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`)
185
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
186
188
 
187
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).
@@ -229,8 +231,15 @@ const hours = createSchedule<Hours>({
229
231
  recurring: [
230
232
  { weekday: 1, value: null, name: "Closed on Mondays", onHolidayRule: "after-holiday" },
231
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
+ },
232
239
  ],
233
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)
234
243
  ```
235
244
 
236
245
  ### Combining calendars
@@ -256,7 +265,7 @@ const bank = createCalendar({
256
265
  const remittance = combineCalendars([company, bank], { businessDay: "all" });
257
266
  remittance.addBusinessDays("2026-12-28", 2); // "2027-01-05"
258
267
  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 }
268
+ // { date: "2026-12-29", weekday: 2, businessDay: false, reason: "closed-annual", name: "New Year closure", rulePath: "closedAnnual[0]", decidedBy: 0, decidedByPath: [0] }
260
269
 
261
270
  // Any of several stores is open
262
271
  const anyStore = combineCalendars([company, bank], { businessDay: "any" });
@@ -264,6 +273,7 @@ anyStore.isBusinessDay("2026-12-29"); // true
264
273
  ```
265
274
 
266
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
267
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
268
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
269
279
  - A combined calendar can be combined again. Option diagnostics work on options, not on calendars, so diagnose each calendar's options separately
@@ -513,7 +523,7 @@ Every method accepts the following inputs. Returned dates are always `"YYYY-MM-D
513
523
  | Temporal `PlainDate`, `PlainDateTime` | A civil date or date and time (no dependency on Temporal) |
514
524
  | Temporal `Instant`, `ZonedDateTime` | An instant |
515
525
 
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).
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).
517
527
 
518
528
  The calendar's time zone is the `timeZone` option, then the holiday provider's time zone, then `"UTC"`.
519
529
 
@@ -562,6 +572,38 @@ const myCountry: HolidayProvider = {
562
572
  const calendar = createCalendar({ holidays: myCountry, closedWeekdays: [0, 6] });
563
573
  ```
564
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
+
565
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.
566
608
 
567
609
  ```ts
@@ -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)の対象にはならないので、
@@ -361,7 +361,8 @@ const isCalendarLike = (v) => typeof v === "object" &&
361
361
  * 営業している日を営業日とする。返すのは通常の営業日カレンダーで、営業日の加減算・調整・期日などが合成結果で動く。
362
362
  *
363
363
  * - dayInfo の reason・name・shiftedFrom・rulePath は、合成結果と同じ判定をした最初のカレンダーの結果で、
364
- * その番号を decidedBy で返す。holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
364
+ * その番号を decidedBy で返す。入れ子の合成では、内側の番号まで含めた列を decidedByPath で返す。
365
+ * holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
365
366
  * - タイムゾーンはすべてのカレンダーで同じでなければならない(time-zone-conflict)。
366
367
  * 2 つ未満、同じカレンダーを 2 回渡した場合は invalid-argument
367
368
  * - 合成したカレンダーをさらに合成することもできる。規則の診断(diagnoseCalendar)の対象にはならないので、
@@ -426,6 +427,8 @@ export function combineCalendars(calendars, options) {
426
427
  if (base.rulePath !== undefined)
427
428
  result.rulePath = base.rulePath;
428
429
  result.decidedBy = decidedBy;
430
+ // 内側が合成したカレンダーなら、その経路の前に自分の番号を付ける(入れ子でなければ長さ 1)
431
+ result.decidedByPath = Object.freeze([decidedBy, ...(base.decidedByPath ?? [])]);
429
432
  return Object.freeze(result);
430
433
  }
431
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);
@@ -390,13 +390,15 @@ function analyze(c, compiled, isCalendar) {
390
390
  ? "This rule matches no day: there are no holidays without a holiday provider"
391
391
  : rule.holiday
392
392
  ? "This rule matches no day: no holiday of the selected kinds, weekdays and months within its effective period"
393
- : "This rule matches no day: no day after a holiday on the selected 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) &&
@@ -405,12 +407,14 @@ function analyze(c, compiled, isCalendar) {
405
407
  setCovers(r.kinds, rule.kinds) &&
406
408
  setCovers(r.weekdays, rule.weekdays) &&
407
409
  setCovers(r.months, rule.months)
408
- : rule.dayAfterHoliday
409
- ? r.dayAfterHoliday &&
410
+ : rule.dayAfterHoliday || rule.dayBeforeHoliday
411
+ ? r.dayAfterHoliday === rule.dayAfterHoliday &&
412
+ r.dayBeforeHoliday === rule.dayBeforeHoliday &&
410
413
  setCovers(r.weekdays, rule.weekdays) &&
411
414
  setCovers(r.months, rule.months)
412
415
  : !r.holiday &&
413
416
  !r.dayAfterHoliday &&
417
+ !r.dayBeforeHoliday &&
414
418
  intervalCovers(r, rule) &&
415
419
  isSubset(bits, recurringBits[i])));
416
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);
@@ -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,6 +91,7 @@ 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;
94
96
  readonly weekdays?: never;
95
97
  readonly kinds?: never;
@@ -108,6 +110,7 @@ export interface DayOfMonthPattern {
108
110
  readonly interval?: never;
109
111
  readonly anchor?: never;
110
112
  readonly dayAfterHoliday?: never;
113
+ readonly dayBeforeHoliday?: never;
111
114
  readonly holiday?: never;
112
115
  readonly weekdays?: never;
113
116
  readonly kinds?: never;
@@ -128,6 +131,28 @@ export interface DayAfterHolidayPattern {
128
131
  readonly interval?: never;
129
132
  readonly anchor?: never;
130
133
  readonly day?: never;
134
+ readonly dayBeforeHoliday?: never;
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;
131
156
  readonly holiday?: never;
132
157
  readonly kinds?: never;
133
158
  }
@@ -152,12 +177,13 @@ export interface HolidayPattern<Kind extends string = string> {
152
177
  readonly anchor?: never;
153
178
  readonly day?: never;
154
179
  readonly dayAfterHoliday?: never;
180
+ readonly dayBeforeHoliday?: never;
155
181
  }
156
182
  /**
157
183
  * 繰り返しのパターン。
158
184
  * @public
159
185
  */
160
- export type RecurringPattern<Kind extends string = string> = WeekdayPattern | DayOfMonthPattern | DayAfterHolidayPattern | HolidayPattern<Kind>;
186
+ export type RecurringPattern<Kind extends string = string> = WeekdayPattern | DayOfMonthPattern | DayAfterHolidayPattern | DayBeforeHolidayPattern | HolidayPattern<Kind>;
161
187
  /**
162
188
  * 繰り返しの規則(営業日カレンダーの closedRecurring・openRecurring)。
163
189
  * @public
@@ -375,11 +401,13 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
375
401
  readonly days: ReadonlySet<number> | null;
376
402
  /** 祝日の翌日の規則か */
377
403
  readonly dayAfterHoliday: boolean;
404
+ /** 祝前日の規則か */
405
+ readonly dayBeforeHoliday: boolean;
378
406
  /** 祝日の規則か */
379
407
  readonly holiday: boolean;
380
408
  /** 祝日の規則で絞る種別(null はすべての祝日) */
381
409
  readonly kinds: ReadonlySet<string> | null;
382
- /** 祝日・祝日の翌日の規則で絞る曜日(null はすべての曜日) */
410
+ /** 祝日・祝日の翌日・祝前日の規則で絞る曜日(null はすべての曜日) */
383
411
  readonly weekdays: ReadonlySet<number> | null;
384
412
  /** 月の集合(null はすべての月) */
385
413
  readonly months: ReadonlySet<number> | null;
@@ -389,12 +417,12 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
389
417
  }
390
418
  /** @internal 繰り返しの規則を検証して取り込む */
391
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>[];
392
- /** @internal その日の祝日を返す関数(祝日・祝日の翌日の規則の判定に使う。提供元の対応年の外は祝日でないとみなすこと) */
420
+ /** @internal その日の祝日を返す関数(祝日・祝日の翌日・祝前日の規則の判定に使う。提供元の対応年の外は祝日でないとみなすこと) */
393
421
  export type HolidayOf = (date: DateString) => Holiday<string> | undefined;
394
422
  /**
395
- * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日の翌日・月)がその日に当てはまるか。
423
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
396
424
  * 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
397
- * 祝日の翌日の規則は isHoliday で判定し、isHoliday を渡さなければ当てはまらない
425
+ * 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない
398
426
  */
399
427
  export declare function matchPattern(rule: RecurringRuleCompiled<unknown>, date: DateString, holidayOf?: HolidayOf): boolean;
400
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"])) {
@@ -311,8 +320,8 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
311
320
  }
312
321
  let weekdays = null;
313
322
  if ("weekdays" in entry) {
314
- if (!hasHoliday && !hasAfter) {
315
- c.error("invalid-option", `${p}.weekdays`, '"weekdays" can be used only with "holiday" or "dayAfterHoliday"');
323
+ if (!hasHoliday && !hasAfter && !hasBefore) {
324
+ c.error("invalid-option", `${p}.weekdays`, '"weekdays" can be used only with "holiday", "dayAfterHoliday" or "dayBeforeHoliday"');
316
325
  ok = false;
317
326
  }
318
327
  else if (!Array.isArray(entry["weekdays"])) {
@@ -357,6 +366,7 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
357
366
  anchor,
358
367
  days,
359
368
  dayAfterHoliday: hasAfter,
369
+ dayBeforeHoliday: hasBefore,
360
370
  holiday: hasHoliday,
361
371
  kinds,
362
372
  weekdays,
@@ -371,15 +381,15 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
371
381
  return rules;
372
382
  }
373
383
  /**
374
- * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日の翌日・月)がその日に当てはまるか。
384
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
375
385
  * 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
376
- * 祝日の翌日の規則は isHoliday で判定し、isHoliday を渡さなければ当てはまらない
386
+ * 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない
377
387
  */
378
388
  export function matchPattern(rule, date, holidayOf) {
379
389
  const { year, month, day } = parseDate(date);
380
390
  if (rule.months && !rule.months.has(month))
381
391
  return false;
382
- if (rule.holiday || rule.dayAfterHoliday) {
392
+ if (rule.holiday || rule.dayAfterHoliday || rule.dayBeforeHoliday) {
383
393
  if (holidayOf === undefined)
384
394
  return false;
385
395
  if (rule.weekdays && !rule.weekdays.has(weekdayOf(date)))
@@ -388,7 +398,10 @@ export function matchPattern(rule, date, holidayOf) {
388
398
  const h = holidayOf(date);
389
399
  return h !== undefined && (rule.kinds === null || rule.kinds.has(h.kind));
390
400
  }
391
- return holidayOf(date) === undefined && holidayOf(addDays(date, -1)) !== undefined;
401
+ // 祝日の翌日は前日が祝日、祝前日は翌日が祝日(どちらもその日自身は祝日でない)
402
+ if (holidayOf(date) !== undefined)
403
+ return false;
404
+ return holidayOf(addDays(date, rule.dayAfterHoliday ? -1 : 1)) !== undefined;
392
405
  }
393
406
  const dim = daysInMonth(year, month);
394
407
  if (rule.weekday !== null) {
@@ -638,6 +651,7 @@ export const RECURRING_RULE_KEYS = [
638
651
  "anchor",
639
652
  "day",
640
653
  "dayAfterHoliday",
654
+ "dayBeforeHoliday",
641
655
  "holiday",
642
656
  "kinds",
643
657
  "weekdays",
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.15.0",
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": {