bizdays-toolkit 0.9.0 → 0.10.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,46 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.10.0] - 2026-10-19
8
+
9
+ ### 移行
10
+
11
+ 動作は変わりません(繰り返しの規則・毎年の月日の規則への省略可能な項目の追加と、診断の改善)。`bizdays-toolkit/jp` の `toJapanDate`・`toJapanDateTime` が非推奨になったので、使っていれば `toDateString(input, "Asia/Tokyo")`・`toDateTimeParts(input, "Asia/Tokyo")`(または営業日カレンダー・スケジュールの `dateOf`・`civilOf`)に書き換える。書き換え方は [docs/migration.md](./docs/migration.md)。
12
+
13
+ ### Added
14
+
15
+ - 繰り返しの規則(`closedRecurring`・`openRecurring`・スケジュールの `recurring`)と毎年の月日の規則(`closedAnnual` のオブジェクトの形・スケジュールの `annual`)に、有効期間 `effectiveFrom`・`effectiveTo`(両端を含む `"YYYY-MM-DD"`。一方だけなら、もう一方は期限なし)を付けられるようにした。期間の外では、その規則は書かれていないのと同じに扱う(判定の順番は変わらない)。「2027 年 4 月から定休日を月曜から火曜に変える」「夏季休業を今年限りでやめる」を 1 つのカレンダーで表せ、切り替え日をまたぐ営業日の計算(`addBusinessDays` など)も正しくなる。`"after-holiday"` の振替は振替元の日で期間を判定する。型 `EffectivePeriod` を追加した。`effectiveFrom` が `effectiveTo` より後なら `invalid-range`
16
+ - 曜日の規則に周期 `interval`(週数。2 以上の整数)と起点 `anchor`(`weekday` と同じ曜日の日付)を付けて、「隔週水曜定休」のような規則を書けるようにした。`anchor` と同じ曜日で、`anchor` との日数の差が 7 × `interval` の倍数の日(起点より前の日も)に当てはまる。`nth` とは併用できず、`months`・`onHolidayRule`・有効期間とは組み合わせられる。「第 1・第 3 水曜」(`nth: [1, 3]`)は水曜が 5 回ある月で隔週からずれるため、別の書き方にした。`interval` と `anchor` の片方だけ、`anchor` の曜日違い、`day` の規則への指定は `invalid-option`
17
+
18
+ ### Deprecated
19
+
20
+ - `bizdays-toolkit/jp` の `toJapanDate`・`toJapanDateTime` を非推奨にした(`@deprecated`)。同じ変換がルートの `toDateString(input, "Asia/Tokyo")`・`toDateTimeParts(input, "Asia/Tokyo")` と、営業日カレンダー・スケジュールの `dateOf`・`civilOf` にあり、国ごとの関数は日本だけの特別扱い(英国には対応物がない)だったため。少なくとも 1 つのマイナー版は残し、1.0.0 で削除する。`etc/api.md` に `@deprecated` を記録するようにした
21
+
22
+ ### Changed
23
+
24
+ - 規則の診断(`bizdays-toolkit/diagnose`)を、有効期間と周期に対応させた。`shadowed` は先の規則の期間が後の規則の期間を含み、周期も含む(毎週は隔週を覆う。位相の違う隔週どうしは覆わない)ときだけ出す。有効期間の中に当てはまる日がない規則は `never-matches`。規則の有効期間が祝日の提供元の対応年の外にあれば `out-of-range`(新しい code は足していない)
25
+ - `redundant`・`no-business-day` の走査の範囲を見直した。周期のある規則があれば、提供元がない設定の 2001〜2028 年を周期がそろう長さ(隔週なら 56 年。上限 200 年)に広げ、規則の有効期間が範囲の外にあれば、その期間も調べる(`outOfRange: "throw"` では対応年の中だけ)。これまでは固定の範囲の外にある期間の規則を「使われない」と誤診した。調べた年は `message` に書く
26
+ - README: 繰り返しの規則の節に、有効期間と隔週の書き方・例を足した。`businessDaysBetween` だけが `to` を含まない理由(区間を足し合わせられ、`addBusinessDays` の逆算になる)を書いた
27
+ - README のサイズの値を測り直した(ルート約 7.9KB・日本の祝日込み約 9.8KB・英国の祝日込み約 9.6KB。0.10.0 で測定)
28
+ - `docs/decisions.md`: 有効期間と周期の設計(30 番)と `businessDaysBetween` の範囲(31 番)を足し、6 番(契約の項目に `names` を追記)・13 番と 16 番(その月にない日の扱いが `dueDate`・`closingMonth` と繰り返しの規則で違うことの相互参照)・14 番(土曜日を避ける方法は個別の日付での上書き、振替と有効期間)・15 番(走査の範囲)の記述を直した
29
+
30
+ ### 開発
31
+
32
+ - サイズの上限(`size-limits.json`)を上げた。ルート 16,500 → 17,500、`./jp` 19,500 → 20,500、`./diagnose` 22,000 → 24,000(gzip)。有効期間と周期の検証・判定(ルートで約 0.8KB)と、診断の走査範囲・包含判定の見直し(約 2.3KB)のため
33
+ - 性質のテスト(`test/diagnose-properties.test.ts`)で、有効期間と周期のある規則も作るようにした。固定の走査範囲(2001〜2028 年)の外や、28 年の周期をまたぐ年の日付も比べる
34
+
35
+ ## [0.9.1] - 2026-10-18
36
+
37
+ ライブラリのコードは 0.9.0 と同じ。
38
+
39
+ ### Changed
40
+
41
+ - README: スケジュールの `daysBetween` を `holiday` だけで絞ると、`dates`・`annual` で値を決めた祝日(特別営業日など)も含まれることと、それを除く絞り込み(`reason` が `"date"`・`"annual"` の日を除く)の例を足した
42
+
43
+ ### 開発
44
+
45
+ - ブラウザーでの確認の Edge の起動オプションに、`--disable-dev-shm-usage` と `--disable-gpu` を足した。CI のランナーで Edge のページの読み込みが止まり、やり直し(`RETRY`)がほぼ毎回起きていたため
46
+
7
47
  ## [0.9.0] - 2026-10-18
8
48
 
9
49
  ### 移行
@@ -226,7 +266,9 @@
226
266
 
227
267
  - 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
228
268
 
