bizdays-toolkit 0.10.0 → 0.11.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,31 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.11.0] - 2026-10-19
8
+
9
+ ### 移行
10
+
11
+ 作業は要りません(繰り返しの規則の新しい形と、カレンダーを合成する関数の追加)。
12
+
13
+ ### Added
14
+
15
+ - 繰り返しの規則(`closedRecurring`・`openRecurring`・スケジュールの `recurring`)に、祝日の翌日の形 `{ dayAfterHoliday: true }` を追加した。前日が祝日でその日自身は祝日でない日(連休なら連休が明けた日)に当てはまる。「祝日」は提供元の祝日だけで、休業日の設定は含めない。当てはまる日は祝日でないので `onHolidayRule` は指定できない(`invalid-option`)。`months`・`name`・有効期間と組み合わせられる。型 `DayAfterHolidayPattern` を追加した
16
+ - 作成済みの営業日カレンダーを合成する `combineCalendars(calendars, { businessDay: "all" | "any", maxSearchDays? })` を追加した。すべてが営業している日(`"all"`)、またはいずれかが営業している日(`"any"`)を営業日とする通常のカレンダーを返し、営業日の加減算・調整・期日などが合成結果で動く。`dayInfo` の `reason`・`name`・`shiftedFrom` は合成結果と同じ判定をした最初のカレンダーの結果で、その番号を新しい項目 `decidedBy` で返す。祝日は各カレンダーの提供元の和集合で判定する。タイムゾーンが違えば `time-zone-conflict`、2 つ未満・同じカレンダーの重複は `invalid-argument`。型 `CombineOptions` を追加した
17
+ - `DayInfo` に省略可能な `decidedBy`(合成したカレンダーで判定を決めたカレンダーの番号)を追加した。合成していないカレンダーでは付かない
18
+ - `HolidayLookup`(営業日カレンダー・スケジュール)に `outOfRange`(設定の値)を追加した。合成するときに引き継ぐため
19
+
20
+ ### Changed
21
+
22
+ - 規則の診断で、祝日の翌日の規則を扱えるようにした。提供元のない設定では `never-matches`。`shadowed` は祝日の翌日の規則どうし(`months` と有効期間)でだけ調べ、曜日の規則との重なりは `redundant` で見つける
23
+ - 繰り返しの規則の形の誤りのメッセージを、`"weekday"`・`"day"`・`"dayAfterHoliday"` のうち 1 つだけを持つ、に改めた(code は `invalid-option` のまま)
24
+ - README: 繰り返しの規則の節に祝日の翌日を、新しい節「カレンダーの合成」を足した。サイズの値を測り直した(ルート約 8.6KB・日本の祝日込み約 10.3KB・英国の祝日込み約 10.2KB。0.11.0 で測定)
25
+
26
+ ### 開発
27
+
28
+ - サイズの上限(`size-limits.json`)を上げた。ルート 17,500 → 19,500、`./jp` 20,500 → 22,500、`./diagnose` 24,000 → 26,000(gzip)。祝日の翌日の規則と `combineCalendars`(ルートで約 1.7KB)と、診断の祝日の翌日への対応(約 1.9KB)のため
29
+ - `createCalendar` の営業日の探索・調整・月の営業日の実装を、祝日を調べる機能と 1 日分の判定を引数に取る内部関数(`buildCalendar`)に切り出し、`combineCalendars` と共用した。公開 API と結果は変わらない
30
+ - 性質のテストに祝日の翌日の規則を加えた
31
+
7
32
  ## [0.10.0] - 2026-10-19
8
33
 
9
34
  ### 移行
@@ -266,7 +291,8 @@
266
291
 
267
292
  - 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
268
293
 
