bizdays-toolkit 0.18.0 → 0.20.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.20.0] - 2026-10-25
8
+
9
+ ### 移行
10
+
11
+ 動作が変わる既存の API はない。サブパス `bizdays-toolkit/fr` が増える。版ごとの作業は [docs/migration.md](./docs/migration.md) にまとめた。
12
+
13
+ ### Added
14
+
15
+ - フランスの祝日の提供元 `bizdays-toolkit/fr` を追加した。`metropole`(ID `FR`。労働法典 L3133-1 の 11 日)と `alsaceMoselle`(ID `FR-AM`。Vendredi saint と 12 月 26 日が加わる)で、タイムゾーンは `Europe/Paris`、対応年は 2000〜2099 年。固定日と復活祭からの日数で計算し、振替はない。名称は data.gouv.fr の表記(フランス語)、`nameEn` は英訳、`key` は英語名から(`labour-day`・`pentecost-monday` など 13 個。型 `KnownFrHolidayKey`)。2 つの祝日が同じ日に当たる年(2008 年・2059 年の Ascension)は復活祭の側の名称で 1 件。祝日は法令で定まり年ごとの告示がないので、すべて `confirmed: true` で `confirmedThrough` は持たない。型 `FrHoliday`・`FrHolidayKind`・`KnownFrHolidayKind` を追加した
16
+ - data.gouv.fr の「Jours fériés en France」(DINUM。Métropole と Alsace-Moselle のゾーンの JSON)の 2006〜2031 年のすべての日付と名称に一致することをテストで確かめる(`test/fixtures/datagouv-jours-feries.json`)
17
+
18
+ ### Changed
19
+
20
+ - README: 提供元の表にフランスを足し、min+gzip のサイズの値を 0.20.0 で測り直した(ルート 10.0 → 11.0 KB。0.18.0・0.19.0 の追加分を含む)
21
+ - `docs/guide.md`・`docs/guide.ja.md`: 「French public holidays / フランスの祝日」の節と、識別子の表のフランスの行を足した
22
+ - `RELEASING.md`: 「フランスのデータの更新」の節を足し、区分の表の「規則で計算する提供元」にフランスを含めた
23
+ - `docs/decisions.md`: 48 番(フランスの提供元と、4 か国目の選び方の基準)を追加した
24
+ - `package.json`: `exports` に `./fr`、keywords に `france` を足した
25
+
26
+ ### 開発
27
+
28
+ - 監視のワークフロー `data-watch.yml` に data.gouv.fr の照合を足した(issue のラベル `datagouv-data`。ハートビートは 4 つの照合がすべて動いたときだけ)。`scripts/verify-datagouv.ts` を英国・シンガポール・日本と同じ形で追加した(`pnpm verify:datagouv`)
29
+ - テスト: `test/fr-rules.test.ts`・`test/fr-datagouv.test.ts` を追加し、`test/holiday-key.test.ts`・ESM/CJS/ブラウザー/workerd の確認・利用者の型チェックにフランスを足した
30
+ - サイズの上限(`size-limits.json`)に `./fr` 6,500(測定 5,404)を足した(gzip)
31
+
32
+ ## [0.19.0] - 2026-10-25
33
+
34
+ ### 移行
35
+
36
+ 動作が変わる既存の API はない(`"after-holiday"` の規則に `skipWeekdays` を付けなければ振替先はこれまでどおり)。繰り返しの規則の形 `{ businessDayOfMonth }` と項目 `skipWeekdays`・`calendar`、診断コード `counting-basis` が増える。版ごとの作業は [docs/migration.md](./docs/migration.md) にまとめた。
37
+
38
+ ### Added
39
+
40
+ - 繰り返しの規則に月の第 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`)
41
+ - `"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`
42
+ - 診断コード `counting-basis`(info)を追加した。`businessDayOfMonth` の規則と、同じ設定の毎週の曜日の規則(`{ weekday }`)が同居する設定で、その曜日が営業日として数えられることを知らせる。`businessDayOfMonth` の規則は `never-matches`(その月にない回)・`shadowed`(n・months・基準のカレンダーの包含)・`redundant` の対象。`holidayPolicy: "closed"` の `businessDayOfMonth` の規則の `"over-holiday"` は `redundant`(info)。`skipWeekdays` が違う規則どうしは `shadowed` にしない
43
+
44
+ ### Changed
45
+
46
+ - `docs/guide.md`・`docs/guide.ja.md`: 繰り返しの規則の表と本文に第 n 営業日と `skipWeekdays` を足し、コード例(銀行の支店とスケジュール)を足した
47
+ - README: 規則の表に `businessDayOfMonth`・`skipWeekdays` を足した
48
+ - `docs/decisions.md`: 47 番(第 n 営業日の規則・`skipWeekdays`・監視の報告書の日付)を追加し、14 番に `skipWeekdays` の追記をした
49
+
50
+ ### 開発
51
+
52
+ - 監視の照合スクリプト(`scripts/verify-govuk.ts`・`verify-datagovsg.ts`・`verify-cao.ts`)の「確認した日付」を UTC から日本時間に揃えた(issue のコメントの日付と同じ基準)
53
+ - テスト: `test/business-day-of-month.test.ts`・`test/skip-weekdays.test.ts` を追加し、`test/types.check.ts`・利用者の型チェックに新しい規則を足した
54
+ - サイズの上限(`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` の振替、診断の追加のため
55
+
7
56
  ## [0.18.0] - 2026-10-25
8
57
 
9
58
  ### 移行
@@ -494,7 +543,9 @@
494
543
 
495
544
  - 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
496
545
 
497
- [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.18.0...HEAD
546
+ [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.20.0...HEAD
547
+ [0.20.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.19.0...v0.20.0
548
+ [0.19.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.18.0...v0.19.0
498
549
  [0.18.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.1...v0.18.0
499
550
  [0.17.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.0...v0.17.1
500
551
  [0.17.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.16.0...v0.17.0
package/README.md CHANGED
@@ -19,12 +19,12 @@
19
19
 
20
20
  A business-day calendar that combines public holidays with company or store closures.
21
21
 
22
- - **Holidays verified day by day against official data**: Japan (Cabinet Office, 1955 to the latest year in the Official Gazette), the UK (GOV.UK; England, Wales, Scotland and Northern Ireland, 2012 onwards) and Singapore (Ministry of Manpower on data.gov.sg, 2020 onwards). Every date is checked in the tests, differences are watched daily and shipped as patch versions
22
+ - **Holidays verified day by day against official data**: Japan (Cabinet Office, 1955 to the latest year in the Official Gazette), France (data.gouv.fr; metropolitan France and Alsace-Moselle, verified for 2006–2031), the UK (GOV.UK; England, Wales, Scotland and Northern Ireland, 2012 onwards) and Singapore (Ministry of Manpower on data.gov.sg, 2020 onwards). Every date is checked in the tests, differences are watched daily and shipped as patch versions
23
23
  - **Business-day arithmetic**: closed weekdays, annual closures (across the new year too), specific closed and open dates and holidays combine into "N business days later", "next business day after the cutoff" and month-based due dates with the four adjustment conventions used in finance
24
24
  - **Recurring rules**: "closed on the 3rd Wednesday", "every other Wednesday", "stocktaking on the last day of the month", "closed on Mondays, or the next weekday when Monday is a holiday", "the day after (or before) a holiday", each with its own behaviour on holidays and an optional effective period
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
- - **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
27
+ - **Lightweight, zero dependencies, time-zone safe**: about 11.0 KB min+gzip (about 12.9 KB with Japanese holidays, about 11.9 KB with French holidays, about 12.8 KB with UK bank holidays, about 11.9 KB with Singapore holidays; measured for 0.20.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
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
@@ -60,11 +60,12 @@ The [guide](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/guide.md)
60
60
 
61
61
  ## What you can do
62
62
 
63
- ### Holidays of Japan, the UK and Singapore
63
+ ### Holidays of Japan, France, the UK and Singapore
64
64
 
65
65
  | Provider | Subpath | Time zone | Supported years | Verified against |
66
66
  | ------------------------------------------------- | -------------------- | ---------------- | --------------- | -------------------------------------- |
67
67
  | `jp` | `bizdays-toolkit/jp` | `Asia/Tokyo` | 1955–2099 | Cabinet Office "National Holidays" CSV |
68
+ | `metropole`, `alsaceMoselle` | `bizdays-toolkit/fr` | `Europe/Paris` | 2000–2099 | data.gouv.fr "Jours fériés en France" |
68
69
  | `england`, `wales`, `scotland`, `northernIreland` | `bizdays-toolkit/uk` | `Europe/London` | 2012–2099 | GOV.UK bank holidays |
69
70
  | `sg` | `bizdays-toolkit/sg` | `Asia/Singapore` | 2020–2027 | data.gov.sg (Ministry of Manpower) |
70
71
 
@@ -74,15 +75,17 @@ Each holiday carries its date, name (and English name), `kind` (national, substi
74
75
 
75
76
  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
77
 
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 or by key (`keys: ["vernal-equinox"]`) |
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 |
78
+ | Rule | Meaning |
79
+ | --------------------------------------------------------- | -------------------------------------------------------------------------------- |
80
+ | `{ weekday: 3, nth: 3 }` | The 3rd Wednesday (`nth` is 1–5 or -1 for the last; an array for several) |
81
+ | `{ weekday: 3, interval: 2, anchor: "2026-01-07" }` | Every other Wednesday, counted from the anchor |
82
+ | `{ day: 20 }`, `{ day: -1 }` | The 20th, the last day of the month |
83
+ | `{ businessDayOfMonth: -1 }`, `{ businessDayOfMonth: 5 }` | The last business day of the month, the 5th business day |
84
+ | `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed by kind or by key (`keys: ["vernal-equinox"]`) |
85
+ | `{ dayAfterHoliday: true }`, `{ dayBeforeHoliday: true }` | The day after (or before) a holiday or a run of holidays |
86
+ | `onHolidayRule: "after-holiday"` | "Closed on Mondays, or the next weekday when Monday is a holiday" (museum rule) |
87
+ | `skipWeekdays: [0, 6]` | With `"after-holiday"`: also skip weekends ("June 15, or the next business day") |
88
+ | `effectiveFrom`, `effectiveTo` | The period the rule is in effect |
86
89
 
87
90
  `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
91
 
@@ -126,7 +129,7 @@ A holiday provider is an object with `id`, `timeZone`, `supportedYears` and `hol
126
129
 
127
130
  Versioning follows [Semantic Versioning](https://semver.org/). See [RELEASING.md](https://github.com/inohan77/bizdays-toolkit/blob/main/RELEASING.md) (Japanese) for details.
128
131
 
129
- - Official data updates (new, moved and one-off holidays by law, Official Gazette or proclamation, fixed equinox days in Japan, newly published years on GOV.UK, newly gazetted years and revised dates in Singapore) are released as **patch** versions, so they reach you with either `^` or `~`
132
+ - Official data updates (new, moved and one-off holidays by law, Official Gazette or proclamation, fixed equinox days in Japan, newly published years on GOV.UK, newly gazetted years and revised dates in Singapore, changes to the French Labour Code) are released as **patch** versions, so they reach you with either `^` or `~`
130
133
  - To freeze the holiday results, pin an exact version (for example `"bizdays-toolkit": "0.8.0"`)
131
134
  - The reflected data is shown by the provider's `dataVersion` (for Japan, the year and month of the Official Gazette reflected; for the UK and Singapore, the year and month of the GOV.UK or data.gov.sg data reflected) and `confirmedThrough` (`jp.dataVersion`, `england.confirmedThrough` and so on). The module-level constants `dataVersion` and `confirmedThrough` in `bizdays-toolkit/jp` and `bizdays-toolkit/uk` are deprecated since 0.12.0 and will be removed in 1.0.0
132
135
  - Before 1.0.0, minor versions may contain breaking changes. What you need to change when moving between minor versions is listed in the [migration guide](https://github.com/inohan77/bizdays-toolkit/blob/main/docs/migration.md) (Japanese) and in the "移行" (migration) part of each version in the CHANGELOG
@@ -140,7 +143,9 @@ UK bank holidays are verified with the GOV.UK bank holidays data (<https://www.g
140
143
 
141
144
  Singapore public holidays are verified with the "Singapore Public Holidays" collection published by the Ministry of Manpower on data.gov.sg (<https://data.gov.sg/collections/691/view>), with the Ministry's press releases as the authority where the two differ (the verification data is a test fixture only; not included in the package).
142
145
 
143
- Contains public sector information licensed under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/), and data from data.gov.sg made available under the [Singapore Open Data Licence](https://data.gov.sg/open-data-licence).
146
+ French public holidays are verified with the "Jours fériés en France" dataset published by DINUM on data.gouv.fr (<https://www.data.gouv.fr/datasets/jours-feries-en-france>, JSON exports for the Métropole and Alsace-Moselle zones; the verification data is a test fixture only; not included in the package).
147
+
148
+ Contains public sector information licensed under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/), data from data.gov.sg made available under the [Singapore Open Data Licence](https://data.gov.sg/open-data-licence), and data from data.gouv.fr made available under the [Licence Ouverte / Open Licence 2.0](https://www.etalab.gouv.fr/licence-ouverte-open-licence).
144
149
 
145
150
  ## License
146
151
 
@@ -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,12 +384,41 @@ 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
@@ -397,15 +429,35 @@ function analyze(c, compiled, isCalendar) {
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
  }
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
+ }
405
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) &&
@@ -417,11 +469,17 @@ function analyze(c, compiled, isCalendar) {
417
469
  r.dayBeforeHoliday === rule.dayBeforeHoliday &&
418
470
  setCovers(r.weekdays, rule.weekdays) &&
419
471
  setCovers(r.months, rule.months)
420
- : !r.holiday &&
421
- !r.dayAfterHoliday &&
422
- !r.dayBeforeHoliday &&
423
- intervalCovers(r, rule) &&
424
- 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])));
425
483
  if (k < 0)
426
484
  return;
427
485
  unused.add(rule);
@@ -23,6 +23,7 @@ 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
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,7 +95,16 @@ 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;
@@ -106,16 +121,61 @@ export interface DayOfMonthPattern {
106
121
  readonly day: DayOfMonth | readonly DayOfMonth[];
107
122
  /** 当てはめる月(省略時はすべての月) */
108
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[];
130
+ readonly weekday?: never;
131
+ readonly nth?: never;
132
+ readonly interval?: never;
133
+ readonly anchor?: never;
134
+ readonly businessDayOfMonth?: never;
135
+ readonly calendar?: never;
136
+ readonly dayAfterHoliday?: never;
137
+ readonly dayBeforeHoliday?: never;
138
+ readonly holiday?: never;
139
+ readonly weekdays?: never;
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;
109
167
  readonly weekday?: never;
110
168
  readonly nth?: never;
111
169
  readonly interval?: never;
112
170
  readonly anchor?: never;
171
+ readonly day?: never;
113
172
  readonly dayAfterHoliday?: never;
114
173
  readonly dayBeforeHoliday?: never;
115
174
  readonly holiday?: never;
116
175
  readonly weekdays?: never;
117
176
  readonly kinds?: never;
118
177
  readonly keys?: never;
178
+ readonly skipWeekdays?: never;
119
179
  }
120
180
  /**
121
181
  * 祝日の翌日(祝日が続く場合は連休が明けた日。その日自身は祝日でない)。「祝日」は提供元の祝日だけ。
@@ -133,10 +193,13 @@ export interface DayAfterHolidayPattern {
133
193
  readonly interval?: never;
134
194
  readonly anchor?: never;
135
195
  readonly day?: never;
196
+ readonly businessDayOfMonth?: never;
197
+ readonly calendar?: never;
136
198
  readonly dayBeforeHoliday?: never;
137
199
  readonly holiday?: never;
138
200
  readonly kinds?: never;
139
201
  readonly keys?: never;
202
+ readonly skipWeekdays?: never;
140
203
  }
141
204
  /**
142
205
  * 祝前日(翌日が祝日で、その日自身は祝日でない日。連休なら連休の初日の前日)。「祝日」は提供元の祝日だけ。
@@ -155,10 +218,13 @@ export interface DayBeforeHolidayPattern {
155
218
  readonly interval?: never;
156
219
  readonly anchor?: never;
157
220
  readonly day?: never;
221
+ readonly businessDayOfMonth?: never;
222
+ readonly calendar?: never;
158
223
  readonly dayAfterHoliday?: never;
159
224
  readonly holiday?: never;
160
225
  readonly kinds?: never;
161
226
  readonly keys?: never;
227
+ readonly skipWeekdays?: never;
162
228
  }
163
229
  /**
164
230
  * 祝日(提供元の祝日である日)。kinds で祝日の種別を、keys で個別の祝日を絞れる(省略時はすべての祝日)。
@@ -188,14 +254,17 @@ export interface HolidayPattern<Kind extends string = string> {
188
254
  readonly interval?: never;
189
255
  readonly anchor?: never;
190
256
  readonly day?: never;
257
+ readonly businessDayOfMonth?: never;
258
+ readonly calendar?: never;
191
259
  readonly dayAfterHoliday?: never;
192
260
  readonly dayBeforeHoliday?: never;
261
+ readonly skipWeekdays?: never;
193
262
  }
194
263
  /**
195
264
  * 繰り返しのパターン。
196
265
  * @public
197
266
  */
198
- 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>;
199
268
  /**
200
269
  * 繰り返しの規則(営業日カレンダーの closedRecurring・openRecurring)。
201
270
  * @public
@@ -421,6 +490,14 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
421
490
  readonly anchor: number | null;
422
491
  /** 日の集合(曜日の規則では null) */
423
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;
424
501
  /** 祝日の翌日の規則か */
425
502
  readonly dayAfterHoliday: boolean;
426
503
  /** 祝前日の規則か */
@@ -439,18 +516,31 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
439
516
  readonly value: T;
440
517
  readonly name: string | undefined;
441
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;
442
526
  /** @internal 繰り返しの規則を検証して取り込む */
443
- 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>[];
444
528
  /** @internal その日の祝日を返す関数(祝日・祝日の翌日・祝前日の規則の判定に使う。提供元の対応年の外は祝日でないとみなすこと) */
445
529
  export type HolidayOf = (date: DateString) => Holiday<string> | undefined;
446
530
  /**
447
- * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
531
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・第 n 営業日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
448
532
  * 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
449
- * 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない
533
+ * 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない。
534
+ * 第 n 営業日の規則は、規則自身の基準(スケジュールの calendar)か basis で数え、どちらもなければ当てはまらない
450
535
  */
451
- 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;
452
537
  /** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む) */
453
- 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;
454
544
  /** @internal 祝日の提供元とタイムゾーンの設定を検証した結果 */
455
545
  export interface HolidaySource<Kind extends string> {
456
546
  readonly providers: readonly HolidayProvider<Kind>[];
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * 営業日カレンダー(createCalendar)は、このスケジュールの真偽値版として作られている。
20
20
  */
21
- import { addDays, daysInMonth, fromDayNumber, isValidDate, parseDate, toDayNumber, weekdayOf, } from "./date.js";
21
+ import { addDays, daysInMonth, formatDate, fromDayNumber, isValidDate, parseDate, toDayNumber, weekdayOf, } from "./date.js";
22
22
  import { checkUnknownKeys, IssueCollector, rangeError } from "./errors.js";
23
23
  import { instantToCivil, toCivil, toDateTimeParts, toInstant, validateTimeZone, } from "./input.js";
24
24
  import { HOLIDAY_KEY_PATTERN } from "./types.js";
@@ -176,6 +176,29 @@ export function compileAnnualRules(c, path, entries, valueOf, allowString) {
176
176
  return rules;
177
177
  }
178
178
  const NTH_VALUES = [1, 2, 3, 4, 5, -1];
179
+ const isBusinessDayOfMonth = (v) => Number.isInteger(v) && v !== 0 && v >= -31 && v <= 31;
180
+ /** @internal カレンダーの判定関数から、月ごとに結果をキャッシュする基準を作る */
181
+ export function basisFromPredicate(isOpen) {
182
+ const cache = new Map();
183
+ return (year, month) => {
184
+ const key = year * 12 + month;
185
+ let days = cache.get(key);
186
+ if (days === undefined) {
187
+ const list = [];
188
+ const dim = daysInMonth(year, month);
189
+ for (let d = 1; d <= dim; d++)
190
+ if (isOpen(formatDate(year, month, d)))
191
+ list.push(d);
192
+ days = Object.freeze(list);
193
+ if (cache.size >= 240)
194
+ cache.clear();
195
+ cache.set(key, days);
196
+ }
197
+ return days;
198
+ };
199
+ }
200
+ /** 作成済みのカレンダーか(createCalendar の結果の形。isBusinessDay と timeZone を持つ) */
201
+ const isCalendarLike = (v) => isObject(v) && typeof v["isBusinessDay"] === "function" && typeof v["timeZone"] === "string";
179
202
  const ON_HOLIDAY_RULES = [
180
203
  "under-holiday",
181
204
  "over-holiday",
@@ -193,7 +216,7 @@ function checkSet(c, path, raw, valid, expected) {
193
216
  return new Set(list);
194
217
  }
195
218
  /** @internal 繰り返しの規則を検証して取り込む */
196
- export function compileRecurringRules(c, path, entries, valueOf, keys) {
219
+ export function compileRecurringRules(c, path, entries, valueOf, keys, context) {
197
220
  if (entries === undefined)
198
221
  return [];
199
222
  if (!Array.isArray(entries)) {
@@ -215,13 +238,15 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
215
238
  const hasAfter = "dayAfterHoliday" in entry;
216
239
  const hasBefore = "dayBeforeHoliday" in entry;
217
240
  const hasHoliday = "holiday" in entry;
241
+ const hasBusinessDay = "businessDayOfMonth" in entry;
218
242
  if (Number(hasWeekday) +
219
243
  Number(hasDay) +
244
+ Number(hasBusinessDay) +
220
245
  Number(hasAfter) +
221
246
  Number(hasBefore) +
222
247
  Number(hasHoliday) !==
223
248
  1) {
224
- c.error("invalid-option", p, 'must have exactly one of "weekday", "day", "dayAfterHoliday", "dayBeforeHoliday" or "holiday"');
249
+ c.error("invalid-option", p, 'must have exactly one of "weekday", "day", "businessDayOfMonth", "dayAfterHoliday", "dayBeforeHoliday" or "holiday"');
225
250
  return;
226
251
  }
227
252
  let kinds = null;
@@ -229,6 +254,9 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
229
254
  let weekday = null;
230
255
  let nth = null;
231
256
  let days = null;
257
+ let businessDays = null;
258
+ let basis = null;
259
+ let basisCalendar = null;
232
260
  let interval = null;
233
261
  let anchor = null;
234
262
  const hasInterval = "interval" in entry;
@@ -288,6 +316,32 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
288
316
  else
289
317
  ok = false;
290
318
  }
319
+ else if (hasBusinessDay) {
320
+ const set = checkSet(c, `${p}.businessDayOfMonth`, entry["businessDayOfMonth"], isBusinessDayOfMonth, "1-31 or -1 (last business day) to -31");
321
+ if (set)
322
+ businessDays = set;
323
+ else
324
+ ok = false;
325
+ if (entry["onHolidayRule"] === "after-holiday") {
326
+ c.error("invalid-option", `${p}.onHolidayRule`, '"after-holiday" cannot be used with "businessDayOfMonth" (shifting the nth business day has no meaning; use "under-holiday" or "over-holiday")');
327
+ ok = false;
328
+ }
329
+ if (context === "calendar") {
330
+ if ("calendar" in entry) {
331
+ c.error("invalid-option", `${p}.calendar`, '"calendar" cannot be used in a calendar (the counting basis is the calendar\'s own options without recurring rules)');
332
+ ok = false;
333
+ }
334
+ }
335
+ else if (!isCalendarLike(entry["calendar"])) {
336
+ c.error("invalid-option", `${p}.calendar`, "must be a calendar made with createCalendar (the basis for counting business days; required in a schedule)");
337
+ ok = false;
338
+ }
339
+ else {
340
+ const calendar = entry["calendar"];
341
+ basisCalendar = calendar;
342
+ basis = basisFromPredicate((date) => calendar.isBusinessDay(date));
343
+ }
344
+ }
291
345
  else {
292
346
  const key = hasAfter ? "dayAfterHoliday" : hasBefore ? "dayBeforeHoliday" : "holiday";
293
347
  if (entry[key] !== true) {
@@ -315,6 +369,10 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
315
369
  }
316
370
  }
317
371
  }
372
+ if (!hasBusinessDay && "calendar" in entry) {
373
+ c.error("invalid-option", `${p}.calendar`, '"calendar" can be used only with "businessDayOfMonth"');
374
+ ok = false;
375
+ }
318
376
  if (!hasHoliday && "kinds" in entry) {
319
377
  c.error("invalid-option", `${p}.kinds`, '"kinds" can be used only with "holiday"');
320
378
  ok = false;
@@ -375,6 +433,28 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
375
433
  c.error("invalid-option", `${p}.onHolidayRule`, `must be one of ${ON_HOLIDAY_RULES.map((v) => `"${v}"`).join(", ")}`);
376
434
  ok = false;
377
435
  }
436
+ let skipWeekdays = null;
437
+ if ("skipWeekdays" in entry) {
438
+ if (entry["onHolidayRule"] !== "after-holiday" || !(hasWeekday || hasDay)) {
439
+ c.error("invalid-option", `${p}.skipWeekdays`, '"skipWeekdays" can be used only with onHolidayRule: "after-holiday" on a "weekday" or "day" rule');
440
+ ok = false;
441
+ }
442
+ else if (!Array.isArray(entry["skipWeekdays"])) {
443
+ c.error("invalid-option", `${p}.skipWeekdays`, "must be a non-empty array of weekdays (0 = Sunday to 6 = Saturday)");
444
+ ok = false;
445
+ }
446
+ else {
447
+ const set = checkSet(c, `${p}.skipWeekdays`, entry["skipWeekdays"], isWeekday, "a weekday (0 = Sunday to 6 = Saturday)");
448
+ if (set === undefined)
449
+ ok = false;
450
+ else if (weekday !== null && set.has(weekday)) {
451
+ c.error("invalid-option", `${p}.skipWeekdays`, `cannot include the rule's own weekday ${weekday} (the rule would be shifted every time)`);
452
+ ok = false;
453
+ }
454
+ else
455
+ skipWeekdays = set;
456
+ }
457
+ }
378
458
  const period = checkPeriod(c, p, entry);
379
459
  if (ok && period && value !== undefined) {
380
460
  rules.push(Object.freeze({
@@ -384,6 +464,10 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
384
464
  interval,
385
465
  anchor,
386
466
  days,
467
+ businessDays,
468
+ basis,
469
+ calendar: basisCalendar,
470
+ skipWeekdays,
387
471
  dayAfterHoliday: hasAfter,
388
472
  dayBeforeHoliday: hasBefore,
389
473
  holiday: hasHoliday,
@@ -401,14 +485,26 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
401
485
  return rules;
402
486
  }
403
487
  /**
404
- * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
488
+ * @internal 繰り返しの規則の形(曜日・第 n 回目・周期・日・第 n 営業日・祝日・祝日の翌日・祝前日・月)がその日に当てはまるか。
405
489
  * 有効期間は考えない(規則の診断で、形どうしの包含を調べるために分けている)。
406
- * 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない
490
+ * 祝日に従属する規則は holidayOf で判定し、holidayOf を渡さなければ当てはまらない。
491
+ * 第 n 営業日の規則は、規則自身の基準(スケジュールの calendar)か basis で数え、どちらもなければ当てはまらない
407
492
  */
408
- export function matchPattern(rule, date, holidayOf) {
493
+ export function matchPattern(rule, date, holidayOf, basis) {
409
494
  const { year, month, day } = parseDate(date);
410
495
  if (rule.months && !rule.months.has(month))
411
496
  return false;
497
+ if (rule.businessDays) {
498
+ // 第 n 営業日: 規則自身の基準(スケジュールの calendar)か、呼び出し側の基準(カレンダーの「繰り返しの規則を除いた判定」)で数える
499
+ const b = rule.basis ?? basis;
500
+ if (b === undefined)
501
+ return false;
502
+ const days = b(year, month);
503
+ const index = days.indexOf(day);
504
+ if (index < 0)
505
+ return false;
506
+ return rule.businessDays.has(index + 1) || rule.businessDays.has(index - days.length);
507
+ }
412
508
  if (rule.holiday || rule.dayAfterHoliday || rule.dayBeforeHoliday) {
413
509
  if (holidayOf === undefined)
414
510
  return false;
@@ -438,8 +534,18 @@ export function matchPattern(rule, date, holidayOf) {
438
534
  return rule.days.has(day) || (rule.days.has(-1) && day === dim);
439
535
  }
440
536
  /** @internal 繰り返しの規則がその日に当てはまるか(有効期間を含む) */
441
- export function matchRecurring(rule, date, holidayOf) {
442
- return inPeriod(rule, toDayNumber(date)) && matchPattern(rule, date, holidayOf);
537
+ export function matchRecurring(rule, date, holidayOf, basis) {
538
+ return inPeriod(rule, toDayNumber(date)) && matchPattern(rule, date, holidayOf, basis);
539
+ }
540
+ /**
541
+ * @internal 第 n 営業日の規則が自分の設定を基準にする場合の基準を作る(繰り返しの規則をすべて除いた判定で営業日か)。
542
+ * 基準が要らない設定(第 n 営業日の規則がない、またはすべて calendar を持つ)では undefined
543
+ */
544
+ export function createBasis(compiled, lookup) {
545
+ if (!compiled.recurring.some((r) => r.businessDays !== null && r.basis === null))
546
+ return undefined;
547
+ const resolve = createResolver({ ...compiled, recurring: [] }, lookup);
548
+ return basisFromPredicate((date) => resolve(date).day.value === true);
443
549
  }
444
550
  /** 規則の value を取り出す(value が必須の規則用) */
445
551
  function requiredValue(c) {
@@ -705,6 +811,9 @@ export const RECURRING_RULE_KEYS = [
705
811
  "keys",
706
812
  "weekdays",
707
813
  "months",
814
+ "businessDayOfMonth",
815
+ "calendar",
816
+ "skipWeekdays",
708
817
  "name",
709
818
  "onHolidayRule",
710
819
  ...PERIOD_KEYS,
@@ -744,7 +853,17 @@ export function compileScheduleOptions(c, options) {
744
853
  }
745
854
  const annual = compileAnnualRules(c, "annual", options["annual"], requiredValue(c), false);
746
855
  const dates = compileDateRules(c, "dates", options["dates"], requiredValue(c), false);
747
- const recurring = compileRecurringRules(c, "recurring", options["recurring"], requiredValue(c), [...RECURRING_RULE_KEYS, "value"]);
856
+ const recurring = compileRecurringRules(c, "recurring", options["recurring"], requiredValue(c), [...RECURRING_RULE_KEYS, "value"], "schedule");
857
+ // 第 n 営業日の規則の基準のカレンダーは、スケジュールと同じタイムゾーンであること(合成と同じ理由。瞬間の暦日が食い違うため)
858
+ for (const rule of recurring) {
859
+ if (rule.businessDays === null)
860
+ continue;
861
+ const entry = options["recurring"][Number(/\[(\d+)\]$/.exec(rule.path)[1])];
862
+ const tz = entry["calendar"].timeZone;
863
+ if (tz !== source.timeZone) {
864
+ c.error("time-zone-conflict", `${rule.path}.calendar`, `the calendar's time zone ${tz} differs from the schedule's ${source.timeZone}`);
865
+ }
866
+ }
748
867
  return { source, weekly, holiday, annual, dates, recurring };
749
868
  }
750
869
  /** 振替元をさかのぼる最大日数(祝日が続く日数の上限。2019 年の大型連休でも 10 日) */
@@ -766,6 +885,7 @@ export function matchAnnual(rule, date) {
766
885
  export function createResolver(compiled, lookup) {
767
886
  const { weekly, holiday: holidayRule, annual, dates, recurring } = compiled;
768
887
  const hasShift = recurring.some((r) => r.onHolidayRule === "after-holiday");
888
+ const basis = createBasis(compiled, lookup);
769
889
  /** 振替・祝日の規則の判断に使う祝日。提供元の対応年の外は祝日でないとみなす(振替元をさかのぼる途中で範囲を出る場合のため) */
770
890
  function holidayOfSafe(date) {
771
891
  try {
@@ -786,7 +906,8 @@ export function createResolver(compiled, lookup) {
786
906
  function shiftedOrigin(rule, date) {
787
907
  for (let k = 1; k <= MAX_HOLIDAY_RUN; k++) {
788
908
  const prev = addDays(date, -k);
789
- if (!isHolidaySafe(prev))
909
+ // 飛ばす日は祝日と、skipWeekdays の曜日。それ以外の日に達したら、振替元はない
910
+ if (!isHolidaySafe(prev) && !rule.skipWeekdays?.has(weekdayOf(prev)))
790
911
  return undefined;
791
912
  if (matchRecurring(rule, prev, holidayOfSafe))
792
913
  return prev;
@@ -823,7 +944,7 @@ export function createResolver(compiled, lookup) {
823
944
  return make(byAnnual.value, "annual", byAnnual, byAnnual.name);
824
945
  if (holiday) {
825
946
  // 3. 祝日の上("over-holiday"。祝日の規則 { holiday: true } もここ)
826
- const over = recurring.find((r) => r.onHolidayRule === "over-holiday" && matchRecurring(r, date, holidayOfSafe));
947
+ const over = recurring.find((r) => r.onHolidayRule === "over-holiday" && matchRecurring(r, date, holidayOfSafe, basis));
827
948
  if (over)
828
949
  return make(over.value, "recurring", over, over.name);
829
950
  // 4. 祝日
@@ -833,14 +954,17 @@ export function createResolver(compiled, lookup) {
833
954
  }
834
955
  // 祝日の扱いが「曜日どおり」なら、祝日の下("under-holiday")の規則も当てはまる。
835
956
  // "after-holiday" の規則は祝日には当てはめない(振り替える)
836
- const under = recurring.find((r) => r.onHolidayRule === "under-holiday" && matchRecurring(r, date, holidayOfSafe));
957
+ const under = recurring.find((r) => r.onHolidayRule === "under-holiday" && matchRecurring(r, date, holidayOfSafe, basis));
837
958
  if (under)
838
959
  return make(under.value, "recurring", under, under.name);
839
960
  return make(weekly[weekday], "weekday", null, undefined);
840
961
  }
841
962
  // 5. 繰り返しの規則(祝日でない日。配列で先に書いたものが優先)。振替先も同じ段階で判定する
842
963
  for (const rule of recurring) {
843
- if (matchRecurring(rule, date, holidayOfSafe))
964
+ // skipWeekdays の曜日には当てはめない(祝日と同じく振り替える)
965
+ if (rule.skipWeekdays?.has(weekday))
966
+ continue;
967
+ if (matchRecurring(rule, date, holidayOfSafe, basis))
844
968
  return make(rule.value, "recurring", rule, rule.name);
845
969
  if (hasShift && rule.onHolidayRule === "after-holiday") {
846
970
  const origin = shiftedOrigin(rule, date);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * フランスの祝日(jours fériés)。本土(metropole)と Alsace-Moselle の 2 つの提供元。
3
+ *
4
+ * 労働法典 L3133-1 が定める 11 日(固定日 8 日と復活祭から決まる 3 日)を規則で計算し、Alsace-Moselle には地方法の
5
+ * Vendredi saint と 12 月 26 日が加わる。振替はない。data.gouv.fr の「Jours fériés en France」(DINUM)の JSON と全日照合している。
6
+ *
7
+ * @example
8
+ * import { createCalendar } from "bizdays-toolkit";
9
+ * import { metropole } from "bizdays-toolkit/fr";
10
+ * const paris = createCalendar({ holidays: metropole, closedWeekdays: [0, 6] });
11
+ *
12
+ * @example Lundi de Pentecôte を「連帯の日」として出勤日にする企業(法律上は祝日のまま。既定の扱いの例外として書く)
13
+ * createCalendar({ holidays: metropole, closedWeekdays: [0, 6],
14
+ * openRecurring: [{ holiday: true, keys: ["pentecost-monday"], name: "Journée de solidarité" }] });
15
+ */
16
+ import type { Holiday, HolidayProvider } from "../core/types.ts";
17
+ import { type FrHolidayKind } from "./rules.ts";
18
+ export type { FrHolidayKind, KnownFrHolidayKey, KnownFrHolidayKind } from "./rules.ts";
19
+ /**
20
+ * フランスの祝日。
21
+ * @public
22
+ */
23
+ export type FrHoliday = Holiday<FrHolidayKind>;
24
+ /**
25
+ * フランス本土(および data.gouv.fr の「Métropole」ゾーンに従う地域)の祝日の提供元。ID は FR、タイムゾーンは Europe/Paris。
26
+ * 対応年は 2000〜2099 年で、祝日は法令で定まり年ごとの告示はないので、すべて confirmed: true(confirmedThrough は持たない)。
27
+ * @public
28
+ */
29
+ export declare const metropole: HolidayProvider<FrHolidayKind>;
30
+ /**
31
+ * Alsace-Moselle(Bas-Rhin・Haut-Rhin・Moselle の 3 県。ISO 3166-2 に 1 つのコードがないので、このライブラリの ID は FR-AM)の
32
+ * 祝日の提供元。本土の 11 日に Vendredi saint(kind "regular"、key "good-friday")と 12 月 26 日("saint-stephens")が加わる。
33
+ * @public
34
+ */
35
+ export declare const alsaceMoselle: HolidayProvider<FrHolidayKind>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * フランスの祝日(jours fériés)。本土(metropole)と Alsace-Moselle の 2 つの提供元。
3
+ *
4
+ * 労働法典 L3133-1 が定める 11 日(固定日 8 日と復活祭から決まる 3 日)を規則で計算し、Alsace-Moselle には地方法の
5
+ * Vendredi saint と 12 月 26 日が加わる。振替はない。data.gouv.fr の「Jours fériés en France」(DINUM)の JSON と全日照合している。
6
+ *
7
+ * @example
8
+ * import { createCalendar } from "bizdays-toolkit";
9
+ * import { metropole } from "bizdays-toolkit/fr";
10
+ * const paris = createCalendar({ holidays: metropole, closedWeekdays: [0, 6] });
11
+ *
12
+ * @example Lundi de Pentecôte を「連帯の日」として出勤日にする企業(法律上は祝日のまま。既定の扱いの例外として書く)
13
+ * createCalendar({ holidays: metropole, closedWeekdays: [0, 6],
14
+ * openRecurring: [{ holiday: true, keys: ["pentecost-monday"], name: "Journée de solidarité" }] });
15
+ */
16
+ import { DATA_VERSION, SUPPORTED_YEARS, computeFrHolidays, } from "./rules.js";
17
+ function provider(id, zone) {
18
+ const cache = new Map();
19
+ return Object.freeze({
20
+ id,
21
+ timeZone: "Europe/Paris",
22
+ supportedYears: SUPPORTED_YEARS,
23
+ dataVersion: DATA_VERSION,
24
+ holidaysInYear(year) {
25
+ let list = cache.get(year);
26
+ if (!list) {
27
+ list = Object.freeze(computeFrHolidays(zone, year));
28
+ cache.set(year, list);
29
+ }
30
+ return list;
31
+ },
32
+ });
33
+ }
34
+ /**
35
+ * フランス本土(および data.gouv.fr の「Métropole」ゾーンに従う地域)の祝日の提供元。ID は FR、タイムゾーンは Europe/Paris。
36
+ * 対応年は 2000〜2099 年で、祝日は法令で定まり年ごとの告示はないので、すべて confirmed: true(confirmedThrough は持たない)。
37
+ * @public
38
+ */
39
+ export const metropole = provider("FR", "metropole");
40
+ /**
41
+ * Alsace-Moselle(Bas-Rhin・Haut-Rhin・Moselle の 3 県。ISO 3166-2 に 1 つのコードがないので、このライブラリの ID は FR-AM)の
42
+ * 祝日の提供元。本土の 11 日に Vendredi saint(kind "regular"、key "good-friday")と 12 月 26 日("saint-stephens")が加わる。
43
+ * @public
44
+ */
45
+ export const alsaceMoselle = provider("FR-AM", "alsace-moselle");
@@ -0,0 +1,47 @@
1
+ /**
2
+ * フランスの祝日(jours fériés。労働法典 L3133-1 の 11 日と、Alsace-Moselle の 2 日)の計算。
3
+ *
4
+ * - 固定日の祝日(1/1・5/1・5/8・7/14・8/15・11/1・11/11・12/25。Alsace-Moselle は 12/26 も)と、
5
+ * 復活祭から決まる祝日(Lundi de Pâques = +1 日、Ascension = +39 日、Lundi de Pentecôte = +50 日。
6
+ * Alsace-Moselle は Vendredi saint = −2 日も)を規則で計算する。振替はない
7
+ * - 2 つの祝日が同じ日に当たる年(復活祭が 3 月 23 日なら 5 月 1 日 = Ascension(2008 年)、3 月 30 日なら 5 月 8 日 = Ascension(2059 年))は、
8
+ * 復活祭から決まる祝日の名称・識別子で 1 件にする
9
+ * (data.gouv.fr の JSON と同じ扱い。提供元は 1 日に 1 件を返す契約のため)
10
+ * - 名称は data.gouv.fr の表記(フランス語)。nameEn はライブラリの英訳
11
+ * - 識別子(key)は英語名から 1 つずつ(KnownFrHolidayKey)
12
+ *
13
+ * 対応年のうち data.gouv.fr が公開している範囲(過去 20 年・未来 5 年)のすべての日付と名称が一致することを、
14
+ * テスト用フィクスチャ(test/fixtures/datagouv-jours-feries.json)で検証している。祝日は法令で定まり年ごとの告示はないので、
15
+ * 対応年はすべて confirmed: true(法改正があれば労働法典の改正として反映する)。
16
+ */
17
+ import type { Holiday } from "../core/types.ts";
18
+ /**
19
+ * フランスの祝日の種別(既知の値)。
20
+ * @public
21
+ */
22
+ export type KnownFrHolidayKind =
23
+ /** 労働法典・地方法で定まる祝日(振替・臨時の祝日はない) */
24
+ "regular";
25
+ /**
26
+ * フランスの祝日の種別。将来の版で値が増えることがあるので、分岐では「その他」も扱うこと。
27
+ * すべての値を網羅したことを型で確かめたい場合は KnownFrHolidayKind を使う。
28
+ * @public
29
+ */
30
+ export type FrHolidayKind = KnownFrHolidayKind | (string & {});
31
+ /**
32
+ * フランスの祝日の識別子(Holiday.key。既知の値)。英語名から 1 つずつ。
33
+ * 規則の keys に書くときの補完に使う(型は開いているので他の文字列も通る)。
34
+ * @beta
35
+ */
36
+ export type KnownFrHolidayKey = "new-year" | "easter-monday" | "labour-day" | "victory-in-europe" | "ascension" | "pentecost-monday" | "bastille" | "assumption" | "all-saints" | "armistice" | "christmas" | "good-friday" | "saint-stephens";
37
+ /** @internal 地域(data.gouv.fr のゾーン) */
38
+ export type FrZone = "metropole" | "alsace-moselle";
39
+ /** @internal 対応年 */
40
+ export declare const SUPPORTED_YEARS: Readonly<{
41
+ from: 2000;
42
+ to: 2099;
43
+ }>;
44
+ /** @internal 反映済みの data.gouv.fr のデータの年月(2006〜2031 年の公開) */
45
+ export declare const DATA_VERSION = "2026-10";
46
+ /** @internal その年・地域の祝日(日付順) */
47
+ export declare function computeFrHolidays(zone: FrZone, year: number): Holiday<FrHolidayKind>[];
@@ -0,0 +1,77 @@
1
+ /**
2
+ * フランスの祝日(jours fériés。労働法典 L3133-1 の 11 日と、Alsace-Moselle の 2 日)の計算。
3
+ *
4
+ * - 固定日の祝日(1/1・5/1・5/8・7/14・8/15・11/1・11/11・12/25。Alsace-Moselle は 12/26 も)と、
5
+ * 復活祭から決まる祝日(Lundi de Pâques = +1 日、Ascension = +39 日、Lundi de Pentecôte = +50 日。
6
+ * Alsace-Moselle は Vendredi saint = −2 日も)を規則で計算する。振替はない
7
+ * - 2 つの祝日が同じ日に当たる年(復活祭が 3 月 23 日なら 5 月 1 日 = Ascension(2008 年)、3 月 30 日なら 5 月 8 日 = Ascension(2059 年))は、
8
+ * 復活祭から決まる祝日の名称・識別子で 1 件にする
9
+ * (data.gouv.fr の JSON と同じ扱い。提供元は 1 日に 1 件を返す契約のため)
10
+ * - 名称は data.gouv.fr の表記(フランス語)。nameEn はライブラリの英訳
11
+ * - 識別子(key)は英語名から 1 つずつ(KnownFrHolidayKey)
12
+ *
13
+ * 対応年のうち data.gouv.fr が公開している範囲(過去 20 年・未来 5 年)のすべての日付と名称が一致することを、
14
+ * テスト用フィクスチャ(test/fixtures/datagouv-jours-feries.json)で検証している。祝日は法令で定まり年ごとの告示はないので、
15
+ * 対応年はすべて confirmed: true(法改正があれば労働法典の改正として反映する)。
16
+ */
17
+ import { addDays, easterSunday, formatDate } from "../core/date.js";
18
+ import { rangeError } from "../core/errors.js";
19
+ /** @internal 対応年 */
20
+ export const SUPPORTED_YEARS = Object.freeze({ from: 2000, to: 2099 });
21
+ /** @internal 反映済みの data.gouv.fr のデータの年月(2006〜2031 年の公開) */
22
+ export const DATA_VERSION = "2026-10";
23
+ /** 固定日の祝日 [月, 日, 名称, 英語名, 識別子] */
24
+ const FIXED = [
25
+ [1, 1, "1er janvier", "New Year’s Day", "new-year"],
26
+ [5, 1, "1er mai", "Labour Day", "labour-day"],
27
+ [5, 8, "8 mai", "Victory in Europe Day", "victory-in-europe"],
28
+ [7, 14, "14 juillet", "Bastille Day", "bastille"],
29
+ [8, 15, "Assomption", "Assumption", "assumption"],
30
+ [11, 1, "Toussaint", "All Saints’ Day", "all-saints"],
31
+ [11, 11, "11 novembre", "Armistice Day", "armistice"],
32
+ [12, 25, "Jour de Noël", "Christmas Day", "christmas"],
33
+ ];
34
+ const FIXED_ALSACE_MOSELLE = [[12, 26, "2ème jour de Noël", "Saint Stephen’s Day", "saint-stephens"]];
35
+ /** 復活祭から決まる祝日 [日数差, 名称, 英語名, 識別子] */
36
+ const EASTER_BASED = [
37
+ [1, "Lundi de Pâques", "Easter Monday", "easter-monday"],
38
+ [39, "Ascension", "Ascension Day", "ascension"],
39
+ [50, "Lundi de Pentecôte", "Pentecost Monday", "pentecost-monday"],
40
+ ];
41
+ const EASTER_BASED_ALSACE_MOSELLE = [[-2, "Vendredi saint", "Good Friday", "good-friday"]];
42
+ /** @internal その年・地域の祝日(日付順) */
43
+ export function computeFrHolidays(zone, year) {
44
+ if (!Number.isInteger(year) || year < SUPPORTED_YEARS.from || year > SUPPORTED_YEARS.to) {
45
+ throw rangeError("out-of-range", `French public holidays are supported for ${SUPPORTED_YEARS.from}-${SUPPORTED_YEARS.to}; requested ${year}`);
46
+ }
47
+ const alsaceMoselle = zone === "alsace-moselle";
48
+ const byDate = new Map();
49
+ for (const [month, day, name, nameEn, key] of alsaceMoselle
50
+ ? [...FIXED, ...FIXED_ALSACE_MOSELLE]
51
+ : FIXED) {
52
+ byDate.set(formatDate(year, month, day), {
53
+ date: formatDate(year, month, day),
54
+ name,
55
+ nameEn,
56
+ key,
57
+ });
58
+ }
59
+ // 復活祭から決まる祝日が固定日と同じ日に当たる年は、復活祭の側で上書きする(data.gouv.fr の JSON と同じ)
60
+ const easter = easterSunday(year);
61
+ for (const [offset, name, nameEn, key] of alsaceMoselle
62
+ ? [...EASTER_BASED_ALSACE_MOSELLE, ...EASTER_BASED]
63
+ : EASTER_BASED) {
64
+ const date = addDays(easter, offset);
65
+ byDate.set(date, { date, name, nameEn, key });
66
+ }
67
+ return [...byDate.values()]
68
+ .sort((a, b) => (a.date < b.date ? -1 : 1))
69
+ .map((e) => Object.freeze({
70
+ date: e.date,
71
+ name: e.name,
72
+ nameEn: e.nameEn,
73
+ kind: "regular",
74
+ confirmed: true,
75
+ key: e.key,
76
+ }));
77
+ }
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * 公開する名前と型は etc/api.md に記録し、pnpm check で意図しない変化を検出する。
9
9
  */
10
10
  export { combineCalendars, createCalendar, createCalendarFromSchedule, type AdjustOptions, type AnnualDate, type AnnualRange, type BusinessDayConvention, type BusinessDayOptions, type Calendar, type CalendarOptions, type CombineOptions, type DateRange, type DayInfo, type DayReason, type DueDateOptions, type HolidayPolicy, type KnownDayReason, type SpecificDate, } from "./core/calendar.ts";
11
- export { createSchedule, type AnnualRangeValue, type AnnualValue, type CivilDateTimeParts, type DateRangeValue, type DateValue, type DayAfterHolidayPattern, type DayBeforeHolidayPattern, type DayOfMonth, type DayOfMonthPattern, type EffectivePeriod, type HolidayLookup, type HolidayPattern, type HolidaySourceOptions, type HolidayValueRule, type KnownScheduleReason, type Month, type Nth, type OnHolidayRule, type RecurringPattern, type RecurringRule, type RecurringValue, type Schedule, type ScheduleDay, type ScheduleOptions, type ScheduleReason, type WeekdayPattern, } from "./core/schedule.ts";
11
+ export { createSchedule, type AnnualRangeValue, type AnnualValue, type BusinessDayOfMonth, type BusinessDayOfMonthPattern, type CivilDateTimeParts, type DateRangeValue, type DateValue, type DayAfterHolidayPattern, type DayBeforeHolidayPattern, type DayOfMonth, type DayOfMonthPattern, type EffectivePeriod, type HolidayLookup, type HolidayPattern, type HolidaySourceOptions, type HolidayValueRule, type KnownScheduleReason, type Month, type Nth, type OnHolidayRule, type RecurringPattern, type RecurringRule, type RecurringValue, type Schedule, type ScheduleDay, type ScheduleOptions, type ScheduleReason, type WeekdayPattern, } from "./core/schedule.ts";
12
12
  export { addDays, diffDays, isValidDate, weekdayOf, type DateString, type Weekday, } from "./core/date.ts";
13
13
  export { closingMonth, closingPeriod, monthOf, type MonthString } from "./core/month.ts";
14
14
  export { isBizdaysError, type ErrorCode, type KnownErrorCode } from "./core/errors.ts";
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "bizdays-toolkit",
3
- "version": "0.18.0",
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; Singapore public holidays verified against data.gov.sg. / 祝日と営業日のカレンダー(タイムゾーン対応・日本の祝日は内閣府、英国の bank holidays は GOV.UK、シンガポールの祝日は data.gov.sg のデータで全日照合)",
3
+ "version": "0.20.0",
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; Singapore public holidays verified against data.gov.sg; French public holidays (metropole, Alsace-Moselle) verified against data.gouv.fr. / 祝日と営業日のカレンダー(タイムゾーン対応・日本の祝日は内閣府、英国の bank holidays は GOV.UK、シンガポールの祝日は data.gov.sg、フランスの祝日は data.gouv.fr のデータで全日照合)",
5
5
  "keywords": [
6
6
  "bank-holidays",
7
7
  "bizdays",
8
8
  "business-day",
9
9
  "business-days",
10
10
  "calendar",
11
+ "france",
11
12
  "holiday",
12
13
  "holidays",
13
14
  "japan",
@@ -51,6 +52,10 @@
51
52
  "types": "./dist/sg/index.d.ts",
52
53
  "default": "./dist/sg/index.js"
53
54
  },
55
+ "./fr": {
56
+ "types": "./dist/fr/index.d.ts",
57
+ "default": "./dist/fr/index.js"
58
+ },
54
59
  "./diagnose": {
55
60
  "types": "./dist/diagnose/index.d.ts",
56
61
  "default": "./dist/diagnose/index.js"
@@ -75,6 +80,7 @@
75
80
  "verify:cao": "node scripts/verify-cao.ts",
76
81
  "verify:govuk": "node scripts/verify-govuk.ts",
77
82
  "verify:datagovsg": "node scripts/verify-datagovsg.ts",
83
+ "verify:datagouv": "node scripts/verify-datagouv.ts",
78
84
  "prepublishOnly": "pnpm check",
79
85
  "release-notes": "node scripts/release-notes.ts",
80
86
  "api:update": "pnpm build && node scripts/api-report.ts --update"