bizdays-toolkit 0.17.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,55 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.19.0] - 2026-10-25
8
+
9
+ ### 移行
10
+
11
+ 動作が変わる既存の API はない(`"after-holiday"` の規則に `skipWeekdays` を付けなければ振替先はこれまでどおり)。繰り返しの規則の形 `{ businessDayOfMonth }` と項目 `skipWeekdays`・`calendar`、診断コード `counting-basis` が増える。版ごとの作業は [docs/migration.md](./docs/migration.md) にまとめた。
12
+
13
+ ### Added
14
+
15
+ - 繰り返しの規則に月の第 n 営業日の形 `{ businessDayOfMonth: n }` を追加した(型 `BusinessDayOfMonthPattern`・`BusinessDayOfMonth`。n は 1〜31 と -1(最後)〜-31、配列可、`months` で絞れる)。「月末最終営業日は棚卸しで休業」「四半期初の第 1・第 5 営業日は予約を止める」を規則として書ける。数える基準はそのカレンダーの繰り返しの規則をすべて除いた判定(個別の日付・毎年の休業日・祝日・定休日の曜日)で、`nthBusinessDayOfMonth`・`lastBusinessDayOfMonth` は規則を適用した後の実際の営業日を返す。`onHolidayRule` は `"under-holiday"`(既定)と `"over-holiday"`(`holidayPolicy: "open"` で第 n 営業日が祝日に当たるときも適用する)を受け、`"after-holiday"` は `invalid-option`。スケジュールでは数える基準のカレンダーを `calendar` で指定する(必須。タイムゾーンが違えば `time-zone-conflict`。カレンダーの側に書くと `invalid-option`)
16
+ - `"after-holiday"` の曜日・日の規則に `skipWeekdays` を追加した。本来の日が祝日かその曜日なら、以降で最初の「祝日でもその曜日でもない日」に振り替える(「6 月 15 日は創立記念日。土日祝なら翌営業日」は `{ day: 15, months: [6], onHolidayRule: "after-holiday", skipWeekdays: [0, 6] }`、「月曜定休。祝日なら翌平日(土曜日には落とさない)」は `{ weekday: 1, onHolidayRule: "after-holiday", skipWeekdays: [6] }`)。`"after-holiday"` 以外の規則や規則自身の曜日に付けると `invalid-option`
17
+ - 診断コード `counting-basis`(info)を追加した。`businessDayOfMonth` の規則と、同じ設定の毎週の曜日の規則(`{ weekday }`)が同居する設定で、その曜日が営業日として数えられることを知らせる。`businessDayOfMonth` の規則は `never-matches`(その月にない回)・`shadowed`(n・months・基準のカレンダーの包含)・`redundant` の対象。`holidayPolicy: "closed"` の `businessDayOfMonth` の規則の `"over-holiday"` は `redundant`(info)。`skipWeekdays` が違う規則どうしは `shadowed` にしない
18
+
19
+ ### Changed
20
+
21
+ - `docs/guide.md`・`docs/guide.ja.md`: 繰り返しの規則の表と本文に第 n 営業日と `skipWeekdays` を足し、コード例(銀行の支店とスケジュール)を足した
22
+ - README: 規則の表に `businessDayOfMonth`・`skipWeekdays` を足した
23
+ - `docs/decisions.md`: 47 番(第 n 営業日の規則・`skipWeekdays`・監視の報告書の日付)を追加し、14 番に `skipWeekdays` の追記をした
24
+
25
+ ### 開発
26
+
27
+ - 監視の照合スクリプト(`scripts/verify-govuk.ts`・`verify-datagovsg.ts`・`verify-cao.ts`)の「確認した日付」を UTC から日本時間に揃えた(issue のコメントの日付と同じ基準)
28
+ - テスト: `test/business-day-of-month.test.ts`・`test/skip-weekdays.test.ts` を追加し、`test/types.check.ts`・利用者の型チェックに新しい規則を足した
29
+ - サイズの上限(`size-limits.json`)を上げた。ルート 25,000 → 26,500(測定 25,100)、`./jp` 28,000 → 29,500(測定 28,128)、`./diagnose` 33,000 → 35,500(測定 34,038)(gzip)。第 n 営業日の規則の判定・基準のキャッシュ・検証と、`skipWeekdays` の振替、診断の追加のため
30
+
31
+ ## [0.18.0] - 2026-10-25
32
+
33
+ ### 移行
34
+
35
+ 動作が変わる既存の API はない。同梱の提供元が返す祝日に省略可能な項目 `key` が加わるので、`dayInfo` の結果や `holidaysInYear` の要素を `deepEqual` で比べているテストは期待値に足す(国民の休日・臨時の祝日には付かない)。祝日の規則に省略可能な `keys` が加わる。版ごとの作業は [docs/migration.md](./docs/migration.md) にまとめた。
36
+
37
+ ### Added
38
+
39
+ - 祝日の識別子 `Holiday.key`(省略可能。`/^[a-z0-9][a-z0-9-]*$/`)と、祝日の規則の絞り込み `{ holiday: true, keys: [...] }`(`kinds`・`weekdays`・`months` との AND)を追加した。春分の日・復活祭に連動する日・太陰暦の祝日のように月日や曜日では書けない祝日を、名称に依存せず個別に指せる。同梱の提供元は毎年繰り返す祝日に `key` を付け(日本 16・英国 12・シンガポール 10。`docs/guide.md` の「Holiday keys」)、振替日は元の祝日の `key` を持ち、国民の休日・一度きりの祝日は持たない。`key` は日付の移動や公式の改名があっても変えない。補完用の型 `KnownJapaneseHolidayKey`・`KnownUkHolidayKey`・`KnownSgHolidayKey` を追加した(`@beta`)。利用者の提供元が付けた `key` は形式だけ検証し、複製・凍結して返す。診断は対応年に現れない `key` を `never-matches`、`key` の集合の包含を `shadowed` で扱う
40
+ - `bizdays-toolkit/provider` に、週末に当たった祝日の振替日を計算する `substituteDays(holidays, { rules, kind, name?, original? })` を追加した(`@beta`。型 `SubstituteDaysOptions`・`SubstituteDirection`)。`rules` は曜日ごとの向き(`"next"`・`"previous"`)で、振替先は「入力の祝日でも、決めた振替日でも、`rules` の曜日でもない最初の日」。振替日は元の祝日の `key`・`names`・`confirmed` を引き継ぐ。日本(2007 年以降)・英国・シンガポールの同梱の提供元の振替日を再現することをテストで確かめている(内部の実装は置き換えていない)
41
+
42
+ ### Changed
43
+
44
+ - `docs/guide.md`・`docs/guide.ja.md`: 「Holiday keys」(祝日の識別子)の節と、繰り返しの規則の `keys` を足した。コード例の結果に `key` が増えた
45
+ - `docs/providers.md`・`docs/providers.ja.md`: `substituteDays` と、自作の提供元に `key` を付ける方法を足した
46
+ - README: 規則の表と機能の箇条書きに `key`・振替の計算を足し、クイックスタートのコメントの "Labour Thanksgiving Day" を `nameEn` の綴り "Labor Thanksgiving Day" に直した
47
+ - `RELEASING.md`: 区分の表で「対応年の拡大」を規則で計算する提供元(日本・英国)に限り、年ごとの表で持つ提供元(シンガポール)に告示済みの新しい年を足すことはパッチに置く、と線を引いた(これまでは 2 つの行が同時に当てはまる状態だった)。「毎年の作業(日本)」を「日本のデータの更新」に改め、監視の差分の種類と対応の表を足した
48
+ - `docs/decisions.md`: 46 番(祝日の識別子と絞り込み、`substituteDays`、内閣府 CSV の毎日の照合、区分の線引き)を追加し、37 番の理由(存在しない「将来の非純粋な判定関数」を前提にしていた)と 44 番の `asOf` の理由(`Date` を断る理由としてタイムゾーンは成り立たなかった)を改めた
49
+
50
+ ### 開発
51
+
52
+ - 監視のワークフロー `data-watch.yml` に内閣府「国民の祝日」CSV の照合を足した(issue のラベル `cao-data`。ハートビートは 3 つの照合がすべて動いたときだけ)。`scripts/verify-cao.ts` を英国・シンガポールと同じ形(`--report`・`--simulate`、終了コード 0・1・2、取得の再試行、「New years published」・フィクスチャの差分の節)にした。翌年の春分・秋分の日の公表が issue で届くので、2 月の作業を記憶に頼らない
53
+ - テスト: `test/holiday-key.test.ts` を追加し、`test/provider.test.ts` に `substituteDays`(同梱の 3 か国との同値・向き・名称・入力の検証)を足した。`test/types.check.ts`・利用者の型チェックに `keys`・既知の `key` の型・`substituteDays` を足した
54
+ - サイズの上限(`size-limits.json`)を上げた。`./provider` 4,500 → 6,500(`substituteDays` と入力の検証のため。測定 6,019)、`./diagnose` 32,000 → 33,000(`keys` の判定と診断のため。測定 31,645)(gzip)
55
+
7
56
  ## [0.17.1] - 2026-10-25