269
- [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.10.0...HEAD
294
+ [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.11.0...HEAD
295
+ [0.11.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.10.0...v0.11.0
270
296
  [0.10.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.9.1...v0.10.0
271
297
  [0.9.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.9.0...v0.9.1
272
298
  [0.9.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.8.1...v0.9.0
package/README.md CHANGED
@@ -20,13 +20,14 @@ A business-day calendar that combines public holidays with company or store clos
20
20
 
21
21
  - **Japanese holidays verified day by day against the Cabinet Office data**: from 1955 through the latest year announced in the Official Gazette, every date in the Cabinet Office "National Holidays" CSV is checked in the tests (substitute holidays, citizens' holidays, holidays for imperial ceremonies and the Olympic moves included)
22
22
  - **UK bank holidays verified day by day against GOV.UK**: choose England, Wales, Scotland or Northern Ireland. From 2012 through the last year GOV.UK has published, every date and name is checked against the GOV.UK data in the tests, and differences are watched daily
23
- - **Recurring rules**: rules such as "closed on the 3rd Wednesday", "closed every other Wednesday", "stocktaking on the last day of the month" or "closed on Mondays (on the next weekday if Monday is a holiday)", together with what happens when they fall on a holiday (follow the holiday, apply anyway, or move to the next day). Rules can have an effective period, so "closed on Tuesdays instead of Mondays from April 2027" is one calendar
23
+ - **Recurring rules**: rules such as "closed on the 3rd Wednesday", "closed every other Wednesday", "stocktaking on the last day of the month", "closed on Mondays (on the next weekday if Monday is a holiday)" or "closed on the day after a holiday", together with what happens when they fall on a holiday (follow the holiday, apply anyway, or move to the next day). Rules can have an effective period, so "closed on Tuesdays instead of Mondays from April 2027" is one calendar
24
24
  - **Day-by-day values (e.g. store hours)**: values that change by weekday, holiday and specific date, such as "closed on Tuesdays, Sunday hours on holidays, closed on New Year's Day, short hours on New Year's Eve", are looked up with the same rules
25
25
  - **Option diagnostics**: `diagnoseCalendar` and `diagnoseSchedule` from `bizdays-toolkit/diagnose` list rules that can never apply, rules whose removal changes nothing, typos in option keys, calendars with no business day and more, without throwing. One line in your tests keeps a change of settings from silently killing a rule
26
26
  - **Month-based due dates**: due dates such as "closing on the 20th, paid on the 10th of the next month (next business day if closed)" or "the last business day of the month", with the four adjustment conventions used in finance (following, preceding, modified-following, modified-preceding)
27
27
  - **Business-day arithmetic**: closed weekdays, annual closures (including ranges across the new year), specific closed and open dates, and holidays combine to compute "N business days later", "next business day after the cut-off time" and more
28
+ - **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
28
29
  - **Time-zone safe**: instants such as `Date` are converted to a civil date in the calendar's time zone (`Asia/Tokyo` for Japan, `Europe/London` for the UK) before evaluation. A server running in UTC does not treat 00:00–09:00 in Japan as the previous day
29
- - **Lightweight, zero dependencies**: about 7.9 KB min+gzip (about 9.8 KB with Japanese holidays, about 9.6 KB with UK bank holidays; measured for 0.10.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
30
+ - **Lightweight, zero dependencies**: about 8.6 KB min+gzip (about 10.3 KB with Japanese holidays, about 10.2 KB with UK bank holidays; measured for 0.11.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
30
31
  - **Extensible to other countries and regions**: holiday providers are pluggable through the `HolidayProvider` interface. Japan and the UK are bundled
31
32
 
32
33
  ## Installation
@@ -134,9 +135,9 @@ Each day is decided in this order of priority (top first).
134
135
 
135
136
  This order is fixed. Only the relation between a recurring rule and holidays can be chosen per rule with `onHolidayRule` (next section).
136
137
 
137
- ### Recurring rules (weekly, every other week, nth weekday, day of month)
138
+ ### Recurring rules (weekly, every other week, nth weekday, day of month, day after a holiday)
138
139
 
139
- 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" or "closed on Mondays (on the next weekday if Monday is a holiday)" go in `closedRecurring` and `openRecurring`.
140
+ 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)" or "closed on the day after a holiday" go in `closedRecurring` and `openRecurring`.
140
141
 
141
142
  ```ts
142
143
  const museum = createCalendar({
@@ -151,6 +152,7 @@ const museum = createCalendar({
151
152
  name: "Closed on the 3rd Wednesday in February and August",
152
153
  },
153
154
  { day: -1, name: "Stocktaking", onHolidayRule: "over-holiday" }, // closed even on holidays
155
+ { dayAfterHoliday: true, name: "Closed on the day after a holiday" },
154
156
  ],
155
157
  });
156
158
 
@@ -158,6 +160,8 @@ museum.isBusinessDay("2026-10-12"); // true (open on Monday, Sports Day)
158
160
  museum.dayInfo("2026-10-13");
159
161
  // { date: "2026-10-13", weekday: 2, businessDay: false, reason: "closed-recurring",
160
162
  // name: "Closed on Mondays", shiftedFrom: "2026-10-12" }
163
+ museum.dayInfo("2026-02-12").name; // "Closed on the day after a holiday" (National Foundation Day is February 11)
164
+ museum.isBusinessDay("2026-05-07"); // false (the day after the May 3–6 run of holidays)
161
165
  ```
162
166
 
163
167
  | Pattern | Meaning |
@@ -166,13 +170,15 @@ museum.dayInfo("2026-10-13");
166
170
  | `{ weekday: 3, nth: 3 }` | The 3rd Wednesday. `nth` is 1–5 or -1 (last); an array such as `[2, 4]` gives several |
167
171
  | `{ weekday: 3, interval: 2, anchor: "2026-01-07" }` | Every other Wednesday: the Wednesdays an even number of weeks from `anchor` (2 or more weeks; see below) |
168
172
  | `{ day: 20 }` | The 20th of every month. `day` is 1–31 or -1 (last day); an array gives several |
173
+ | `{ dayAfterHoliday: true }` | The day after a holiday: the first non-holiday after a holiday or a run of holidays (holidays of the provider only) |
169
174
  | `months: [8, 12]` | Narrows any pattern to these months |
170
175
  | `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) |
171
176
 
172
177
  - **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
173
178
  - **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
174
179
  - **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`
175
- - Among recurring rules, the one written first in the array wins, and `openRecurring` wins over `closedRecurring`
180
+ - **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
181
+ - 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
176
182
 
177
183
  **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).
178
184
 
@@ -222,6 +228,43 @@ const hours = createSchedule<Hours>({
222
228
  });
223
229
  ```
224
230
 
231
+ ### Combining calendars
232
+
233
+ `combineCalendars` makes one calendar out of several: the days on which **all** of them are open (`businessDay: "all"`), or on which **any** of them is open (`"any"`). The result is an ordinary calendar, so business-day arithmetic, `adjust`, `dueDate` and the rest work on the combination.
234
+
235
+ ```ts
236
+ import { combineCalendars, createCalendar } from "bizdays-toolkit";
237
+ import { jp } from "bizdays-toolkit/jp";
238
+
239
+ const company = createCalendar({
240
+ holidays: jp,
241
+ closedWeekdays: [0, 6],
242
+ closedAnnual: [{ from: "12-29", to: "01-03", name: "New Year closure" }],
243
+ });
244
+ const bank = createCalendar({
245
+ holidays: jp,
246
+ closedWeekdays: [0, 6],
247
+ closedAnnual: [{ from: "12-31", to: "01-03", name: "Bank holiday period" }],
248
+ });
249
+
250
+ // A remittance is processed only on days when both the company and the bank are open
251
+ const remittance = combineCalendars([company, bank], { businessDay: "all" });
252
+ remittance.addBusinessDays("2026-12-28", 2); // "2027-01-05"
253
+ remittance.dayInfo("2026-12-29");
254
+ // { date: "2026-12-29", weekday: 2, businessDay: false, reason: "closed-annual", name: "New Year closure", decidedBy: 0 }
255
+
256
+ // Any of several stores is open
257
+ const anyStore = combineCalendars([company, bank], { businessDay: "any" });
258
+ anyStore.isBusinessDay("2026-12-29"); // true
259
+ ```
260
+
261
+ - `dayInfo` reports `reason`, `name` and `shiftedFrom` from the first calendar whose result equals the combined result, and the index of that calendar in `decidedBy`. 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
262
+ - `holiday`, `isHoliday`, `holidaysInYear` and so on use the union of the calendars' holiday providers. `outOfRange` is `"throw"` if any calendar throws outside its supported years
263
+ - 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
264
+ - A combined calendar can be combined again. Option diagnostics work on options, not on calendars, so diagnose each calendar's options separately
265
+
266
+ Combining options instead would not work: a specific open day (`openDates`) in one set of options would override closures in the other, and wrapping one calendar as a holiday provider of the other would turn its closures into "holidays" (`isHoliday` would be true, and `holidayPolicy: "open"` would open on them).
267
+
225
268
  ### Common recipes
226
269
 
227
270
  **Treat citizens' holidays (days between two holidays) as business days**: wrap the holiday provider and filter by kind.
@@ -474,21 +517,22 @@ diagnoseCalendar({
474
517
  // ]
475
518
  ```
476
519
 
477
- | `severity` | `code` | Meaning |
478
- | ---------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
479
- | `error` | The same codes as exceptions | Errors that make `createCalendar` or `createSchedule` throw. Creation stops at the first one; diagnostics return them all |
480
- | `error` | `invalid-provider-data` | Errors in the data returned by a holiday provider. Creation checks only the years used; diagnostics check every supported year |
481
- | `warning` | `unknown-option` | Unknown keys (ignored at creation). If a close known key exists, `message` suggests it |
482
- | `warning` | `shadowed` | A rule fully covered by an earlier rule of the same type (`relatedPath`) and never used |
483
- | `warning` | `never-matches` | A recurring rule that matches no day (such as `{ day: 30, months: [2] }`, or a 5th Wednesday with an effective period that has none) |
484
- | `warning` | `out-of-range` | A specific date, or the effective period of a rule, outside the supported years of the holiday provider (`relatedPath`) |
485
- | `warning` | `no-business-day` | No business day at all (calendars only; searching for a business day throws) |
486
- | `info` | `redundant` | A rule whose removal changes nothing (covered by a combination of earlier rules, same as a closed weekday, the same value as the weekday in a schedule, and so on) |
520
+ | `severity` | `code` | Meaning |
521
+ | ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
522
+ | `error` | The same codes as exceptions | Errors that make `createCalendar` or `createSchedule` throw. Creation stops at the first one; diagnostics return them all |
523
+ | `error` | `invalid-provider-data` | Errors in the data returned by a holiday provider. Creation checks only the years used; diagnostics check every supported year |
524
+ | `warning` | `unknown-option` | Unknown keys (ignored at creation). If a close known key exists, `message` suggests it |
525
+ | `warning` | `shadowed` | A rule fully covered by an earlier rule of the same type (`relatedPath`) and never used |
526
+ | `warning` | `never-matches` | A recurring rule that matches no day (such as `{ day: 30, months: [2] }`, a 5th Wednesday with an effective period that has none, or `dayAfterHoliday` without a holiday provider) |
527
+ | `warning` | `out-of-range` | A specific date, or the effective period of a rule, outside the supported years of the holiday provider (`relatedPath`) |
528
+ | `warning` | `no-business-day` | No business day at all (calendars only; searching for a business day throws) |
529
+ | `info` | `redundant` | A rule whose removal changes nothing (covered by a combination of earlier rules, same as a closed weekday, the same value as the weekday in a schedule, and so on) |
487
530
 
488
531
  - Branch on `code`. `message` is in English and its wording may change. Future versions may add codes
489
532
  - When there is an `error`, the rule relation checks (`shadowed`, `redundant` and so on) are skipped
490
533
  - `shadowed` compares only rules of the same type (specific dates, annual dates, recurring rules). The order of evaluation is fixed, so rules unused across types are found as `redundant`. Specific open days (`openDates`) and specific closed days (`closedDates`) are of the same type, and a closed day covered by an earlier open day is `shadowed`
491
534
  - `shadowed` for recurring rules also considers `onHolidayRule`. For example, an `"under-holiday"` rule does not cover an `"over-holiday"` rule on the same days (the later rule applies on holidays)
535
+ - `shadowed` for `dayAfterHoliday` rules is checked only against other `dayAfterHoliday` rules (by `months` and period), because which days they match depend on the holidays of each year; overlap with weekday rules is found as `redundant`
492
536
  - `shadowed` for rules with effective periods also requires the earlier rule's period to contain the later rule's period, and for `interval` rules the earlier rule's cycle to contain the later one (a weekly rule covers an every-other-week rule; two every-other-week rules with opposite phases do not cover each other)
493
537
  - `redundant` compares the results with and without the rule day by day. Specific dates are checked over their range; annual dates and recurring rules are checked, with specific dates removed, over the holiday provider's supported years (1955–2099 for Japan), or 2001–2028 without a provider (28 years cover every combination of weekday and date; with `interval` rules the range is extended to the length of their cycle, such as 56 years for every other week). Effective periods outside that range are checked as well (with `outOfRange: "throw"`, only within the supported years). `message` names the years checked. Schedule values are compared by content for arrays and plain objects, and by identity otherwise (such as `Date`)
494
538
  - With Japanese holidays, checking every day takes a few hundred milliseconds. Use it in tests or at startup, not per request. As a development and test tool, it lives in a subpath so it stays out of your app's main bundle
@@ -15,6 +15,8 @@
15
15
  * 祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)
16
16
  *
17
17
  * 内部では、営業日かどうか(真偽値)を返すスケジュール(createSchedule)として作っている。
18
+ * 営業日の探索・調整・月の営業日などの機能は buildCalendar に切り出し、作成済みのカレンダーを
19
+ * 合成する combineCalendars(両方が営業している日・どちらかが営業している日)でも同じ実装を使う。
18
20
  */
19
21
  import { type DateString, type Weekday } from "./date.ts";
20
22
  import { IssueCollector } from "./errors.ts";
@@ -114,6 +116,24 @@ export interface DayInfo<Kind extends string = string> {
114
116
  readonly holiday?: Holiday<Kind>;
115
117
  /** 祝日から振り替えられた繰り返しの規則で決まった場合、振替の元の日付(onHolidayRule: "after-holiday") */
116
118
  readonly shiftedFrom?: DateString;
119
+ /**
120
+ * 合成したカレンダー(combineCalendars)で、判定を決めたカレンダーの番号(渡した配列の位置)。
121
+ * reason・name・shiftedFrom はそのカレンダーの結果。合成していないカレンダーでは付かない
122
+ */
123
+ readonly decidedBy?: number;
124
+ }
125
+ /**
126
+ * カレンダーの合成のオプション。
127
+ * @public
128
+ */
129
+ export interface CombineOptions {
130
+ /**
131
+ * 営業日とする条件。"all": すべてのカレンダーが営業日(例: 自社と銀行の両方が営業している日)/
132
+ * "any": いずれかのカレンダーが営業日(例: 複数の店舗のどこかが開いている日)
133
+ */
134
+ readonly businessDay: "all" | "any";
135
+ /** 営業日を探すときに調べる最大日数(既定: 3660)。超えたら Error */
136
+ readonly maxSearchDays?: number;
117
137
  }
118
138
  /**
119
139
  * 営業日を探すときのオプション。
@@ -217,4 +237,17 @@ export declare function compileCalendarOptions<Kind extends string>(c: IssueColl
217
237
  * @public
218
238
  */
219
239
  export declare function createCalendar<Kind extends string = string>(options?: CalendarOptions<Kind>): Calendar<Kind>;
240
+ /**
241
+ * 作成済みの営業日カレンダーを合成する。"all" はすべてのカレンダーが営業している日、"any" はいずれかが
242
+ * 営業している日を営業日とする。返すのは通常の営業日カレンダーで、営業日の加減算・調整・期日などが合成結果で動く。
243
+ *
244
+ * - dayInfo の reason・name・shiftedFrom は、合成結果と同じ判定をした最初のカレンダーの結果で、
245
+ * その番号を decidedBy で返す。holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
246
+ * - タイムゾーンはすべてのカレンダーで同じでなければならない(time-zone-conflict)。
247
+ * 2 つ未満、同じカレンダーを 2 回渡した場合は invalid-argument
248
+ * - 合成したカレンダーをさらに合成することもできる。規則の診断(diagnoseCalendar)の対象にはならないので、
249
+ * 元のカレンダーの設定をそれぞれ診断する
250
+ * @public
251
+ */
252
+ export declare function combineCalendars<Kind extends string = string>(calendars: readonly Calendar<Kind>[], options: CombineOptions): Calendar<Kind>;
220
253
  export {};
@@ -15,12 +15,14 @@
15
15
  * 祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)
16
16
  *
17
17
  * 内部では、営業日かどうか(真偽値)を返すスケジュール(createSchedule)として作っている。
18
+ * 営業日の探索・調整・月の営業日などの機能は buildCalendar に切り出し、作成済みのカレンダーを
19
+ * 合成する combineCalendars(両方が営業している日・どちらかが営業している日)でも同じ実装を使う。
18
20
  */
19
21
  import { addDays, daysInMonth, formatDate, fromDayNumber, toDayNumber, } from "./date.js";
20
22
  import { checkUnknownKeys, IssueCollector, plainError, rangeError } from "./errors.js";
21
23
  import { parseTimeOfDay, toCivil } from "./input.js";
22
24
  import { dayOfMonth, isDayOfMonthValue, parseMonth } from "./month.js";
23
- import { buildSchedule, compileAnnualRules, compileDateRules, compileHolidaySource, compileRecurringRules, isWeekday, mapDays, RECURRING_RULE_KEYS, } from "./schedule.js";
25
+ import { buildSchedule, compileAnnualRules, compileDateRules, compileHolidaySource, compileRecurringRules, createHolidayLookup, isWeekday, mapDays, RECURRING_RULE_KEYS, } from "./schedule.js";
24
26
  const CALENDAR_OPTION_KEYS = [
25
27
  "holidays",
26
28
  "timeZone",
@@ -139,8 +141,14 @@ export function createCalendar(options = {}) {
139
141
  result.shiftedFrom = day.shiftedFrom;
140
142
  return Object.freeze(result);
141
143
  }
142
- const isOpen = (date) => schedule.valueOn(date);
143
- const dateOf = schedule.dateOf;
144
+ return buildCalendar(schedule, (date) => schedule.valueOn(date), info, maxSearchDays);
145
+ }
146
+ /**
147
+ * @internal 祝日を調べる機能と 1 日分の判定から、営業日カレンダーの機能(探索・調整・月の営業日・期日)を組み立てる。
148
+ * createCalendar と combineCalendars で共用する
149
+ */
150
+ function buildCalendar(lookup, isOpen, info, maxSearchDays) {
151
+ const dateOf = lookup.dateOf;
144
152
  function search(start, step) {
145
153
  let n = toDayNumber(start);
146
154
  for (let i = 0; i < maxSearchDays; i++, n += step) {
@@ -226,22 +234,23 @@ export function createCalendar(options = {}) {
226
234
  return found;
227
235
  }
228
236
  const calendar = {
229
- timeZone: schedule.timeZone,
230
- providers: schedule.providers,
237
+ timeZone: lookup.timeZone,
238
+ providers: lookup.providers,
239
+ outOfRange: lookup.outOfRange,
231
240
  dateOf,
232
- civilOf: schedule.civilOf,
233
- today: schedule.today,
234
- isHoliday: schedule.isHoliday,
235
- getHoliday: schedule.getHoliday,
236
- holidaysInYear: schedule.holidaysInYear,
237
- holidaysBetween: schedule.holidaysBetween,
241
+ civilOf: lookup.civilOf,
242
+ today: lookup.today,
243
+ isHoliday: lookup.isHoliday,
244
+ getHoliday: lookup.getHoliday,
245
+ holidaysInYear: lookup.holidaysInYear,
246
+ holidaysBetween: lookup.holidaysBetween,
238
247
  dayInfo: (input) => info(dateOf(input)),
239
248
  daysBetween: (from, to) => mapDays(dateOf(from), dateOf(to), info),
240
249
  isBusinessDay: (input) => isOpen(dateOf(input)),
241
250
  nextBusinessDay: (input) => search(addDays(dateOf(input), 1), 1),
242
251
  previousBusinessDay: (input) => search(addDays(dateOf(input), -1), -1),
243
252
  businessDayOnOrAfter(input, opts = {}) {
244
- const civil = toCivil(input, schedule.timeZone);
253
+ const civil = toCivil(input, lookup.timeZone);
245
254
  let start = civil.date;
246
255
  if (opts.cutoff !== undefined) {
247
256
  const cutoff = parseTimeOfDay(opts.cutoff);
@@ -295,3 +304,81 @@ export function createCalendar(options = {}) {
295
304
  };
296
305
  return Object.freeze(calendar);
297
306
  }
307
+ const isCalendarLike = (v) => typeof v === "object" &&
308
+ v !== null &&
309
+ typeof v.dayInfo === "function" &&
310
+ typeof v.isBusinessDay === "function" &&
311
+ typeof v.timeZone === "string" &&
312
+ Array.isArray(v.providers);
313
+ /**
314
+ * 作成済みの営業日カレンダーを合成する。"all" はすべてのカレンダーが営業している日、"any" はいずれかが
315
+ * 営業している日を営業日とする。返すのは通常の営業日カレンダーで、営業日の加減算・調整・期日などが合成結果で動く。
316
+ *
317
+ * - dayInfo の reason・name・shiftedFrom は、合成結果と同じ判定をした最初のカレンダーの結果で、
318
+ * その番号を decidedBy で返す。holiday・isHoliday などは、すべてのカレンダーの祝日の提供元の和集合で判定する
319
+ * - タイムゾーンはすべてのカレンダーで同じでなければならない(time-zone-conflict)。
320
+ * 2 つ未満、同じカレンダーを 2 回渡した場合は invalid-argument
321
+ * - 合成したカレンダーをさらに合成することもできる。規則の診断(diagnoseCalendar)の対象にはならないので、
322
+ * 元のカレンダーの設定をそれぞれ診断する
323
+ * @public
324
+ */
325
+ export function combineCalendars(calendars, options) {
326
+ if (!Array.isArray(calendars) || calendars.length < 2) {
327
+ throw rangeError("invalid-argument", "calendars must be an array of two or more calendars");
328
+ }
329
+ calendars.forEach((c, i) => {
330
+ if (!isCalendarLike(c))
331
+ throw rangeError("invalid-argument", `calendars[${i}] is not a calendar`);
332
+ if (calendars.indexOf(c) !== i) {
333
+ throw rangeError("invalid-argument", `calendars[${i}] is the same calendar as calendars[${calendars.indexOf(c)}]`);
334
+ }
335
+ });
336
+ const mode = options?.businessDay;
337
+ if (mode !== "all" && mode !== "any") {
338
+ throw rangeError("invalid-option", 'businessDay must be "all" or "any"');
339
+ }
340
+ const maxSearchDays = options.maxSearchDays ?? 3660;
341
+ if (!Number.isInteger(maxSearchDays) || maxSearchDays < 1) {
342
+ throw rangeError("invalid-option", "maxSearchDays must be a positive integer");
343
+ }
344
+ const zones = [...new Set(calendars.map((c) => c.timeZone))];
345
+ if (zones.length > 1) {
346
+ throw rangeError("time-zone-conflict", `Calendars use different time zones (${zones.join(", ")}); combine calendars with the same time zone`);
347
+ }
348
+ const providers = [];
349
+ for (const c of calendars)
350
+ for (const p of c.providers)
351
+ if (!providers.includes(p))
352
+ providers.push(p);
353
+ const lookup = createHolidayLookup({
354
+ providers: Object.freeze(providers),
355
+ timeZone: zones[0],
356
+ outOfRange: calendars.every((c) => c.outOfRange === "ignore") ? "ignore" : "throw",
357
+ });
358
+ const isOpen = (date) => mode === "all"
359
+ ? calendars.every((c) => c.isBusinessDay(date))
360
+ : calendars.some((c) => c.isBusinessDay(date));
361
+ function info(date) {
362
+ const results = calendars.map((c) => c.dayInfo(date));
363
+ const open = mode === "all" ? results.every((r) => r.businessDay) : results.some((r) => r.businessDay);
364
+ // 合成結果と同じ判定をした最初のカレンダー("all" で休業なら最初に休みのもの、営業ならすべて営業なので 0 番)
365
+ const decidedBy = results.findIndex((r) => r.businessDay === open);
366
+ const base = results[decidedBy];
367
+ const result = {
368
+ date,
369
+ weekday: base.weekday,
370
+ businessDay: open,
371
+ reason: base.reason,
372
+ };
373
+ if (base.name !== undefined)
374
+ result.name = base.name;
375
+ const holiday = lookup.holidayOn(date);
376
+ if (holiday !== undefined)
377
+ result.holiday = holiday;
378
+ if (base.shiftedFrom !== undefined)
379
+ result.shiftedFrom = base.shiftedFrom;
380
+ result.decidedBy = decidedBy;
381
+ return Object.freeze(result);
382
+ }
383
+ return buildCalendar(lookup, isOpen, info, maxSearchDays);
384
+ }
@@ -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: 取り除いても結果が変わらない規則
@@ -22,7 +22,7 @@
22
22
  import { daysInMonth, fromDayNumber, parseDate, toDayNumber } from "./date.js";
23
23
  import { compileCalendarOptions } from "./calendar.js";
24
24
  import { IssueCollector } from "./errors.js";
25
- import { compileScheduleOptions, createHolidayLookup, createResolver, matchMonthDay, matchPattern, } from "./schedule.js";
25
+ import { compileScheduleOptions, createHolidayLookup, createResolver, inPeriod, matchMonthDay, matchPattern, } from "./schedule.js";
26
26
  // ------------------------------------------------------------
27
27
  // 知らないキーの候補
28
28
  // ------------------------------------------------------------
@@ -358,20 +358,48 @@ function analyze(c, compiled, isCalendar) {
358
358
  });
359
359
  // --- 繰り返しの規則: 当てはまる日がない・覆われている ---
360
360
  const recurringBits = recurring.map(patternBits);
361
+ /** 祝日の翌日の規則の判定に使う(提供元の対応年の外は祝日でないとみなす) */
362
+ const isHolidaySafe = (date) => {
363
+ try {
364
+ return lookup.holidayOn(date) !== undefined;
365
+ }
366
+ catch {
367
+ return false;
368
+ }
369
+ };
370
+ /** 祝日の翌日の規則が、調べる期間と有効期間の中で 1 日でも当てはまるか(祝日は年ごとに違うので、全日を調べる) */
371
+ const dayAfterHolidayMatches = (rule) => {
372
+ if (source.providers.length === 0 || !window)
373
+ return false;
374
+ let found = false;
375
+ eachDay(window, (n) => (found = inPeriod(rule, n) && matchPattern(rule, fromDayNumber(n), isHolidaySafe)));
376
+ return found;
377
+ };
361
378
  recurring.forEach((rule, j) => {
362
379
  const bits = recurringBits[j];
363
- if (!matchesAnyDay(rule, cycle)) {
380
+ const matches = rule.dayAfterHoliday
381
+ ? dayAfterHolidayMatches(rule)
382
+ : matchesAnyDay(rule, cycle);
383
+ if (!matches) {
364
384
  unused.add(rule);
365
- c.warning("never-matches", rule.path, rule.effectiveFrom === null && rule.effectiveTo === null
366
- ? "This rule matches no day (for example, a day that does not exist in the selected months)"
367
- : "This rule matches no day within its effective period");
385
+ c.warning("never-matches", rule.path, rule.dayAfterHoliday
386
+ ? source.providers.length === 0
387
+ ? "This rule matches no day: there are no holidays without a holiday provider"
388
+ : "This rule matches no day: no day after a holiday within its effective period"
389
+ : rule.effectiveFrom === null && rule.effectiveTo === null
390
+ ? "This rule matches no day (for example, a day that does not exist in the selected months)"
391
+ : "This rule matches no day within its effective period");
368
392
  return;
369
393
  }
394
+ // 祝日の翌日の規則は祝日次第で年ごとに変わるので、形の包含は祝日の翌日の規則どうし(月の絞り込み)だけで調べる
370
395
  const k = recurring.findIndex((r, i) => i < j &&
371
396
  canCover(r, rule) &&
372
397
  periodCovers(r, rule) &&
373
- intervalCovers(r, rule) &&
374
- isSubset(bits, recurringBits[i]));
398
+ (rule.dayAfterHoliday
399
+ ? r.dayAfterHoliday &&
400
+ (r.months === null ||
401
+ (rule.months !== null && [...rule.months].every((m) => r.months.has(m))))
402
+ : !r.dayAfterHoliday && intervalCovers(r, rule) && isSubset(bits, recurringBits[i])));
375
403
  if (k < 0)
376
404
  return;
377
405
  unused.add(rule);
@@ -11,7 +11,8 @@
11
11
  *
12
12
  * "under-holiday" の規則は、祝日の扱いが "weekday"(曜日どおり)なら祝日にも当てはまる。
13
13
  * "after-holiday" の規則は、祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)。
14
- * 毎年の月日・繰り返しの規則の有効期間(effectiveFrom・effectiveTo)の外では、その規則は書かれていないのと同じ
14
+ * 毎年の月日・繰り返しの規則の有効期間(effectiveFrom・effectiveTo)の外では、その規則は書かれていないのと同じ。
15
+ * 祝日の翌日の規則(dayAfterHoliday)は、前日が祝日でその日自身は祝日でない日に当てはまる(提供元の祝日だけで判定)
15
16
  *
16
17
  * 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
17
18
  */
@@ -87,6 +88,7 @@ export interface WeekdayPattern {
87
88
  /** 当てはめる月(省略時はすべての月) */
88
89
  readonly months?: readonly Month[];
89
90
  readonly day?: never;
91
+ readonly dayAfterHoliday?: never;
90
92
  }
91
93
  /**
92
94
  * 毎月の日の繰り返し(例: 毎月 20 日、毎月末日)。その月にない日(2 月 30 日など)は当てはまらない。
@@ -101,12 +103,28 @@ export interface DayOfMonthPattern {
101
103
  readonly nth?: never;
102
104
  readonly interval?: never;
103
105
  readonly anchor?: never;
106
+ readonly dayAfterHoliday?: never;
107
+ }
108
+ /**
109
+ * 祝日の翌日(祝日が続く場合は連休が明けた日。その日自身は祝日でない)。「祝日」は提供元の祝日だけ。
110
+ * 当てはまる日は祝日でないので、onHolidayRule は指定できない(指定すると invalid-option)。
111
+ * @public
112
+ */
113
+ export interface DayAfterHolidayPattern {
114
+ readonly dayAfterHoliday: true;
115
+ /** 当てはめる月(翌日の月。省略時はすべての月) */
116
+ readonly months?: readonly Month[];
117
+ readonly weekday?: never;
118
+ readonly nth?: never;
119
+ readonly interval?: never;
120
+ readonly anchor?: never;
121
+ readonly day?: never;
104
122
  }
105
123
  /**
106
124
  * 繰り返しのパターン。
107
125
  * @public
108
126
  */
109
- export type RecurringPattern = WeekdayPattern | DayOfMonthPattern;
127
+ export type RecurringPattern = WeekdayPattern | DayOfMonthPattern | DayAfterHolidayPattern;
110
128
  /**
111
129
  * 繰り返しの規則(営業日カレンダーの closedRecurring・openRecurring)。
112
130
  * @public
@@ -229,6 +247,8 @@ export interface HolidayLookup<Kind extends string = string> {
229
247
  readonly timeZone: string;
230
248
  /** 祝日の提供元 */
231
249
  readonly providers: readonly HolidayProvider<Kind>[];
250
+ /** 祝日の提供元の対応年を外れた日付の扱い(設定の outOfRange) */
251
+ readonly outOfRange: "throw" | "ignore";
232
252
  /** 入力をタイムゾーンでの暦日に変換する */
233
253
  dateOf(input: DateInput): DateString;
234
254
  /** 入力をタイムゾーンでの暦日と時刻に変換する */
@@ -310,6 +330,8 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
310
330
  readonly anchor: number | null;
311
331
  /** 日の集合(曜日の規則では null) */
312
332
  readonly days: ReadonlySet<number> | null;
333
+ /** 祝日の翌日の規則か */
334
+ readonly dayAfterHoliday: boolean;
313
335
  /** 月の集合(null はすべての月) */
314
336
  readonly months: ReadonlySet<number> | null;
315
337
  readonly onHolidayRule: OnHolidayRule;
@@ -318,13 +340,16 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
318
340
  }
319
341
  /** @internal 繰り返しの規則を検証して取り込む */
320
342
  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>[];
343
+ /** @internal 祝日かを返す関数(祝日の翌日の規則の判定に使う。提供元の対応年の外は祝日でないとみなすこと) */
344
+ export type IsHoliday = (date: DateString) => boolean;
321
345
  /**
322
- * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・月)がその日に当てはまるか。
323
- * 有効期間と祝日かどうかは考えない(規則の診断で、形どうしの包含を調べるために分けている)
346
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日の翌日・月)がその日に当てはまるか。
347
+ * 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
348
+ * 祝日の翌日の規則は isHoliday で判定し、isHoliday を渡さなければ当てはまらない
324
349
  */
325
- export declare function matchPattern(rule: RecurringRuleCompiled<unknown>, date: DateString): boolean;
326
- /** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む。祝日かどうかは考えない) */
327
- export declare function matchRecurring(rule: RecurringRuleCompiled<unknown>, date: DateString): boolean;
350
+ export declare function matchPattern(rule: RecurringRuleCompiled<unknown>, date: DateString, isHoliday?: IsHoliday): boolean;
351
+ /** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む) */
352
+ export declare function matchRecurring(rule: RecurringRuleCompiled<unknown>, date: DateString, isHoliday: IsHoliday): boolean;
328
353
  /** @internal 祝日の提供元とタイムゾーンの設定を検証した結果 */
329
354
  export interface HolidaySource<Kind extends string> {
330
355
  readonly providers: readonly HolidayProvider<Kind>[];
@@ -11,7 +11,8 @@
11
11
  *
12
12
  * "under-holiday" の規則は、祝日の扱いが "weekday"(曜日どおり)なら祝日にも当てはまる。
13
13
  * "after-holiday" の規則は、祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)。
14
- * 毎年の月日・繰り返しの規則の有効期間(effectiveFrom・effectiveTo)の外では、その規則は書かれていないのと同じ
14
+ * 毎年の月日・繰り返しの規則の有効期間(effectiveFrom・effectiveTo)の外では、その規則は書かれていないのと同じ。
15
+ * 祝日の翌日の規則(dayAfterHoliday)は、前日が祝日でその日自身は祝日でない日に当てはまる(提供元の祝日だけで判定)
15
16
  *
16
17
  * 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
17
18
  */
@@ -208,8 +209,9 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
208
209
  const name = checkName(c, p, entry["name"]);
209
210
  const hasWeekday = "weekday" in entry;
210
211
  const hasDay = "day" in entry;
211
- if (hasWeekday === hasDay) {
212
- c.error("invalid-option", p, 'must have either "weekday" or "day" (not both)');
212
+ const hasAfter = "dayAfterHoliday" in entry;
213
+ if (Number(hasWeekday) + Number(hasDay) + Number(hasAfter) !== 1) {
214
+ c.error("invalid-option", p, 'must have exactly one of "weekday", "day" or "dayAfterHoliday"');
213
215
  return;
214
216
  }
215
217
  let ok = true;
@@ -268,11 +270,23 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
268
270
  ok = false;
269
271
  }
270
272
  }
271
- const set = checkSet(c, `${p}.day`, entry["day"], isDayOfMonth, "1-31 or -1 (last day)");
272
- if (set)
273
- days = set;
274
- else
275
- ok = false;
273
+ if (hasDay) {
274
+ const set = checkSet(c, `${p}.day`, entry["day"], isDayOfMonth, "1-31 or -1 (last day)");
275
+ if (set)
276
+ days = set;
277
+ else
278
+ ok = false;
279
+ }
280
+ else {
281
+ if (entry["dayAfterHoliday"] !== true) {
282
+ c.error("invalid-option", `${p}.dayAfterHoliday`, "must be true");
283
+ ok = false;
284
+ }
285
+ if ("onHolidayRule" in entry) {
286
+ c.error("invalid-option", `${p}.onHolidayRule`, 'cannot be used with "dayAfterHoliday" (the day after a holiday is never a holiday)');
287
+ ok = false;
288
+ }
289
+ }
276
290
  }
277
291
  let months = null;
278
292
  if (entry["months"] !== undefined) {
@@ -302,6 +316,7 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
302
316
  interval,
303
317
  anchor,
304
318
  days,
319
+ dayAfterHoliday: hasAfter,
305
320
  months,
306
321
  onHolidayRule: onHoliday,
307
322
  value,
@@ -313,13 +328,17 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
313
328
  return rules;
314
329
  }
315
330
  /**
316
- * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・月)がその日に当てはまるか。
317
- * 有効期間と祝日かどうかは考えない(規則の診断で、形どうしの包含を調べるために分けている)
331
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日の翌日・月)がその日に当てはまるか。
332
+ * 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
333
+ * 祝日の翌日の規則は isHoliday で判定し、isHoliday を渡さなければ当てはまらない
318
334
  */
319
- export function matchPattern(rule, date) {
335
+ export function matchPattern(rule, date, isHoliday) {
320
336
  const { year, month, day } = parseDate(date);
321
337
  if (rule.months && !rule.months.has(month))
322
338
  return false;
339
+ if (rule.dayAfterHoliday) {
340
+ return isHoliday !== undefined && !isHoliday(date) && isHoliday(addDays(date, -1));
341
+ }
323
342
  const dim = daysInMonth(year, month);
324
343
  if (rule.weekday !== null) {
325
344
  if (weekdayOf(date) !== rule.weekday)
@@ -332,9 +351,9 @@ export function matchPattern(rule, date) {
332
351
  }
333
352
  return rule.days.has(day) || (rule.days.has(-1) && day === dim);
334
353
  }
335
- /** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む。祝日かどうかは考えない) */
336
- export function matchRecurring(rule, date) {
337
- return inPeriod(rule, toDayNumber(date)) && matchPattern(rule, date);
354
+ /** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む) */
355
+ export function matchRecurring(rule, date, isHoliday) {
356
+ return inPeriod(rule, toDayNumber(date)) && matchPattern(rule, date, isHoliday);
338
357
  }
339
358
  /** 規則の value を取り出す(value が必須の規則用) */
340
359
  function requiredValue(c) {
@@ -524,6 +543,7 @@ export function createHolidayLookup(source) {
524
543
  return {
525
544
  timeZone,
526
545
  providers,
546
+ outOfRange,
527
547
  holidayOn,
528
548
  dateOf,
529
549
  civilOf: ((input) => toDateTimeParts(input, timeZone)),
@@ -565,6 +585,7 @@ export const RECURRING_RULE_KEYS = [
565
585
  "interval",
566
586
  "anchor",
567
587
  "day",
588
+ "dayAfterHoliday",
568
589
  "months",
569
590
  "name",
570
591
  "onHolidayRule",
@@ -645,7 +666,7 @@ export function createResolver(compiled, lookup) {
645
666
  const prev = addDays(date, -k);
646
667
  if (!isHolidaySafe(prev))
647
668
  return undefined;
648
- if (matchRecurring(rule, prev))
669
+ if (matchRecurring(rule, prev, isHolidaySafe))
649
670
  return prev;
650
671
  }
651
672
  return undefined;
@@ -678,7 +699,7 @@ export function createResolver(compiled, lookup) {
678
699
  return make(byAnnual.value, "annual", byAnnual, byAnnual.name);
679
700
  if (holiday) {
680
701
  // 3. 祝日の上("over-holiday")
681
- const over = recurring.find((r) => r.onHolidayRule === "over-holiday" && matchRecurring(r, date));
702
+ const over = recurring.find((r) => r.onHolidayRule === "over-holiday" && matchRecurring(r, date, isHolidaySafe));
682
703
  if (over)
683
704
  return make(over.value, "recurring", over, over.name);
684
705
  // 4. 祝日
@@ -688,14 +709,14 @@ export function createResolver(compiled, lookup) {
688
709
  }
689
710
  // 祝日の扱いが「曜日どおり」なら、祝日の下("under-holiday")の規則も当てはまる。
690
711
  // "after-holiday" の規則は祝日には当てはめない(振り替える)
691
- const under = recurring.find((r) => r.onHolidayRule === "under-holiday" && matchRecurring(r, date));
712
+ const under = recurring.find((r) => r.onHolidayRule === "under-holiday" && matchRecurring(r, date, isHolidaySafe));
692
713
  if (under)
693
714
  return make(under.value, "recurring", under, under.name);
694
715
  return make(weekly[weekday], "weekday", null, undefined);
695
716
  }
696
717
  // 5. 繰り返しの規則(祝日でない日。配列で先に書いたものが優先)。振替先も同じ段階で判定する
697
718
  for (const rule of recurring) {
698
- if (matchRecurring(rule, date))
719
+ if (matchRecurring(rule, date, isHolidaySafe))
699
720
  return make(rule.value, "recurring", rule, rule.name);
700
721
  if (hasShift && rule.onHolidayRule === "after-holiday") {
701
722
  const origin = shiftedOrigin(rule, date);
@@ -725,6 +746,7 @@ export function buildSchedule(compiled) {
725
746
  const schedule = {
726
747
  timeZone: lookup.timeZone,
727
748
  providers: lookup.providers,
749
+ outOfRange: lookup.outOfRange,
728
750
  dateOf: lookup.dateOf,
729
751
  civilOf: lookup.civilOf,
730
752
  today: lookup.today,
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@
7
7
  * 公開する API には TSDoc の @public(安定)または @beta(マイナー版で変わりうる)を付ける。
8
8
  * 公開する名前と型は etc/api.md に記録し、pnpm check で意図しない変化を検出する。
9
9
  */
10
- export { createCalendar, type AdjustOptions, type AnnualDate, type AnnualRange, type BusinessDayConvention, type BusinessDayOptions, type Calendar, type CalendarOptions, 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 DayOfMonth, type DayOfMonthPattern, type EffectivePeriod, type HolidayLookup, 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";
10
+ export { combineCalendars, createCalendar, 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 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, monthOf, type MonthString } from "./core/month.ts";
14
14
  export { isBizdaysError, type ErrorCode, type KnownErrorCode } from "./core/errors.ts";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * 公開する API には TSDoc の @public(安定)または @beta(マイナー版で変わりうる)を付ける。
8
8
  * 公開する名前と型は etc/api.md に記録し、pnpm check で意図しない変化を検出する。
9
9
  */
10
- export { createCalendar, } from "./core/calendar.js";
10
+ export { combineCalendars, createCalendar, } from "./core/calendar.js";
11
11
  export { createSchedule, } from "./core/schedule.js";
12
12
  export { addDays, diffDays, isValidDate, weekdayOf, } from "./core/date.js";
13
13
  export { closingMonth, monthOf } from "./core/month.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bizdays-toolkit",
3
- "version": "0.10.0",
3
+ "version": "0.11.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",