229
- [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.9.0...HEAD
269
+ [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.10.0...HEAD
270
+ [0.10.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.9.1...v0.10.0
271
+ [0.9.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.9.0...v0.9.1
230
272
  [0.9.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.8.1...v0.9.0
231
273
  [0.8.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.8.0...v0.8.1
232
274
  [0.8.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.7.0...v0.8.0
package/README.md CHANGED
@@ -20,13 +20,13 @@ 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", "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)
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
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
28
  - **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.5 KB min+gzip (about 9.0 KB with Japanese holidays, about 9.1 KB with UK bank holidays; measured for 0.9.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
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
30
  - **Extensible to other countries and regions**: holiday providers are pluggable through the `HolidayProvider` interface. Japan and the UK are bundled
31
31
 
32
32
  ## Installation
@@ -117,7 +117,7 @@ shipping.dayInfo("2027-01-01");
117
117
  shipping.daysBetween("2026-12-30", "2027-01-04").map((d) => d.businessDay); // [true, false, false, false, false, true]
118
118
  ```
119
119
 
120
- `daysBetween(from, to)` returns `dayInfo` for every day in the period (both ends included).
120
+ `daysBetween(from, to)` returns `dayInfo` for every day in the period (both ends included). `listBusinessDays` and `holidaysBetween` include both ends too; only `businessDaysBetween(from, to)` counts from `from` (inclusive) to `to` (exclusive), so that `businessDaysBetween(a, b) + businessDaysBetween(b, c)` equals `businessDaysBetween(a, c)` and it is the inverse of `addBusinessDays`.
121
121
 
122
122
  `createCalendar` and `createSchedule` validate the options when they are created, so the usual pattern is to create them once and reuse them. Creating is also cheap (about 0.02 ms for a calendar and 0.04 ms for a schedule with Japanese holidays on a development machine), so creating one per request is not a problem in practice.
123
123
 
@@ -134,9 +134,9 @@ Each day is decided in this order of priority (top first).
134
134
 
135
135
  This order is fixed. Only the relation between a recurring rule and holidays can be chosen per rule with `onHolidayRule` (next section).
136
136
 
137
- ### Recurring rules (weekly, nth weekday, day of month)
137
+ ### Recurring rules (weekly, every other week, nth weekday, day of month)
138
138
 
139
- Recurring closed and open days such as "closed on the 3rd Wednesday", "closed on the 2nd and 4th Saturdays", "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`.
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
140
 
141
141
  ```ts
142
142
  const museum = createCalendar({
@@ -160,17 +160,43 @@ museum.dayInfo("2026-10-13");
160
160
  // name: "Closed on Mondays", shiftedFrom: "2026-10-12" }
161
161
  ```
162
162
 
163
- | Pattern | Meaning |
164
- | ------------------------ | ------------------------------------------------------------------------------------- |
165
- | `{ weekday: 1 }` | Every Monday |
166
- | `{ weekday: 3, nth: 3 }` | The 3rd Wednesday. `nth` is 1–5 or -1 (last); an array such as `[2, 4]` gives several |
167
- | `{ day: 20 }` | The 20th of every month. `day` is 1–31 or -1 (last day); an array gives several |
168
- | `months: [8, 12]` | Narrows any pattern to these months |
163
+ | Pattern | Meaning |
164
+ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
165
+ | `{ weekday: 1 }` | Every Monday |
166
+ | `{ weekday: 3, nth: 3 }` | The 3rd Wednesday. `nth` is 1–5 or -1 (last); an array such as `[2, 4]` gives several |
167
+ | `{ 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
+ | `{ day: 20 }` | The 20th of every month. `day` is 1–31 or -1 (last day); an array gives several |
169
+ | `months: [8, 12]` | Narrows any pattern to these months |
170
+ | `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) |
169
171
 
170
172
  - **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
171
173
  - **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
+ - **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`
172
175
  - Among recurring rules, the one written first in the array wins, and `openRecurring` wins over `closedRecurring`
173
176
 
177
+ **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
+
179
+ ```ts
180
+ const shop = createCalendar({
181
+ holidays: jp,
182
+ closedRecurring: [
183
+ { weekday: 1, name: "Closed on Mondays", effectiveTo: "2027-03-31" },
184
+ { weekday: 2, name: "Closed on Tuesdays", effectiveFrom: "2027-04-01" },
185
+ { weekday: 3, interval: 2, anchor: "2026-01-07", name: "Closed every other Wednesday" },
186
+ ],
187
+ closedAnnual: [{ from: "08-13", to: "08-16", name: "Summer closure", effectiveTo: "2026-12-31" }],
188
+ });
189
+
190
+ shop.isBusinessDay("2027-03-29"); // false (Monday, before the change)
191
+ shop.isBusinessDay("2027-04-06"); // false (Tuesday, after the change)
192
+ shop.addBusinessDays("2027-03-30", 3); // "2027-04-03" (counts across the change; March 31 is an every-other-week Wednesday)
193
+ shop.isBusinessDay("2027-08-13"); // true (the summer closure ended in 2026)
194
+ ```
195
+
196
+ - Outside its period a rule is simply not applied; the order of priority does not change. A rule with only `effectiveFrom` or only `effectiveTo` is open-ended on the other side
197
+ - An `"after-holiday"` rule is judged by the date it would originally apply to: if that date is within the period, the rule is moved to the next non-holiday even when that day is outside the period
198
+ - Changing the closed weekday is written as two weekly rules with periods, as above, rather than with `closedWeekdays` (which has no period)
199
+
174
200
  **What happens on a holiday** (`onHolidayRule`) is chosen per rule.
175
201
 
176
202
  | Value | Meaning | How a store would announce it |
@@ -318,9 +344,15 @@ storeHours
318
344
  .daysBetween("2026-11-01", "2026-11-30")
319
345
  .filter((d) => d.holiday)
320
346
  .map((d) => d.date); // ["2026-11-03", "2026-11-23"]
347
+
348
+ // Holidays not overridden by specific or annual dates (New Year's Day is closed by the annual rule)
349
+ storeHours
350
+ .daysBetween("2026-12-28", "2027-01-11")
351
+ .filter((d) => d.holiday && d.reason !== "date" && d.reason !== "annual")
352
+ .map((d) => d.date); // ["2027-01-11"]
321
353
  ```
322
354
 
323
- `reason` is the rule that decided the value. With `holiday: "weekday"` (holidays follow the weekday rule), a holiday has `reason: "weekday"`, so check `holiday` to know whether a day is a holiday.
355
+ `reason` is the rule that decided the value. With `holiday: "weekday"` (holidays follow the weekday rule), a holiday has `reason: "weekday"`, so check `holiday` to know whether a day is a holiday. Filtering by `holiday` alone also keeps holidays whose value is set by `dates` or `annual` (such as special hours on New Year's Day). To list only holidays that follow the holiday handling, also exclude `reason` `"date"` and `"annual"`, as in the last example: `holiday` tells whether the day is a holiday, and `reason` tells which rule decided the value.
324
356
 
325
357
  Values are decided in the order `dates` (specific dates and ranges) → `annual` (annual dates and ranges) → `recurring` (`"over-holiday"`) → holidays → `recurring` (default and moved days) → `weekly` (weekday). When rules of the same type overlap, the one written first in the array wins.
326
358
 
@@ -332,7 +364,7 @@ const hours = storeHours.valueOn(date);
332
364
  const isOpen = hours !== null && hours.open <= time && time < hours.close;
333
365
  ```
334
366
 
335
- For inputs that always carry a time (a `Date`, Temporal `Instant`, `ZonedDateTime` or `PlainDateTime`, or a `"YYYY-MM-DDTHH:mm"` string), `time` is typed as `string`, so no `null` check is needed. `civilOf` is also on business-day calendars, and the same conversion is available as `toJapanDateTime` in `bizdays-toolkit/jp` and `toDateTimeParts(input, timeZone)` in the root.
367
+ For inputs that always carry a time (a `Date`, Temporal `Instant`, `ZonedDateTime` or `PlainDateTime`, or a `"YYYY-MM-DDTHH:mm"` string), `time` is typed as `string`, so no `null` check is needed. `civilOf` is also on business-day calendars, and the same conversion is available as `toDateTimeParts(input, timeZone)` in the root. (`toJapanDate` and `toJapanDateTime` in `bizdays-toolkit/jp` are deprecated since 0.10.0: use `toDateString(input, "Asia/Tokyo")`, `toDateTimeParts(input, "Asia/Tokyo")` or `dateOf`/`civilOf` on a calendar instead. They will be removed in 1.0.0.)
336
368
 
337
369
  ### Date and date-time inputs
338
370
 
@@ -448,8 +480,8 @@ diagnoseCalendar({
448
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 |
449
481
  | `warning` | `unknown-option` | Unknown keys (ignored at creation). If a close known key exists, `message` suggests it |
450
482
  | `warning` | `shadowed` | A rule fully covered by an earlier rule of the same type (`relatedPath`) and never used |
451
- | `warning` | `never-matches` | A recurring rule that matches no day (such as `{ day: 30, months: [2] }`) |
452
- | `warning` | `out-of-range` | A specific date outside the supported years of the holiday provider (`relatedPath`) |
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`) |
453
485
  | `warning` | `no-business-day` | No business day at all (calendars only; searching for a business day throws) |
454
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) |
455
487
 
@@ -457,7 +489,8 @@ diagnoseCalendar({
457
489
  - When there is an `error`, the rule relation checks (`shadowed`, `redundant` and so on) are skipped
458
490
  - `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`
459
491
  - `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)
460
- - `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). Schedule values are compared by content for arrays and plain objects, and by identity otherwise (such as `Date`)
492
+ - `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
+ - `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`)
461
494
  - 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
462
495
 
463
496
  Keeping options safe with a test:
@@ -20,7 +20,7 @@ import { type DateString, type Weekday } from "./date.ts";
20
20
  import { IssueCollector } from "./errors.ts";
21
21
  import { type DateInput } from "./input.ts";
22
22
  import { type MonthString } from "./month.ts";
23
- import { type CompiledSchedule, type DayOfMonth, type RecurringRule, type HolidayLookup, type HolidaySourceOptions } from "./schedule.ts";
23
+ import { type CompiledSchedule, type DayOfMonth, type EffectivePeriod, type RecurringRule, type HolidayLookup, type HolidaySourceOptions } from "./schedule.ts";
24
24
  import type { Holiday } from "./types.ts";
25
25
  /**
26
26
  * 祝日の扱い。
@@ -28,18 +28,18 @@ import type { Holiday } from "./types.ts";
28
28
  */
29
29
  export type HolidayPolicy = "closed" | "open" | "ignore";
30
30
  /**
31
- * 毎年の月日 "MM-DD"(名前付きも可)。
31
+ * 毎年の月日 "MM-DD"(名前付き・有効期間付きも可)。
32
32
  * @public
33
33
  */
34
- export type AnnualDate = string | {
34
+ export type AnnualDate = string | ({
35
35
  readonly date: string;
36
36
  readonly name?: string;
37
- };
37
+ } & EffectivePeriod);
38
38
  /**
39
39
  * 毎年の月日の期間(from > to なら年をまたぐ。例: 12-31 〜 01-03)。
40
40
  * @public
41
41
  */
42
- export interface AnnualRange {
42
+ export interface AnnualRange extends EffectivePeriod {
43
43
  readonly from: string;
44
44
  readonly to: string;
45
45
  readonly name?: string;
@@ -68,15 +68,16 @@ export interface DateRange {
68
68
  export interface CalendarOptions<Kind extends string = string> extends HolidaySourceOptions<Kind> {
69
69
  /** 定休日の曜日(0 = 日曜日) */
70
70
  readonly closedWeekdays?: readonly Weekday[];
71
- /** 毎年の休業日 */
71
+ /** 毎年の休業日(有効期間 effectiveFrom・effectiveTo を付けられる) */
72
72
  readonly closedAnnual?: readonly (AnnualDate | AnnualRange)[];
73
73
  /** 臨時休業日 */
74
74
  readonly closedDates?: readonly (SpecificDate | DateRange)[];
75
75
  /** 臨時営業日(ほかのすべての規則より優先) */
76
76
  readonly openDates?: readonly (SpecificDate | DateRange)[];
77
77
  /**
78
- * 繰り返しの休業日(毎週・毎月の第 n 曜日・毎月の日)。
79
- * 祝日に当たったときの扱いは規則ごとに onHolidayRule で指定する(既定: "under-holiday")
78
+ * 繰り返しの休業日(毎週・隔週などの周期・毎月の第 n 曜日・毎月の日)。
79
+ * 祝日に当たったときの扱いは規則ごとに onHolidayRule で指定する(既定: "under-holiday")。
80
+ * 有効期間(effectiveFrom・effectiveTo)を付けると、期間の外では書かれていないのと同じに扱う
80
81
  */
81
82
  readonly closedRecurring?: readonly RecurringRule[];
82
83
  /** 繰り返しの営業日。closedRecurring と同じ日に当たる場合はこちらが優先 */
@@ -8,14 +8,16 @@
8
8
  * - warning … 作れるが、意図と違う可能性が高いもの
9
9
  * unknown-option … 知らないキー(作成時は無視される)。近い既知のキーがあれば message に候補を挙げる
10
10
  * shadowed … 同じ種類の先の規則に完全に覆われ、決して使われない(relatedPath に先の規則)
11
- * never-matches … 当てはまる日がない繰り返しの規則(2 月 30 日など)
12
- * out-of-range … 個別の日付が祝日の提供元の対応年の外にある(relatedPath に提供元)
11
+ * never-matches … 当てはまる日がない繰り返しの規則(2 月 30 日、有効期間の中に第 5 水曜日がない、など)
12
+ * out-of-range … 個別の日付、または規則の有効期間が祝日の提供元の対応年の外にある(relatedPath に提供元)
13
13
  * no-business-day … 営業日が 1 日もない設定(営業日カレンダーのみ)
14
14
  * - info … redundant: 取り除いても結果が変わらない規則
15
15
  *
16
16
  * 判定の順番(個別の日付 → 毎年の月日 → 繰り返し(over)→ 祝日 → 繰り返し(under・振替先)→ 曜日)は
17
17
  * 固定なので、shadowed は同じ種類の規則どうしに限って調べる。種類をまたいで使われない規則は
18
18
  * redundant(取り除いても結果が変わらない)として見つかる。
19
+ * 有効期間(effectiveFrom・effectiveTo)のある規則は期間の包含も、周期(interval)のある規則は周期の包含も
20
+ * shadowed の条件にする。redundant の走査の範囲は、周期がそろう長さと、範囲の外にある有効期間まで広げる
19
21
  */
20
22
  import { type CalendarOptions } from "./calendar.ts";
21
23
  import { type KnownErrorCode } from "./errors.ts";
@@ -8,19 +8,21 @@
8
8
  * - warning … 作れるが、意図と違う可能性が高いもの
9
9
  * unknown-option … 知らないキー(作成時は無視される)。近い既知のキーがあれば message に候補を挙げる
10
10
  * shadowed … 同じ種類の先の規則に完全に覆われ、決して使われない(relatedPath に先の規則)
11
- * never-matches … 当てはまる日がない繰り返しの規則(2 月 30 日など)
12
- * out-of-range … 個別の日付が祝日の提供元の対応年の外にある(relatedPath に提供元)
11
+ * never-matches … 当てはまる日がない繰り返しの規則(2 月 30 日、有効期間の中に第 5 水曜日がない、など)
12
+ * out-of-range … 個別の日付、または規則の有効期間が祝日の提供元の対応年の外にある(relatedPath に提供元)
13
13
  * no-business-day … 営業日が 1 日もない設定(営業日カレンダーのみ)
14
14
  * - info … redundant: 取り除いても結果が変わらない規則
15
15
  *
16
16
  * 判定の順番(個別の日付 → 毎年の月日 → 繰り返し(over)→ 祝日 → 繰り返し(under・振替先)→ 曜日)は
17
17
  * 固定なので、shadowed は同じ種類の規則どうしに限って調べる。種類をまたいで使われない規則は
18
18
  * redundant(取り除いても結果が変わらない)として見つかる。
19
+ * 有効期間(effectiveFrom・effectiveTo)のある規則は期間の包含も、周期(interval)のある規則は周期の包含も
20
+ * shadowed の条件にする。redundant の走査の範囲は、周期がそろう長さと、範囲の外にある有効期間まで広げる
19
21
  */
20
22
  import { daysInMonth, fromDayNumber, parseDate, toDayNumber } from "./date.js";
21
23
  import { compileCalendarOptions } from "./calendar.js";
22
24
  import { IssueCollector } from "./errors.js";
23
- import { compileScheduleOptions, createHolidayLookup, createResolver, matchAnnual, matchRecurring, } from "./schedule.js";
25
+ import { compileScheduleOptions, createHolidayLookup, createResolver, matchMonthDay, matchPattern, } from "./schedule.js";
24
26
  // ------------------------------------------------------------
25
27
  // 知らないキーの候補
26
28
  // ------------------------------------------------------------
@@ -68,13 +70,29 @@ function closestKey(key, known) {
68
70
  // ------------------------------------------------------------
69
71
  // 調べる期間
70
72
  // ------------------------------------------------------------
71
- /** 繰り返しの規則の当てはまり方は、年の種類(1 月 1 日の曜日 × うるう年か)の 14 通りで決まる。28 年でそろう */
73
+ /**
74
+ * 繰り返しの規則の当てはまり方は、年の種類(1 月 1 日の曜日 × うるう年か)の 14 通りで決まり、28 年でそろう。
75
+ * 周期(interval)のある規則は、28 年(10,227 日)が 7 × interval 日の倍数とは限らないので、別に周期を求める(patternCycle)
76
+ */
72
77
  const PATTERN_FROM = toDayNumber("2001-01-01");
73
- const PATTERN_TO = toDayNumber("2028-12-31");
74
- /** 祝日の提供元があるときに調べる年の上限(長すぎる対応年は 1900〜2099 年に絞る) */
78
+ const PATTERN_DAYS = toDayNumber("2028-12-31") - PATTERN_FROM + 1;
79
+ /** 祝日の提供元があるときに調べる年の上限(長すぎる対応年は 1900〜2099 年に絞る)。周期・有効期間で広げる長さの上限でもある */
75
80
  const MAX_WINDOW_YEARS = 200;
76
81
  /** 個別の日付の期間で、取り除いた結果と比べる最大日数 */
77
82
  const MAX_DATE_RULE_DAYS = 366 * MAX_WINDOW_YEARS;
83
+ const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
84
+ const lcm = (a, b) => (a / gcd(a, b)) * b;
85
+ /**
86
+ * 繰り返しの規則の当てはまり方がそろう日数(最大 MAX_WINDOW_YEARS 年)。
87
+ * 周期 k 週の規則は、28 年と 7k 日の最小公倍数でそろう(隔週なら 56 年)
88
+ */
89
+ function patternCycle(rules) {
90
+ let weeks = 1;
91
+ for (const rule of rules)
92
+ if (rule.interval !== null)
93
+ weeks = lcm(weeks, rule.interval);
94
+ return Math.min(lcm(PATTERN_DAYS, 7 * weeks), 366 * MAX_WINDOW_YEARS);
95
+ }
78
96
  /** 長すぎる年の範囲を 1900〜2099 年(重ならなければ先頭の 200 年)に絞る */
79
97
  function clipYears(from, to) {
80
98
  if (to - from + 1 <= MAX_WINDOW_YEARS)
@@ -83,43 +101,93 @@ function clipYears(from, to) {
83
101
  const b = Math.min(to, 2099);
84
102
  return a <= b ? [a, b] : [from, from + MAX_WINDOW_YEARS - 1];
85
103
  }
86
- /** 毎年の月日・繰り返しの規則を調べる期間(祝日が年ごとに変わるので、提供元の対応年を使う) */
87
- function scanWindow(source) {
104
+ const dayOfYear = (year, last) => toDayNumber(`${String(year).padStart(4, "0")}-${last ? "12-31" : "01-01"}`);
105
+ /**
106
+ * 毎年の月日・繰り返しの規則を調べる期間。祝日が年ごとに変わるので、提供元の対応年を使う。
107
+ * 提供元がなければ、繰り返しの規則がそろう周期(2001 年から 28 年、周期のある規則があれば長く)。
108
+ * 規則の有効期間が基準の範囲の外にあれば、その期間(期限のない側は周期の長さ)も加える
109
+ * (対応年の外で例外になる設定では加えない)
110
+ */
111
+ function scanWindow(source, rules, cycle) {
112
+ const ranges = [];
113
+ let extend = true;
88
114
  if (source.providers.length === 0) {
89
- return { from: PATTERN_FROM, to: PATTERN_TO, label: "2001-2028" };
90
- }
91
- const ranges = source.providers.map((p) => p.supportedYears);
92
- let from;
93
- let to;
94
- if (source.outOfRange === "ignore") {
95
- from = Math.min(...ranges.map((r) => r.from));
96
- to = Math.max(...ranges.map((r) => r.to));
115
+ ranges.push({ from: PATTERN_FROM, to: PATTERN_FROM + cycle - 1 });
97
116
  }
98
117
  else {
99
- from = Math.max(...ranges.map((r) => r.from));
100
- to = Math.min(...ranges.map((r) => r.to));
118
+ const years = source.providers.map((p) => p.supportedYears);
119
+ let from;
120
+ let to;
121
+ if (source.outOfRange === "ignore") {
122
+ from = Math.min(...years.map((r) => r.from));
123
+ to = Math.max(...years.map((r) => r.to));
124
+ }
125
+ else {
126
+ from = Math.max(...years.map((r) => r.from));
127
+ to = Math.min(...years.map((r) => r.to));
128
+ extend = false;
129
+ }
130
+ if (from > to)
131
+ return undefined;
132
+ [from, to] = clipYears(from, to);
133
+ ranges.push({ from: dayOfYear(from, false), to: dayOfYear(to, true) });
134
+ }
135
+ if (extend) {
136
+ for (const rule of rules) {
137
+ if (rule.effectiveFrom === null && rule.effectiveTo === null)
138
+ continue;
139
+ const from = rule.effectiveFrom ?? rule.effectiveTo - cycle + 1;
140
+ const to = Math.min(rule.effectiveTo ?? from + cycle - 1, from + 366 * MAX_WINDOW_YEARS - 1);
141
+ ranges.push({ from, to });
142
+ }
143
+ }
144
+ ranges.sort((a, b) => a.from - b.from);
145
+ const merged = [];
146
+ for (const r of ranges) {
147
+ const last = merged[merged.length - 1];
148
+ if (last && r.from <= last.to + 1)
149
+ merged[merged.length - 1] = { from: last.from, to: Math.max(last.to, r.to) };
150
+ else
151
+ merged.push(r);
152
+ }
153
+ const yearOf = (n) => parseDate(fromDayNumber(n)).year;
154
+ const label = merged.map((r) => `${yearOf(r.from)}-${yearOf(r.to)}`).join(", ");
155
+ return { ranges: merged, label };
156
+ }
157
+ /** 期間の日ごとに fn を呼ぶ(true を返したら止める) */
158
+ function eachDay(window, fn) {
159
+ for (const { from, to } of window.ranges) {
160
+ for (let n = from; n <= to; n++)
161
+ if (fn(n) === true)
162
+ return;
101
163
  }
102
- if (from > to)
103
- return undefined;
104
- [from, to] = clipYears(from, to);
105
- return {
106
- from: toDayNumber(`${String(from).padStart(4, "0")}-01-01`),
107
- to: toDayNumber(`${String(to).padStart(4, "0")}-12-31`),
108
- label: `${from}-${to}`,
109
- };
110
164
  }
111
165
  // ------------------------------------------------------------
112
166
  // 規則の当てはまる日(覆われているかの判定用)
113
167
  // ------------------------------------------------------------
114
- /** 繰り返しの規則が当てはまる日(代表の 28 年間)。祝日・振替は考えない */
115
- function recurringMatches(rule) {
116
- const bits = new Uint8Array(PATTERN_TO - PATTERN_FROM + 1);
117
- for (let n = PATTERN_FROM; n <= PATTERN_TO; n++) {
118
- if (matchRecurring(rule, fromDayNumber(n)))
119
- bits[n - PATTERN_FROM] = 1;
168
+ /** 繰り返しの規則の形(有効期間・周期の位相は除く)が当てはまる日(代表の 28 年間)。祝日・振替は考えない */
169
+ function patternBits(rule) {
170
+ const bits = new Uint8Array(PATTERN_DAYS);
171
+ // 周期のある規則は、位相を除いた形(毎週)で調べ、周期と位相は intervalCovers で比べる
172
+ const weekly = rule.interval === null ? rule : { ...rule, interval: null, anchor: null };
173
+ for (let i = 0; i < PATTERN_DAYS; i++) {
174
+ if (matchPattern(weekly, fromDayNumber(PATTERN_FROM + i)))
175
+ bits[i] = 1;
120
176
  }
121
177
  return bits;
122
178
  }
179
+ /**
180
+ * 繰り返しの規則が当てはまる日が 1 日でもあるか。
181
+ * 有効期間があればその期間(期限のない側は、当てはまり方がそろう周期の長さ)を、なければ周期の長さを 1 日ずつ調べる
182
+ */
183
+ function matchesAnyDay(rule, cycle) {
184
+ const from = rule.effectiveFrom ?? (rule.effectiveTo === null ? PATTERN_FROM : rule.effectiveTo - cycle + 1);
185
+ const to = Math.min(rule.effectiveTo ?? Infinity, from + cycle - 1);
186
+ for (let n = from; n <= to; n++)
187
+ if (matchPattern(rule, fromDayNumber(n)))
188
+ return true;
189
+ return false;
190
+ }
123
191
  /** 毎年の月日の規則が当てはまる月日(うるう年の 366 日) */
124
192
  const LEAP_MONTH_DAYS = (() => {
125
193
  const list = [];
@@ -130,12 +198,28 @@ const LEAP_MONTH_DAYS = (() => {
130
198
  }
131
199
  return list;
132
200
  })();
133
- function annualMatches(rule) {
134
- return Uint8Array.from(LEAP_MONTH_DAYS, (md) => (matchAnnual(rule, md) ? 1 : 0));
201
+ function annualBits(rule) {
202
+ return Uint8Array.from(LEAP_MONTH_DAYS, (md) => (matchMonthDay(rule, md) ? 1 : 0));
135
203
  }
136
- const isEmpty = (bits) => bits.every((b) => b === 0);
137
204
  /** a が当てはまる日に、b もすべて当てはまるか(a ⊆ b) */
138
205
  const isSubset = (a, b) => a.every((v, i) => v === 0 || b[i] === 1);
206
+ /** 先の規則の有効期間が、後の規則の有効期間を含むか */
207
+ const periodCovers = (earlier, later) => (earlier.effectiveFrom === null ||
208
+ (later.effectiveFrom !== null && earlier.effectiveFrom <= later.effectiveFrom)) &&
209
+ (earlier.effectiveTo === null ||
210
+ (later.effectiveTo !== null && earlier.effectiveTo >= later.effectiveTo));
211
+ /**
212
+ * 周期の点で、先の規則が後の規則を覆えるか(形の包含は patternBits で別に調べる)。
213
+ * 周期のある先の規則は、後の規則の周期がその倍数で、位相も同じ(起点の差が先の周期の倍数)ときだけ覆う
214
+ */
215
+ function intervalCovers(earlier, later) {
216
+ if (earlier.interval === null)
217
+ return true;
218
+ if (later.interval === null)
219
+ return false;
220
+ return (later.interval % earlier.interval === 0 &&
221
+ (later.anchor - earlier.anchor) % (7 * earlier.interval) === 0);
222
+ }
139
223
  /**
140
224
  * 先の繰り返しの規則 earlier が、後の規則 later を完全に覆えるか(祝日の扱いの組み合わせ)。
141
225
  * - "over-holiday" の規則は、祝日でも先に当てはめる "over-holiday" の規則にしか覆われない
@@ -221,7 +305,8 @@ function analyze(c, compiled, isCalendar) {
221
305
  if (!checkProviderData(c, source))
222
306
  return;
223
307
  const lookup = createHolidayLookup(source);
224
- const window = scanWindow(source);
308
+ const cycle = patternCycle(recurring);
309
+ const window = scanWindow(source, [...annual, ...recurring], cycle);
225
310
  /** 使われないことが分かった規則(redundant を重ねて出さない) */
226
311
  const unused = new Set();
227
312
  // --- 個別の日付: 対応年の外・覆われている ---
@@ -244,25 +329,49 @@ function analyze(c, compiled, isCalendar) {
244
329
  c.warning("shadowed", rule.path, `This rule is never used: the earlier rule ${cover.path} covers all of its days`, cover.path);
245
330
  }
246
331
  });
247
- // --- 毎年の月日: 覆われている ---
248
- const annualBits = annual.map(annualMatches);
332
+ // --- 有効期間が提供元の対応年の外にある(対応年の外で例外になる設定では、その規則は決して使われない) ---
333
+ for (const rule of [...annual, ...recurring]) {
334
+ if (rule.effectiveFrom === null && rule.effectiveTo === null)
335
+ continue;
336
+ source.providers.forEach((p, i) => {
337
+ const { from, to } = p.supportedYears;
338
+ const after = rule.effectiveFrom !== null && yearOf(rule.effectiveFrom) > to;
339
+ const before = rule.effectiveTo !== null && yearOf(rule.effectiveTo) < from;
340
+ if (!after && !before)
341
+ return;
342
+ if (source.outOfRange === "throw")
343
+ unused.add(rule);
344
+ c.warning("out-of-range", rule.path, `The effective period is outside the years supported by holiday provider ${p.id} (${from}-${to}); ` +
345
+ (source.outOfRange === "throw"
346
+ ? "looking up these dates throws, so this rule is never used"
347
+ : "holidays are not considered for these dates"), source.providers.length === 1 ? "holidays" : `holidays[${i}]`);
348
+ });
349
+ }
350
+ // --- 毎年の月日: 覆われている(月日と有効期間の両方で) ---
351
+ const annualMatches = annual.map(annualBits);
249
352
  annual.forEach((rule, j) => {
250
- const k = annualBits.findIndex((bits, i) => i < j && isSubset(annualBits[j], bits));
353
+ const k = annualMatches.findIndex((bits, i) => i < j && periodCovers(annual[i], rule) && isSubset(annualMatches[j], bits));
251
354
  if (k < 0)
252
355
  return;
253
356
  unused.add(rule);
254
357
  c.warning("shadowed", rule.path, `This rule is never used: the earlier rule ${annual[k].path} covers all of its days`, annual[k].path);
255
358
  });
256
359
  // --- 繰り返しの規則: 当てはまる日がない・覆われている ---
257
- const recurringBits = recurring.map(recurringMatches);
360
+ const recurringBits = recurring.map(patternBits);
258
361
  recurring.forEach((rule, j) => {
259
362
  const bits = recurringBits[j];
260
- if (isEmpty(bits)) {
363
+ if (!matchesAnyDay(rule, cycle)) {
261
364
  unused.add(rule);
262
- c.warning("never-matches", rule.path, "This rule matches no day (for example, a day that does not exist in the selected months)");
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");
263
368
  return;
264
369
  }
265
- const k = recurring.findIndex((r, i) => i < j && canCover(r, rule) && isSubset(bits, recurringBits[i]));
370
+ const k = recurring.findIndex((r, i) => i < j &&
371
+ canCover(r, rule) &&
372
+ periodCovers(r, rule) &&
373
+ intervalCovers(r, rule) &&
374
+ isSubset(bits, recurringBits[i]));
266
375
  if (k < 0)
267
376
  return;
268
377
  unused.add(rule);
@@ -273,10 +382,7 @@ function analyze(c, compiled, isCalendar) {
273
382
  if (isCalendar && window && !dates.some((r) => r.value === true)) {
274
383
  const resolve = createResolver(without(compiled, null, true), lookup);
275
384
  let found = false;
276
- for (let n = window.from; n <= window.to && !found; n++) {
277
- if (resolve(fromDayNumber(n)).day.value === true)
278
- found = true;
279
- }
385
+ eachDay(window, (n) => (found = resolve(fromDayNumber(n)).day.value === true));
280
386
  if (!found) {
281
387
  c.warning("no-business-day", "", `No day is a business day with these options (checked ${window.label}); ` +
282
388
  "searching for a business day throws");
@@ -322,11 +428,11 @@ function analyze(c, compiled, isCalendar) {
322
428
  const base = without(compiled, null, true);
323
429
  const resolveBase = createResolver(base, lookup);
324
430
  const decided = new Map(candidates.map((r) => [r, []]));
325
- for (let n = window.from; n <= window.to; n++) {
431
+ eachDay(window, (n) => {
326
432
  const rule = resolveBase(fromDayNumber(n)).rule;
327
433
  if (rule)
328
434
  decided.get(rule)?.push(n);
329
- }
435
+ });
330
436
  for (const rule of candidates) {
331
437
  const days = decided.get(rule);
332
438
  if (!redundant(rule, base, days))
@@ -43,7 +43,7 @@ export interface TemporalDateTimeLike extends TemporalDateLike {
43
43
  }
44
44
  /**
45
45
  * 時刻を必ず持つ入力(瞬間、Temporal.PlainDateTime、"YYYY-MM-DDTHH:mm…" の文字列)。
46
- * これを渡すと、civilOf・toDateTimeParts・toJapanDateTime の time の型が string になる。
46
+ * これを渡すと、civilOf・toDateTimeParts の time の型が string になる。
47
47
  * @public
48
48
  */
49
49
  export type DateTimeInput = Date | TemporalInstantLike | TemporalDateTimeLike | `${number}-${number}-${number}T${string}`;
@@ -10,7 +10,8 @@
10
10
  * 6. weekly … 曜日ごとの値
11
11
  *
12
12
  * "under-holiday" の規則は、祝日の扱いが "weekday"(曜日どおり)なら祝日にも当てはまる。
13
- * "after-holiday" の規則は、祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)
13
+ * "after-holiday" の規則は、祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)。
14
+ * 毎年の月日・繰り返しの規則の有効期間(effectiveFrom・effectiveTo)の外では、その規則は書かれていないのと同じ
14
15
  *
15
16
  * 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
16
17
  */
@@ -57,14 +58,32 @@ export type Month = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
57
58
  * @public
58
59
  */
59
60
  export type OnHolidayRule = "under-holiday" | "over-holiday" | "after-holiday";
61
+ /**
62
+ * 規則の有効期間(両端を含む "YYYY-MM-DD")。期間の外では、その規則は書かれていないのと同じに扱う。
63
+ * @public
64
+ */
65
+ export interface EffectivePeriod {
66
+ /** この日から有効(省略時は期限なし) */
67
+ readonly effectiveFrom?: DateString;
68
+ /** この日まで有効(省略時は期限なし) */
69
+ readonly effectiveTo?: DateString;
70
+ }
60
71
  /**
61
72
  * 曜日の繰り返し。nth を省略すると毎週、指定すると月の第 n 回目(例: 第 3 水曜日)。
73
+ * interval と anchor を指定すると、anchor と同じ曜日で、anchor から interval 週おきの日(例: 隔週水曜日)。
62
74
  * @public
63
75
  */
64
76
  export interface WeekdayPattern {
65
77
  readonly weekday: Weekday;
66
- /** 月の第 n 回目(その月で n 回目のその曜日。1〜7 日が第 1)。配列で複数指定できる */
78
+ /** 月の第 n 回目(その月で n 回目のその曜日。1〜7 日が第 1)。配列で複数指定できる。interval とは併用できない */
67
79
  readonly nth?: Nth | readonly Nth[];
80
+ /** 周期(週)。2 以上の整数。anchor と一緒に指定する(例: 2 で隔週) */
81
+ readonly interval?: number;
82
+ /**
83
+ * 周期の起点の日付 "YYYY-MM-DD"(weekday と同じ曜日)。この日の前後に interval 週おきに当てはまる。
84
+ * 起点より前の日にも当てはまるので、起点から始めるには effectiveFrom を併せて指定する
85
+ */
86
+ readonly anchor?: DateString;
68
87
  /** 当てはめる月(省略時はすべての月) */
69
88
  readonly months?: readonly Month[];
70
89
  readonly day?: never;
@@ -80,6 +99,8 @@ export interface DayOfMonthPattern {
80
99
  readonly months?: readonly Month[];
81
100
  readonly weekday?: never;
82
101
  readonly nth?: never;
102
+ readonly interval?: never;
103
+ readonly anchor?: never;
83
104
  }
84
105
  /**
85
106
  * 繰り返しのパターン。
@@ -90,7 +111,7 @@ export type RecurringPattern = WeekdayPattern | DayOfMonthPattern;
90
111
  * 繰り返しの規則(営業日カレンダーの closedRecurring・openRecurring)。
91
112
  * @public
92
113
  */
93
- export type RecurringRule = RecurringPattern & {
114
+ export type RecurringRule = RecurringPattern & EffectivePeriod & {
94
115
  readonly name?: string;
95
116
  /** 祝日に当たったときの扱い(既定: "under-holiday") */
96
117
  readonly onHolidayRule?: OnHolidayRule;
@@ -106,7 +127,7 @@ export type RecurringValue<T> = RecurringRule & {
106
127
  * 毎年の月日 "MM-DD" の値。
107
128
  * @public
108
129
  */
109
- export interface AnnualValue<T> {
130
+ export interface AnnualValue<T> extends EffectivePeriod {
110
131
  readonly date: string;
111
132
  readonly value: T;
112
133
  readonly name?: string;
@@ -115,7 +136,7 @@ export interface AnnualValue<T> {
115
136
  * 毎年の月日の期間の値(from > to なら年をまたぐ。例: 12-31 〜 01-03)。
116
137
  * @public
117
138
  */
118
- export interface AnnualRangeValue<T> {
139
+ export interface AnnualRangeValue<T> extends EffectivePeriod {
119
140
  readonly from: string;
120
141
  readonly to: string;
121
142
  readonly value: T;
@@ -252,8 +273,13 @@ export interface DateRule<T> {
252
273
  readonly value: T;
253
274
  readonly name: string | undefined;
254
275
  }
276
+ /** @internal 有効期間(日数。省略した側は null) */
277
+ export interface CompiledPeriod {
278
+ readonly effectiveFrom: number | null;
279
+ readonly effectiveTo: number | null;
280
+ }
255
281
  /** @internal 毎年の月日の規則 */
256
- export interface AnnualRule<T> {
282
+ export interface AnnualRule<T> extends CompiledPeriod {
257
283
  /** 設定の中の位置(例: "closedAnnual[0]") */
258
284
  readonly path: string;
259
285
  readonly from: string;
@@ -261,6 +287,8 @@ export interface AnnualRule<T> {
261
287
  readonly value: T;
262
288
  readonly name: string | undefined;
263
289
  }
290
+ /** @internal 日数 n が有効期間の中か */
291
+ export declare const inPeriod: (rule: CompiledPeriod, n: number) => boolean;
264
292
  /**
265
293
  * @internal 個別の日付の規則を検証して取り込む。
266
294
  * entry は "YYYY-MM-DD"・{ date }・{ from, to } のいずれか。値は valueOf で決める
@@ -269,13 +297,17 @@ export declare function compileDateRules<T>(c: IssueCollector, path: string, ent
269
297
  /** @internal 毎年の月日の規則を検証して取り込む(entry は "MM-DD"・{ date }・{ from, to }) */
270
298
  export declare function compileAnnualRules<T>(c: IssueCollector, path: string, entries: unknown, valueOf: (entry: Record<string, unknown>, path: string) => T | undefined, allowString: boolean): AnnualRule<T>[];
271
299
  /** @internal 繰り返しの規則(検証済み) */
272
- export interface RecurringRuleCompiled<T> {
300
+ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
273
301
  /** 設定の中の位置(例: "closedRecurring[0]") */
274
302
  readonly path: string;
275
303
  /** 曜日の規則なら曜日、日の規則なら null */
276
304
  readonly weekday: Weekday | null;
277
305
  /** 第 n 回目の集合(null は毎週) */
278
306
  readonly nth: ReadonlySet<number> | null;
307
+ /** 周期(週)。null は毎週・第 n 回目・日の規則 */
308
+ readonly interval: number | null;
309
+ /** 周期の起点(日数)。interval が null なら null */
310
+ readonly anchor: number | null;
279
311
  /** 日の集合(曜日の規則では null) */
280
312
  readonly days: ReadonlySet<number> | null;
281
313
  /** 月の集合(null はすべての月) */
@@ -286,7 +318,12 @@ export interface RecurringRuleCompiled<T> {
286
318
  }
287
319
  /** @internal 繰り返しの規則を検証して取り込む */
288
320
  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>[];
289
- /** @internal 繰り返しの規則がその日に当てはまるか(祝日かどうかは考えない) */
321
+ /**
322
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・月)がその日に当てはまるか。
323
+ * 有効期間と祝日かどうかは考えない(規則の診断で、形どうしの包含を調べるために分けている)
324
+ */
325
+ export declare function matchPattern(rule: RecurringRuleCompiled<unknown>, date: DateString): boolean;
326
+ /** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む。祝日かどうかは考えない) */
290
327
  export declare function matchRecurring(rule: RecurringRuleCompiled<unknown>, date: DateString): boolean;
291
328
  /** @internal 祝日の提供元とタイムゾーンの設定を検証した結果 */
292
329
  export interface HolidaySource<Kind extends string> {
@@ -310,11 +347,15 @@ export interface CompiledSchedule<T, Kind extends string> {
310
347
  readonly recurring: readonly RecurringRuleCompiled<T>[];
311
348
  }
312
349
  /** @internal 繰り返しの規則の項目 */
350
+ /** @internal 有効期間の項目 */
351
+ export declare const PERIOD_KEYS: string[];
313
352
  export declare const RECURRING_RULE_KEYS: string[];
314
353
  /** @internal スケジュールの設定を検証して取り込む(誤りは c に集める) */
315
354
  export declare function compileScheduleOptions<T, Kind extends string>(c: IssueCollector, options: unknown): CompiledSchedule<T, Kind> | undefined;
316
- /** @internal 毎年の月日の規則がその月日 "MM-DD" に当てはまるか */
317
- export declare function matchAnnual(rule: AnnualRule<unknown>, monthDay: string): boolean;
355
+ /** @internal 毎年の月日の規則の形がその月日 "MM-DD" に当てはまるか(有効期間は考えない) */
356
+ export declare function matchMonthDay(rule: AnnualRule<unknown>, monthDay: string): boolean;
357
+ /** @internal 毎年の月日の規則がその日に当てはまるか(有効期間を含む) */
358
+ export declare function matchAnnual(rule: AnnualRule<unknown>, date: DateString): boolean;
318
359
  /** @internal 値を決めた規則(祝日・曜日で決まった場合は null) */
319
360
  export type DecidingRule<T> = DateRule<T> | AnnualRule<T> | RecurringRuleCompiled<T>;
320
361
  /** @internal 1 日分の結果と、値を決めた規則 */
@@ -10,7 +10,8 @@
10
10
  * 6. weekly … 曜日ごとの値
11
11
  *
12
12
  * "under-holiday" の規則は、祝日の扱いが "weekday"(曜日どおり)なら祝日にも当てはまる。
13
- * "after-holiday" の規則は、祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)
13
+ * "after-holiday" の規則は、祝日には当てはめず、その日以降で最初の祝日でない日に当てはめる(振替)。
14
+ * 毎年の月日・繰り返しの規則の有効期間(effectiveFrom・effectiveTo)の外では、その規則は書かれていないのと同じ
14
15
  *
15
16
  * 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
16
17
  */
@@ -36,6 +37,31 @@ export function isValidMonthDay(value) {
36
37
  }
37
38
  /** @internal 曜日の番号として正しいか */
38
39
  export const isWeekday = (value) => Number.isInteger(value) && value >= 0 && value <= 6;
40
+ /** @internal 日数 n が有効期間の中か */
41
+ export const inPeriod = (rule, n) => (rule.effectiveFrom === null || n >= rule.effectiveFrom) &&
42
+ (rule.effectiveTo === null || n <= rule.effectiveTo);
43
+ /** 有効期間(effectiveFrom・effectiveTo)を検証して日数にする(誤りは c に記録して undefined) */
44
+ function checkPeriod(c, path, entry) {
45
+ let ok = true;
46
+ const read = (key) => {
47
+ const raw = entry[key];
48
+ if (raw === undefined)
49
+ return null;
50
+ const n = checkDate(c, `${path}.${key}`, raw);
51
+ if (n === undefined)
52
+ ok = false;
53
+ return n ?? null;
54
+ };
55
+ const effectiveFrom = read("effectiveFrom");
56
+ const effectiveTo = read("effectiveTo");
57
+ if (!ok)
58
+ return undefined;
59
+ if (effectiveFrom !== null && effectiveTo !== null && effectiveFrom > effectiveTo) {
60
+ c.error("invalid-range", path, `"effectiveFrom" must not be after "effectiveTo" (${String(entry["effectiveFrom"])} > ${String(entry["effectiveTo"])})`);
61
+ return undefined;
62
+ }
63
+ return { effectiveFrom, effectiveTo };
64
+ }
39
65
  function checkName(c, path, name) {
40
66
  if (name === undefined)
41
67
  return undefined;
@@ -123,11 +149,12 @@ export function compileAnnualRules(c, path, entries, valueOf, allowString) {
123
149
  }
124
150
  const value = valueOf(entry, p);
125
151
  const name = checkName(c, p, entry["name"]);
152
+ const period = checkPeriod(c, p, entry);
126
153
  if ("date" in entry) {
127
- checkUnknownKeys(c, p, entry, ["date", "name", "value"]);
154
+ checkUnknownKeys(c, p, entry, ["date", "name", "value", ...PERIOD_KEYS]);
128
155
  const md = checkMonthDay(c, `${p}.date`, entry["date"]);
129
- if (md !== undefined && value !== undefined) {
130
- rules.push({ path: p, from: md, to: md, value, name });
156
+ if (md !== undefined && value !== undefined && period) {
157
+ rules.push({ path: p, from: md, to: md, value, name, ...period });
131
158
  }
132
159
  return;
133
160
  }
@@ -135,11 +162,11 @@ export function compileAnnualRules(c, path, entries, valueOf, allowString) {
135
162
  c.error("invalid-option", p, 'must have "date" or both "from" and "to"');
136
163
  return;
137
164
  }
138
- checkUnknownKeys(c, p, entry, ["from", "to", "name", "value"]);
165
+ checkUnknownKeys(c, p, entry, ["from", "to", "name", "value", ...PERIOD_KEYS]);
139
166
  const from = checkMonthDay(c, `${p}.from`, entry["from"]);
140
167
  const to = checkMonthDay(c, `${p}.to`, entry["to"]);
141
- if (from !== undefined && to !== undefined && value !== undefined) {
142
- rules.push({ path: p, from, to, value, name });
168
+ if (from !== undefined && to !== undefined && value !== undefined && period) {
169
+ rules.push({ path: p, from, to, value, name, ...period });
143
170
  }
144
171
  });
145
172
  return rules;
@@ -189,6 +216,10 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
189
216
  let weekday = null;
190
217
  let nth = null;
191
218
  let days = null;
219
+ let interval = null;
220
+ let anchor = null;
221
+ const hasInterval = "interval" in entry;
222
+ const hasAnchor = "anchor" in entry;
192
223
  if (hasWeekday) {
193
224
  if (isWeekday(entry["weekday"]))
194
225
  weekday = entry["weekday"];
@@ -203,11 +234,39 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
203
234
  else
204
235
  ok = false;
205
236
  }
237
+ if (hasInterval !== hasAnchor) {
238
+ c.error("invalid-option", p, '"interval" and "anchor" must be specified together');
239
+ ok = false;
240
+ }
241
+ else if (hasInterval) {
242
+ if ("nth" in entry) {
243
+ c.error("invalid-option", `${p}.interval`, '"interval" cannot be used with "nth"');
244
+ ok = false;
245
+ }
246
+ const i = entry["interval"];
247
+ if (!Number.isInteger(i) || i < 2) {
248
+ c.error("invalid-option", `${p}.interval`, "must be an integer of 2 or more (weeks)");
249
+ ok = false;
250
+ }
251
+ else
252
+ interval = i;
253
+ const a = checkDate(c, `${p}.anchor`, entry["anchor"]);
254
+ if (a === undefined)
255
+ ok = false;
256
+ else if (weekday !== null && weekdayOf(entry["anchor"]) !== weekday) {
257
+ c.error("invalid-option", `${p}.anchor`, `must be on the same weekday as "weekday" (${String(entry["anchor"])} is weekday ${weekdayOf(entry["anchor"])})`);
258
+ ok = false;
259
+ }
260
+ else
261
+ anchor = a;
262
+ }
206
263
  }
207
264
  else {
208
- if ("nth" in entry) {
209
- c.error("invalid-option", `${p}.nth`, '"nth" can be used only with "weekday"');
210
- ok = false;
265
+ for (const key of ["nth", "interval", "anchor"]) {
266
+ if (key in entry) {
267
+ c.error("invalid-option", `${p}.${key}`, `"${key}" can be used only with "weekday"`);
268
+ ok = false;
269
+ }
211
270
  }
212
271
  const set = checkSet(c, `${p}.day`, entry["day"], isDayOfMonth, "1-31 or -1 (last day)");
213
272
  if (set)
@@ -234,23 +293,30 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
234
293
  c.error("invalid-option", `${p}.onHolidayRule`, `must be one of ${ON_HOLIDAY_RULES.map((v) => `"${v}"`).join(", ")}`);
235
294
  ok = false;
236
295
  }
237
- if (ok && value !== undefined) {
296
+ const period = checkPeriod(c, p, entry);
297
+ if (ok && period && value !== undefined) {
238
298
  rules.push(Object.freeze({
239
299
  path: p,
240
300
  weekday,
241
301
  nth,
302
+ interval,
303
+ anchor,
242
304
  days,
243
305
  months,
244
306
  onHolidayRule: onHoliday,
245
307
  value,
246
308
  name,
309
+ ...period,
247
310
  }));
248
311
  }
249
312
  });
250
313
  return rules;
251
314
  }
252
- /** @internal 繰り返しの規則がその日に当てはまるか(祝日かどうかは考えない) */
253
- export function matchRecurring(rule, date) {
315
+ /**
316
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・月)がその日に当てはまるか。
317
+ * 有効期間と祝日かどうかは考えない(規則の診断で、形どうしの包含を調べるために分けている)
318
+ */
319
+ export function matchPattern(rule, date) {
254
320
  const { year, month, day } = parseDate(date);
255
321
  if (rule.months && !rule.months.has(month))
256
322
  return false;
@@ -258,12 +324,18 @@ export function matchRecurring(rule, date) {
258
324
  if (rule.weekday !== null) {
259
325
  if (weekdayOf(date) !== rule.weekday)
260
326
  return false;
327
+ if (rule.interval !== null)
328
+ return (toDayNumber(date) - rule.anchor) % (7 * rule.interval) === 0;
261
329
  if (rule.nth === null)
262
330
  return true;
263
331
  return rule.nth.has(Math.ceil(day / 7)) || (rule.nth.has(-1) && day + 7 > dim);
264
332
  }
265
333
  return rule.days.has(day) || (rule.days.has(-1) && day === dim);
266
334
  }
335
+ /** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む。祝日かどうかは考えない) */
336
+ export function matchRecurring(rule, date) {
337
+ return inPeriod(rule, toDayNumber(date)) && matchPattern(rule, date);
338
+ }
267
339
  /** 規則の value を取り出す(value が必須の規則用) */
268
340
  function requiredValue(c) {
269
341
  return (entry, path) => {
@@ -485,7 +557,19 @@ const SCHEDULE_OPTION_KEYS = [
485
557
  "recurring",
486
558
  ];
487
559
  /** @internal 繰り返しの規則の項目 */
488
- export const RECURRING_RULE_KEYS = ["weekday", "nth", "day", "months", "name", "onHolidayRule"];
560
+ /** @internal 有効期間の項目 */
561
+ export const PERIOD_KEYS = ["effectiveFrom", "effectiveTo"];
562
+ export const RECURRING_RULE_KEYS = [
563
+ "weekday",
564
+ "nth",
565
+ "interval",
566
+ "anchor",
567
+ "day",
568
+ "months",
569
+ "name",
570
+ "onHolidayRule",
571
+ ...PERIOD_KEYS,
572
+ ];
489
573
  /** @internal スケジュールの設定を検証して取り込む(誤りは c に集める) */
490
574
  export function compileScheduleOptions(c, options) {
491
575
  if (!isObject(options)) {
@@ -526,12 +610,16 @@ export function compileScheduleOptions(c, options) {
526
610
  }
527
611
  /** 振替元をさかのぼる最大日数(祝日が続く日数の上限。2019 年の大型連休でも 10 日) */
528
612
  const MAX_HOLIDAY_RUN = 31;
529
- /** @internal 毎年の月日の規則がその月日 "MM-DD" に当てはまるか */
530
- export function matchAnnual(rule, monthDay) {
613
+ /** @internal 毎年の月日の規則の形がその月日 "MM-DD" に当てはまるか(有効期間は考えない) */
614
+ export function matchMonthDay(rule, monthDay) {
531
615
  return rule.from <= rule.to
532
616
  ? monthDay >= rule.from && monthDay <= rule.to
533
617
  : monthDay >= rule.from || monthDay <= rule.to;
534
618
  }
619
+ /** @internal 毎年の月日の規則がその日に当てはまるか(有効期間を含む) */
620
+ export function matchAnnual(rule, date) {
621
+ return inPeriod(rule, toDayNumber(date)) && matchMonthDay(rule, date.slice(5));
622
+ }
535
623
  /**
536
624
  * @internal 検証済みの規則から、1 日分の値と値を決めた規則を返す関数を作る。
537
625
  * 規則の診断では、規則を取り除いた版と結果を比べるために、祝日を調べる機能(lookup)を共有して作る
@@ -585,8 +673,7 @@ export function createResolver(compiled, lookup) {
585
673
  const byDate = dates.find((r) => dayNumber >= r.from && dayNumber <= r.to);
586
674
  if (byDate)
587
675
  return make(byDate.value, "date", byDate, byDate.name);
588
- const monthDay = date.slice(5);
589
- const byAnnual = annual.find((r) => matchAnnual(r, monthDay));
676
+ const byAnnual = annual.find((r) => matchAnnual(r, date));
590
677
  if (byAnnual)
591
678
  return make(byAnnual.value, "annual", byAnnual, byAnnual.name);
592
679
  if (holiday) {
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * 公開する名前と型は etc/api.md に記録し、pnpm check で意図しない変化を検出する。
9
9
  */
10
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 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";
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";
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";
@@ -47,16 +47,23 @@ export declare function holidaysInYear(year: number): readonly JapaneseHoliday[]
47
47
  export declare function holidaysBetween(from: DateInput, to: DateInput): readonly JapaneseHoliday[];
48
48
  /**
49
49
  * 日本時間での暦日。
50
+ * @deprecated 0.10.0 で非推奨。ルートの toDateString(input, "Asia/Tokyo")、または営業日カレンダー・スケジュールの
51
+ * dateOf(input) に移行する(同じ変換。docs/migration.md)。1.0.0 で削除する
50
52
  * @public
51
53
  */
52
54
  export declare function toJapanDate(input: DateInput): DateString;
53
55
  /**
54
56
  * 日本時間での暦日と時刻("HH:mm"。暦日だけの入力では null)。
55
57
  * 時刻を必ず持つ入力(Date・Temporal.Instant など)では、time の型が string になる。
58
+ * @deprecated 0.10.0 で非推奨。ルートの toDateTimeParts(input, "Asia/Tokyo")、または営業日カレンダー・スケジュールの
59
+ * civilOf(input) に移行する(同じ変換・同じ型。docs/migration.md)。1.0.0 で削除する
56
60
  * @public
57
61
  */
58
62
  export declare function toJapanDateTime(input: DateTimeInput): CivilDateTimeParts<string>;
59
- /** @public */
63
+ /**
64
+ * @deprecated 0.10.0 で非推奨。toDateTimeParts(input, "Asia/Tokyo") または civilOf(input) に移行する
65
+ * @public
66
+ */
60
67
  export declare function toJapanDateTime(input: DateInput): CivilDateTimeParts;
61
68
  /**
62
69
  * 反映済みの官報の年月。
package/dist/jp/index.js CHANGED
@@ -64,6 +64,8 @@ export function holidaysBetween(from, to) {
64
64
  }
65
65
  /**
66
66
  * 日本時間での暦日。
67
+ * @deprecated 0.10.0 で非推奨。ルートの toDateString(input, "Asia/Tokyo")、または営業日カレンダー・スケジュールの
68
+ * dateOf(input) に移行する(同じ変換。docs/migration.md)。1.0.0 で削除する
67
69
  * @public
68
70
  */
69
71
  export function toJapanDate(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bizdays-toolkit",
3
- "version": "0.9.0",
3
+ "version": "0.10.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",