8
57
 
9
58
  ### Changed
@@ -469,7 +518,9 @@
469
518
 
470
519
  - 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
471
520
 
472
- [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.1...HEAD
521
+ [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.19.0...HEAD
522
+ [0.19.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.18.0...v0.19.0
523
+ [0.18.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.1...v0.18.0
473
524
  [0.17.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.0...v0.17.1
474
525
  [0.17.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.16.0...v0.17.0
475
526
  [0.16.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.15.0...v0.16.0
package/README.md CHANGED
@@ -25,7 +25,7 @@ A business-day calendar that combines public holidays with company or store clos
25
25
  - **Day-by-day values and combined calendars**: store hours that change by weekday, holiday and date; a business-day calendar made from such a schedule; and the days on which all (or any) of several calendars are open
26
26
  - **Option diagnostics**: `bizdays-toolkit/diagnose` lists rules that never apply, rules that are shadowed or redundant, rules that have ended, typos in option names and options with no business day at all, without throwing. One line in a test keeps options honest
27
27
  - **Lightweight, zero dependencies, time-zone safe**: about 10.0 KB min+gzip (about 11.8 KB with Japanese holidays, about 11.7 KB with UK bank holidays, about 10.9 KB with Singapore holidays; measured for 0.17.0). Standard JavaScript and `Intl` only, ESM with bundled types; tested on Node.js, Chrome, Firefox, Edge, Safari, Cloudflare Workers, Bun and Deno. Instants such as `Date` are converted to a civil date in the calendar's time zone before evaluation
28
- - **Extensible to other countries and regions**: holiday providers are pluggable through the `HolidayProvider` interface, and `bizdays-toolkit/provider` exposes the calendar arithmetic (nth weekday of a month, Easter) that most countries' holidays are built from
28
+ - **Extensible to other countries and regions**: holiday providers are pluggable through the `HolidayProvider` interface, and `bizdays-toolkit/provider` exposes the calendar arithmetic (nth weekday of a month, Easter, substitute days for holidays on a weekend) that most countries' holidays are built from
29
29
 
30
30
  ## Installation
31
31
 
@@ -43,7 +43,7 @@ Types are bundled. CI type-checks a consumer project against the published type
43
43
  import { createCalendar } from "bizdays-toolkit";
44
44
  import { isHoliday, jp } from "bizdays-toolkit/jp";
45
45
 
46
- isHoliday("2026-11-23"); // true (Labour Thanksgiving Day)
46
+ isHoliday("2026-11-23"); // true (Labor Thanksgiving Day)
47
47
 
48
48
  const calendar = createCalendar({
49
49
  holidays: jp,
@@ -74,17 +74,19 @@ Each holiday carries its date, name (and English name), `kind` (national, substi
74
74
 
75
75
  A calendar is built from the holiday provider and your closures: `closedWeekdays`, `closedAnnual` (month-day ranges, across the new year too), `closedDates` and `openDates`, and recurring rules in `closedRecurring` / `openRecurring`:
76
76
 
77
- | Rule | Meaning |
78
- | --------------------------------------------------------- | ------------------------------------------------------------------------------- |
79
- | `{ weekday: 3, nth: 3 }` | The 3rd Wednesday (`nth` is 1–5 or -1 for the last; an array for several) |
80
- | `{ weekday: 3, interval: 2, anchor: "2026-01-07" }` | Every other Wednesday, counted from the anchor |
81
- | `{ day: 20 }`, `{ day: -1 }` | The 20th, the last day of the month |
82
- | `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed by kind |
83
- | `{ dayAfterHoliday: true }`, `{ dayBeforeHoliday: true }` | The day after (or before) a holiday or a run of holidays |
84
- | `onHolidayRule: "after-holiday"` | "Closed on Mondays, or the next weekday when Monday is a holiday" (museum rule) |
85
- | `effectiveFrom`, `effectiveTo` | The period the rule is in effect |
86
-
87
- `dayInfo` tells you why a day is open or closed (`reason`, `name`, `rulePath`). Details: [Business-day calendars](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#business-day-calendars), [Recurring rules](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#recurring-rules), [Common recipes](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#common-recipes).
77
+ | Rule | Meaning |
78
+ | --------------------------------------------------------- | -------------------------------------------------------------------------------- |
79
+ | `{ weekday: 3, nth: 3 }` | The 3rd Wednesday (`nth` is 1–5 or -1 for the last; an array for several) |
80
+ | `{ weekday: 3, interval: 2, anchor: "2026-01-07" }` | Every other Wednesday, counted from the anchor |
81
+ | `{ day: 20 }`, `{ day: -1 }` | The 20th, the last day of the month |
82
+ | `{ businessDayOfMonth: -1 }`, `{ businessDayOfMonth: 5 }` | The last business day of the month, the 5th business day |
83
+ | `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed by kind or by key (`keys: ["vernal-equinox"]`) |
84
+ | `{ dayAfterHoliday: true }`, `{ dayBeforeHoliday: true }` | The day after (or before) a holiday or a run of holidays |
85
+ | `onHolidayRule: "after-holiday"` | "Closed on Mondays, or the next weekday when Monday is a holiday" (museum rule) |
86
+ | `skipWeekdays: [0, 6]` | With `"after-holiday"`: also skip weekends ("June 15, or the next business day") |
87
+ | `effectiveFrom`, `effectiveTo` | The period the rule is in effect |
88
+
89
+ `dayInfo` tells you why a day is open or closed (`reason`, `name`, `rulePath`). Every bundled holiday that recurs each year has a stable `key` (`vernal-equinox`, `good-friday`, `hari-raya-puasa`, …), so a rule can select individual holidays that no date pattern can express. Details: [Business-day calendars](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#business-day-calendars), [Recurring rules](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#recurring-rules), [Holiday keys](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#holiday-keys), [Common recipes](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md#common-recipes).
88
90
 
89
91
  ### Due dates, schedules and combined calendars
90
92
 
@@ -72,8 +72,8 @@ export function compileCalendarOptions(c, options) {
72
72
  const annual = compileAnnualRules(c, "closedAnnual", o["closedAnnual"], () => false, true);
73
73
  const open = compileDateRules(c, "openDates", o["openDates"], () => true, true);
74
74
  const closed = compileDateRules(c, "closedDates", o["closedDates"], () => false, true);
75
- const openRecurring = compileRecurringRules(c, "openRecurring", o["openRecurring"], () => true, RECURRING_RULE_KEYS);
76
- const closedRecurring = compileRecurringRules(c, "closedRecurring", o["closedRecurring"], () => false, RECURRING_RULE_KEYS);
75
+ const openRecurring = compileRecurringRules(c, "openRecurring", o["openRecurring"], () => true, RECURRING_RULE_KEYS, "calendar");
76
+ const closedRecurring = compileRecurringRules(c, "closedRecurring", o["closedRecurring"], () => false, RECURRING_RULE_KEYS, "calendar");
77
77
  return {
78
78
  schedule: {
79
79
  source,
@@ -13,6 +13,8 @@
13
13
  * no-business-day … 営業日が 1 日もない設定(営業日カレンダーのみ)
14
14
  * - info … redundant: 取り除いても結果が変わらない規則・設定(判定に祝日を使わない設定の unconfirmedHolidays: "throw" を含む)
15
15
  * expired: 第 2 引数の asOf より前に終わった規則・日付(asOf を渡したときだけ)
16
+ * counting-basis: 第 n 営業日の規則(businessDayOfMonth)の数える基準は繰り返しの規則を除くので、
17
+ * 同じ設定の毎週の曜日の規則({ weekday })の曜日は営業日として数えられる(relatedPath にその規則)
16
18
  *
17
19
  * 未確定の祝日の扱い(unconfirmedHolidays)は診断では常に "accept" で評価する。診断は設定の形を調べるもので、
18
20
  * 祝日が確定しているかどうかは関係がなく、"throw" のまま評価すると例外を握りつぶして「当てはまらない」と誤判定するため
@@ -51,7 +53,9 @@ export type KnownDiagnosticCode = KnownErrorCode
51
53
  /** 取り除いても結果が変わらない */
52
54
  | "redundant"
53
55
  /** 基準日(asOf)より前に終わった規則・日付(整理の候補) */
54
- | "expired";
56
+ | "expired"
57
+ /** 第 n 営業日の規則の数える基準に、同じ設定の曜日の規則が入らない(その曜日を営業日として数える) */
58
+ | "counting-basis";
55
59
  /**
56
60
  * 診断コード。将来の版で値が増えることがあるので、分岐では「その他」も扱うこと。
57
61
  * @public
@@ -13,6 +13,8 @@
13
13
  * no-business-day … 営業日が 1 日もない設定(営業日カレンダーのみ)
14
14
  * - info … redundant: 取り除いても結果が変わらない規則・設定(判定に祝日を使わない設定の unconfirmedHolidays: "throw" を含む)
15
15
  * expired: 第 2 引数の asOf より前に終わった規則・日付(asOf を渡したときだけ)
16
+ * counting-basis: 第 n 営業日の規則(businessDayOfMonth)の数える基準は繰り返しの規則を除くので、
17
+ * 同じ設定の毎週の曜日の規則({ weekday })の曜日は営業日として数えられる(relatedPath にその規則)
16
18
  *
17
19
  * 未確定の祝日の扱い(unconfirmedHolidays)は診断では常に "accept" で評価する。診断は設定の形を調べるもので、
18
20
  * 祝日が確定しているかどうかは関係がなく、"throw" のまま評価すると例外を握りつぶして「当てはまらない」と誤判定するため
@@ -26,7 +28,7 @@
26
28
  import { daysInMonth, fromDayNumber, isValidDate, parseDate, toDayNumber, } from "./date.js";
27
29
  import { compileCalendarOptions } from "./calendar.js";
28
30
  import { IssueCollector, rangeError } from "./errors.js";
29
- import { compileScheduleOptions, createHolidayLookup, createResolver, inPeriod, matchMonthDay, matchPattern, } from "./schedule.js";
31
+ import { compileScheduleOptions, createBasis, createHolidayLookup, createResolver, inPeriod, matchMonthDay, matchPattern, } from "./schedule.js";
30
32
  // ------------------------------------------------------------
31
33
  // 知らないキーの候補
32
34
  // ------------------------------------------------------------
@@ -311,6 +313,7 @@ function analyze(c, compiled, isCalendar) {
311
313
  if (!checkProviderData(c, source))
312
314
  return;
313
315
  const lookup = createHolidayLookup({ ...source, unconfirmedHolidays: "accept" });
316
+ const basis = createBasis(compiled, lookup);
314
317
  const cycle = patternCycle(recurring);
315
318
  const window = scanWindow(source, [...annual, ...recurring], cycle);
316
319
  /** 使われないことが分かった規則(redundant を重ねて出さない) */
@@ -381,34 +384,84 @@ function analyze(c, compiled, isCalendar) {
381
384
  eachDay(window, (n) => (found = inPeriod(rule, n) && matchPattern(rule, fromDayNumber(n), holidayOfSafe)));
382
385
  return found;
383
386
  };
387
+ /** 第 n 営業日の規則が、調べる期間と有効期間の中で 1 日でも当てはまるか(基準は祝日次第で年ごとに違うので、全日を調べる。対応年の外は当てはまらないとみなす) */
388
+ const businessDayRuleMatches = (rule) => {
389
+ if (!window)
390
+ return false;
391
+ let found = false;
392
+ eachDay(window, (n) => {
393
+ if (!inPeriod(rule, n))
394
+ return false;
395
+ try {
396
+ found = matchPattern(rule, fromDayNumber(n), holidayOfSafe, basis);
397
+ }
398
+ catch {
399
+ found = false;
400
+ }
401
+ return found;
402
+ });
403
+ return found;
404
+ };
384
405
  /** 集合 a(null はすべて)が集合 b(null はすべて)を含むか */
385
406
  const setCovers = (a, b) => a === null || (b !== null && [...b].every((v) => a.has(v)));
407
+ /** 振替で飛ばす曜日が同じか(違えば当てはまる日が違うので覆わない) */
408
+ const sameSkip = (a, b) => a.skipWeekdays === null
409
+ ? b.skipWeekdays === null
410
+ : b.skipWeekdays !== null &&
411
+ a.skipWeekdays.size === b.skipWeekdays.size &&
412
+ [...a.skipWeekdays].every((w) => b.skipWeekdays.has(w));
386
413
  recurring.forEach((rule, j) => {
387
414
  const bits = recurringBits[j];
388
415
  const holidayBased = rule.holiday || rule.dayAfterHoliday || rule.dayBeforeHoliday;
389
- const matches = holidayBased ? holidayRuleMatches(rule) : matchesAnyDay(rule, cycle);
416
+ const businessDayBased = rule.businessDays !== null;
417
+ const matches = holidayBased
418
+ ? holidayRuleMatches(rule)
419
+ : businessDayBased
420
+ ? businessDayRuleMatches(rule)
421
+ : matchesAnyDay(rule, cycle);
390
422
  if (!matches) {
391
423
  unused.add(rule);
392
424
  c.warning("never-matches", rule.path, holidayBased
393
425
  ? source.providers.length === 0
394
426
  ? "This rule matches no day: there are no holidays without a holiday provider"
395
427
  : rule.holiday
396
- ? "This rule matches no day: no holiday of the selected kinds, weekdays and months within its effective period"
428
+ ? "This rule matches no day: no holiday of the selected kinds, keys, weekdays and months within its effective period"
397
429
  : rule.dayAfterHoliday
398
430
  ? "This rule matches no day: no day after a holiday on the selected weekdays and months within its effective period"
399
431
  : "This rule matches no day: no day before a holiday on the selected weekdays and months within its effective period"
400
- : rule.effectiveFrom === null && rule.effectiveTo === null
401
- ? "This rule matches no day (for example, a day that does not exist in the selected months)"
402
- : "This rule matches no day within its effective period");
432
+ : businessDayBased
433
+ ? "This rule matches no day: no month has that many business days of the counting basis in the selected months within its effective period"
434
+ : rule.effectiveFrom === null && rule.effectiveTo === null
435
+ ? "This rule matches no day (for example, a day that does not exist in the selected months)"
436
+ : "This rule matches no day within its effective period");
403
437
  return;
404
438
  }
405
- // 祝日・祝日の翌日・祝前日の規則は祝日次第で年ごとに変わるので、形の包含は同じ形の規則どうし(種別・曜日・月の絞り込み)だけで調べる
439
+ // 第 n 営業日の規則の "over-holiday" は、祝日が基準の営業日にならない設定(holidayPolicy: "closed")では結果を変えない
440
+ if (businessDayBased &&
441
+ rule.basis === null &&
442
+ rule.onHolidayRule === "over-holiday" &&
443
+ typeof compiled.holiday === "object" &&
444
+ "value" in compiled.holiday &&
445
+ compiled.holiday.value === false) {
446
+ c.info("redundant", `${rule.path}.onHolidayRule`, 'This option does not change the result: holidays are closed (holidayPolicy: "closed"), so an nth business day is never a holiday');
447
+ }
448
+ // 第 n 営業日の規則の数える基準は繰り返しの規則を除くので、同じ設定の毎週の曜日の規則があれば知らせる(定休日を曜日の規則で書いた設定)
449
+ if (businessDayBased && rule.basis === null) {
450
+ const weekly = recurring.find((r) => r.weekday !== null && r.nth === null && r.interval === null && r.months === null);
451
+ if (weekly) {
452
+ c.info("counting-basis", rule.path, `The business days are counted without recurring rules, so the weekday rule ${weekly.path} does not reduce the count; ` +
453
+ "write regular closed weekdays as closedWeekdays if they should not be counted", weekly.path);
454
+ }
455
+ }
456
+ // 祝日・祝日の翌日・祝前日の規則は祝日次第で年ごとに変わるので、形の包含は同じ形の規則どうし(種別・識別子・曜日・月の絞り込み)だけで調べる
406
457
  const k = recurring.findIndex((r, i) => i < j &&
407
458
  canCover(r, rule) &&
408
459
  periodCovers(r, rule) &&
460
+ sameSkip(r, rule) &&
409
461
  (rule.holiday
410
462
  ? r.holiday &&
411
463
  setCovers(r.kinds, rule.kinds) &&
464
+ setCovers(r.keys, rule.keys) &&
412
465
  setCovers(r.weekdays, rule.weekdays) &&
413
466
  setCovers(r.months, rule.months)
414
467
  : rule.dayAfterHoliday || rule.dayBeforeHoliday
@@ -416,11 +469,17 @@ function analyze(c, compiled, isCalendar) {
416
469
  r.dayBeforeHoliday === rule.dayBeforeHoliday &&
417
470
  setCovers(r.weekdays, rule.weekdays) &&
418
471
  setCovers(r.months, rule.months)
419
- : !r.holiday &&
420
- !r.dayAfterHoliday &&
421
- !r.dayBeforeHoliday &&
422
- intervalCovers(r, rule) &&
423
- isSubset(bits, recurringBits[i])));
472
+ : rule.businessDays !== null
473
+ ? r.businessDays !== null &&
474
+ r.calendar === rule.calendar &&
475
+ setCovers(r.businessDays, rule.businessDays) &&
476
+ setCovers(r.months, rule.months)
477
+ : !r.holiday &&
478
+ !r.dayAfterHoliday &&
479
+ !r.dayBeforeHoliday &&
480
+ r.businessDays === null &&
481
+ intervalCovers(r, rule) &&
482
+ isSubset(bits, recurringBits[i])));
424
483
  if (k < 0)
425
484
  return;
426
485
  unused.add(rule);
@@ -14,7 +14,7 @@
14
14
  * 毎年の月日・繰り返しの規則の有効期間(effectiveFrom・effectiveTo)の外では、その規則は書かれていないのと同じ。
15
15
  * 祝日の翌日の規則(dayAfterHoliday)は、前日が祝日でその日自身は祝日でない日に当てはまる(提供元の祝日だけで判定)。
16
16
  * 祝前日の規則(dayBeforeHoliday)は、翌日が祝日でその日自身は祝日でない日に当てはまる(同じく提供元の祝日だけ)。
17
- * 祝日の規則(holiday: true。kinds で種別を絞れる)は、「祝日の上」の段階で判定する
17
+ * 祝日の規則(holiday: true。kinds で種別を、keys で個別の祝日を絞れる)は、「祝日の上」の段階で判定する
18
18
  *
19
19
  * 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
20
20
  */
@@ -22,7 +22,8 @@ import { type DateString, type Weekday } from "./date.ts";
22
22
  import { IssueCollector } from "./errors.ts";
23
23
  import { type CivilDateTimeParts, type DateInput, type DateTimeInput } from "./input.ts";
24
24
  export type { CivilDateTimeParts } from "./input.ts";
25
- import type { Holiday, HolidayProvider } from "./types.ts";
25
+ import { type Holiday, type HolidayProvider } from "./types.ts";
26
+ import type { Calendar } from "./calendar.ts";
26
27
  /**
27
28
  * 祝日の値の決め方(スケジュールの holiday の設定)。
28
29
  * @public
@@ -48,6 +49,11 @@ export type Nth = 1 | 2 | 3 | 4 | 5 | -1;
48
49
  * @public
49
50
  */
50
51
  export type DayOfMonth = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -1;
52
+ /**
53
+ * 月の第 n 営業日(1〜31。-1 は最後、-2 はその前、… -31 まで。0 は不可)。
54
+ * @public
55
+ */
56
+ export type BusinessDayOfMonth = number;
51
57
  /**
52
58
  * 月(1〜12)。
53
59
  * @public
@@ -89,12 +95,22 @@ export interface WeekdayPattern {
89
95
  readonly anchor?: DateString;
90
96
  /** 当てはめる月(省略時はすべての月) */
91
97
  readonly months?: readonly Month[];
98
+ /**
99
+ * onHolidayRule: "after-holiday" と組で、祝日に加えて飛ばす曜日(0 = 日曜日〜6 = 土曜日)。
100
+ * 本来の日が祝日かこの曜日なら、その日以降で最初の、祝日でもこの曜日でもない日に振り替える
101
+ * (例: 月曜定休で、月曜が祝日なら翌平日(土曜日には落とさない)は { weekday: 1, onHolidayRule: "after-holiday", skipWeekdays: [6] })。
102
+ * weekday 自身は含められない。"after-holiday" 以外と組にすると invalid-option
103
+ */
104
+ readonly skipWeekdays?: readonly Weekday[];
92
105
  readonly day?: never;
106
+ readonly businessDayOfMonth?: never;
107
+ readonly calendar?: never;
93
108
  readonly dayAfterHoliday?: never;
94
109
  readonly dayBeforeHoliday?: never;
95
110
  readonly holiday?: never;
96
111
  readonly weekdays?: never;
97
112
  readonly kinds?: never;
113
+ readonly keys?: never;
98
114
  }
99
115
  /**
100
116
  * 毎月の日の繰り返し(例: 毎月 20 日、毎月末日)。その月にない日(2 月 30 日など)は当てはまらない。
@@ -105,15 +121,61 @@ export interface DayOfMonthPattern {
105
121
  readonly day: DayOfMonth | readonly DayOfMonth[];
106
122
  /** 当てはめる月(省略時はすべての月) */
107
123
  readonly months?: readonly Month[];
124
+ /**
125
+ * onHolidayRule: "after-holiday" と組で、祝日に加えて飛ばす曜日。本来の日が祝日かこの曜日なら、その日以降で最初の、
126
+ * 祝日でもこの曜日でもない日に振り替える(例: 「6 月 15 日は創立記念日。土日祝なら翌営業日」は
127
+ * { day: 15, months: [6], onHolidayRule: "after-holiday", skipWeekdays: [0, 6] })。"after-holiday" 以外と組にすると invalid-option
128
+ */
129
+ readonly skipWeekdays?: readonly Weekday[];
108
130
  readonly weekday?: never;
109
131
  readonly nth?: never;
110
132
  readonly interval?: never;
111
133
  readonly anchor?: never;
134
+ readonly businessDayOfMonth?: never;
135
+ readonly calendar?: never;
112
136
  readonly dayAfterHoliday?: never;
113
137
  readonly dayBeforeHoliday?: never;
114
138
  readonly holiday?: never;
115
139
  readonly weekdays?: never;
116
140
  readonly kinds?: never;
141
+ readonly keys?: never;
142
+ }
143
+ /**
144
+ * 月の第 n 営業日(例: 月末最終営業日、第 5 営業日)。n は 1〜31、-1 は最後、-2 はその前。その月にない回は当てはまらない。
145
+ *
146
+ * 数える基準はこの規則を書いたカレンダーの「繰り返しの規則をすべて除いた判定」(個別の日付 → 毎年の月日 → 祝日 → 定休日の曜日)で、
147
+ * 定休日の曜日・祝日・年末年始休業(closedAnnual)・臨時休業日は数えに入らない。繰り返しの規則(定休日を closedRecurring の
148
+ * 曜日の規則で書いたものを含む)は基準に入らない。nthBusinessDayOfMonth・lastBusinessDayOfMonth はこの規則を適用した後の
149
+ * 実際の営業日を返すので、{ businessDayOfMonth: -1 } を休業にすると lastBusinessDayOfMonth はその前日になる。
150
+ * onHolidayRule は "under-holiday"(既定)と "over-holiday" を受け、"after-holiday" は invalid-option(第 n 営業日をずらす意味がない)。
151
+ * holidayPolicy: "open" のカレンダーでは祝日も営業日として数えるので第 n 営業日が祝日に当たることがあり、既定ではその日は
152
+ * 祝日の段階が先に決めてこの規則は当てはまらない(曜日の規則と同じ優先順位)。「第 1 営業日は祝日でも休業」は "over-holiday" で書く。
153
+ * holidayPolicy: "closed" では祝日が基準の営業日にならないので "over-holiday" は結果を変えない(診断が info で知らせる)。
154
+ *
155
+ * スケジュール(createSchedule の recurring)では、スケジュール自身に営業・休業の概念がないので、数える基準にする作成済みの
156
+ * カレンダーを calendar で指定する(必須。タイムゾーンはスケジュールと同じであること)。カレンダー(closedRecurring・openRecurring)
157
+ * では calendar は指定できない(invalid-option。基準は自分の設定で決まる)。
158
+ * @public
159
+ */
160
+ export interface BusinessDayOfMonthPattern {
161
+ /** 月の第 n 営業日(1〜31、-1 は最後、-2 はその前、… -31 まで)。配列で複数指定できる */
162
+ readonly businessDayOfMonth: BusinessDayOfMonth | readonly BusinessDayOfMonth[];
163
+ /** 当てはめる月(省略時はすべての月) */
164
+ readonly months?: readonly Month[];
165
+ /** 営業日を数える基準のカレンダー(スケジュールでは必須。カレンダーでは指定できない) */
166
+ readonly calendar?: Calendar;
167
+ readonly weekday?: never;
168
+ readonly nth?: never;
169
+ readonly interval?: never;
170
+ readonly anchor?: never;
171
+ readonly day?: never;
172
+ readonly dayAfterHoliday?: never;
173
+ readonly dayBeforeHoliday?: never;
174
+ readonly holiday?: never;
175
+ readonly weekdays?: never;
176
+ readonly kinds?: never;
177
+ readonly keys?: never;
178
+ readonly skipWeekdays?: never;
117
179
  }
118
180
  /**
119
181
  * 祝日の翌日(祝日が続く場合は連休が明けた日。その日自身は祝日でない)。「祝日」は提供元の祝日だけ。
@@ -131,9 +193,13 @@ export interface DayAfterHolidayPattern {
131
193
  readonly interval?: never;
132
194
  readonly anchor?: never;
133
195
  readonly day?: never;
196
+ readonly businessDayOfMonth?: never;
197
+ readonly calendar?: never;
134
198
  readonly dayBeforeHoliday?: never;
135
199
  readonly holiday?: never;
136
200
  readonly kinds?: never;
201
+ readonly keys?: never;
202
+ readonly skipWeekdays?: never;
137
203
  }
138
204
  /**
139
205
  * 祝前日(翌日が祝日で、その日自身は祝日でない日。連休なら連休の初日の前日)。「祝日」は提供元の祝日だけ。
@@ -152,12 +218,16 @@ export interface DayBeforeHolidayPattern {
152
218
  readonly interval?: never;
153
219
  readonly anchor?: never;
154
220
  readonly day?: never;
221
+ readonly businessDayOfMonth?: never;
222
+ readonly calendar?: never;
155
223
  readonly dayAfterHoliday?: never;
156
224
  readonly holiday?: never;
157
225
  readonly kinds?: never;
226
+ readonly keys?: never;
227
+ readonly skipWeekdays?: never;
158
228
  }
159
229
  /**
160
- * 祝日(提供元の祝日である日)。kinds で祝日の種別を絞れる(省略時はすべての祝日)。
230
+ * 祝日(提供元の祝日である日)。kinds で祝日の種別を、keys で個別の祝日を絞れる(省略時はすべての祝日)。
161
231
  * 「祝日の上」の段階(holidayPolicy・スケジュールの holiday より先)で判定し、祝日の既定の扱いの例外を書く
162
232
  * (例: 「2027 年 4 月から祝日も営業」「振替休日は診療」「5 月の祝日は特別営業時間」)。
163
233
  * 当てはまる日は祝日なので、onHolidayRule は指定できない(指定すると invalid-option)。
@@ -167,6 +237,14 @@ export interface HolidayPattern<Kind extends string = string> {
167
237
  readonly holiday: true;
168
238
  /** 当てはめる祝日の種別(Holiday.kind。省略時はすべて)。提供元の種別の型で補完が効く(型は開いているので他の文字列も通る) */
169
239
  readonly kinds?: readonly Kind[];
240
+ /**
241
+ * 当てはめる祝日の識別子(Holiday.key。省略時はすべて)。kinds・weekdays・months との AND。
242
+ * 春分の日・復活祭に連動する日・太陰暦の祝日のように月日や第 n 曜日では書けない祝日を個別に指す
243
+ * (例: 「春分の日・秋分の日は営業」は { holiday: true, keys: ["vernal-equinox", "autumnal-equinox"] })。
244
+ * 振替日は元の祝日の key を持つので「成人の日とその振替」を指せる。当日だけなら kinds を併せて指定する。
245
+ * key を持たない祝日(国民の休日・臨時の祝日)には当てはまらない
246
+ */
247
+ readonly keys?: readonly string[];
170
248
  /** 当てはめる曜日(省略時はすべての曜日。例: [6] で土曜の祝日だけ) */
171
249
  readonly weekdays?: readonly Weekday[];
172
250
  /** 当てはめる月(省略時はすべての月) */
@@ -176,14 +254,17 @@ export interface HolidayPattern<Kind extends string = string> {
176
254
  readonly interval?: never;
177
255
  readonly anchor?: never;
178
256
  readonly day?: never;
257
+ readonly businessDayOfMonth?: never;
258
+ readonly calendar?: never;
179
259
  readonly dayAfterHoliday?: never;
180
260
  readonly dayBeforeHoliday?: never;
261
+ readonly skipWeekdays?: never;
181
262
  }
182
263
  /**
183
264
  * 繰り返しのパターン。
184
265
  * @public
185
266
  */
186
- export type RecurringPattern<Kind extends string = string> = WeekdayPattern | DayOfMonthPattern | DayAfterHolidayPattern | DayBeforeHolidayPattern | HolidayPattern<Kind>;
267
+ export type RecurringPattern<Kind extends string = string> = WeekdayPattern | DayOfMonthPattern | BusinessDayOfMonthPattern | DayAfterHolidayPattern | DayBeforeHolidayPattern | HolidayPattern<Kind>;
187
268
  /**
188
269
  * 繰り返しの規則(営業日カレンダーの closedRecurring・openRecurring)。
189
270
  * @public
@@ -409,6 +490,14 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
409
490
  readonly anchor: number | null;
410
491
  /** 日の集合(曜日の規則では null) */
411
492
  readonly days: ReadonlySet<number> | null;
493
+ /** 第 n 営業日の集合(第 n 営業日の規則以外では null) */
494
+ readonly businessDays: ReadonlySet<number> | null;
495
+ /** 営業日を数える基準(スケジュールの第 n 営業日の規則の calendar から。null なら自分の設定の「繰り返しの規則を除いた判定」) */
496
+ readonly basis: BasisOfMonth | null;
497
+ /** basis の元のカレンダー(診断で同じ基準かを比べるため。basis が null なら null) */
498
+ readonly calendar: Calendar | null;
499
+ /** "after-holiday" の振替で、祝日に加えて飛ばす曜日(null はなし) */
500
+ readonly skipWeekdays: ReadonlySet<number> | null;
412
501
  /** 祝日の翌日の規則か */
413
502
  readonly dayAfterHoliday: boolean;
414
503
  /** 祝前日の規則か */
@@ -417,6 +506,8 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
417
506
  readonly holiday: boolean;
418
507
  /** 祝日の規則で絞る種別(null はすべての祝日) */
419
508
  readonly kinds: ReadonlySet<string> | null;
509
+ /** 祝日の規則で絞る識別子(null はすべての祝日) */
510
+ readonly keys: ReadonlySet<string> | null;
420
511
  /** 祝日・祝日の翌日・祝前日の規則で絞る曜日(null はすべての曜日) */
421
512
  readonly weekdays: ReadonlySet<number> | null;
422
513
  /** 月の集合(null はすべての月) */
@@ -425,18 +516,31 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
425
516
  readonly value: T;
426
517
  readonly name: string | undefined;
427
518
  }
519
+ /**
520
+ * @internal 営業日を数える基準。年と月を受け取り、その月の基準で営業日である日(1〜31)を日付順に返す。
521
+ * 第 n 営業日の規則(businessDayOfMonth)の判定に使う
522
+ */
523
+ export type BasisOfMonth = (year: number, month: number) => readonly number[];
524
+ /** @internal カレンダーの判定関数から、月ごとに結果をキャッシュする基準を作る */
525
+ export declare function basisFromPredicate(isOpen: (date: DateString) => boolean): BasisOfMonth;
428
526
  /** @internal 繰り返しの規則を検証して取り込む */
429
- 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>[];
527
+ export declare function compileRecurringRules<T>(c: IssueCollector, path: string, entries: unknown, valueOf: (entry: Record<string, unknown>, path: string) => T | undefined, keys: readonly string[], context: "calendar" | "schedule"): RecurringRuleCompiled<T>[];
430
528
  /** @internal その日の祝日を返す関数(祝日・祝日の翌日・祝前日の規則の判定に使う。提供元の対応年の外は祝日でないとみなすこと) */
431
529
  export type HolidayOf = (date: DateString) => Holiday<string> | undefined;
432
530
  /**
433
- * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
531
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・第 n 営業日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
434
532
  * 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
435
- * 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない
533
+ * 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない。
534
+ * 第 n 営業日の規則は、規則自身の基準(スケジュールの calendar)か basis で数え、どちらもなければ当てはまらない
436
535
  */
437
- export declare function matchPattern(rule: RecurringRuleCompiled<unknown>, date: DateString, holidayOf?: HolidayOf): boolean;
536
+ export declare function matchPattern(rule: RecurringRuleCompiled<unknown>, date: DateString, holidayOf?: HolidayOf, basis?: BasisOfMonth): boolean;
438
537
  /** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む) */
439
- export declare function matchRecurring(rule: RecurringRuleCompiled<unknown>, date: DateString, holidayOf: HolidayOf): boolean;
538
+ export declare function matchRecurring(rule: RecurringRuleCompiled<unknown>, date: DateString, holidayOf: HolidayOf, basis?: BasisOfMonth): boolean;
539
+ /**
540
+ * @internal 第 n 営業日の規則が自分の設定を基準にする場合の基準を作る(繰り返しの規則をすべて除いた判定で営業日か)。
541
+ * 基準が要らない設定(第 n 営業日の規則がない、またはすべて calendar を持つ)では undefined
542
+ */
543
+ export declare function createBasis<T, Kind extends string>(compiled: CompiledSchedule<T, Kind>, lookup: HolidayLookupInternal<Kind>): BasisOfMonth | undefined;
440
544
  /** @internal 祝日の提供元とタイムゾーンの設定を検証した結果 */
441
545
  export interface HolidaySource<Kind extends string> {
442
546
  readonly providers: readonly HolidayProvider<Kind>[];