bizdays-toolkit 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.18.0] - 2026-10-25
8
+
9
+ ### 移行
10
+
11
+ 動作が変わる既存の API はない。同梱の提供元が返す祝日に省略可能な項目 `key` が加わるので、`dayInfo` の結果や `holidaysInYear` の要素を `deepEqual` で比べているテストは期待値に足す(国民の休日・臨時の祝日には付かない)。祝日の規則に省略可能な `keys` が加わる。版ごとの作業は [docs/migration.md](./docs/migration.md) にまとめた。
12
+
13
+ ### Added
14
+
15
+ - 祝日の識別子 `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` で扱う
16
+ - `bizdays-toolkit/provider` に、週末に当たった祝日の振替日を計算する `substituteDays(holidays, { rules, kind, name?, original? })` を追加した(`@beta`。型 `SubstituteDaysOptions`・`SubstituteDirection`)。`rules` は曜日ごとの向き(`"next"`・`"previous"`)で、振替先は「入力の祝日でも、決めた振替日でも、`rules` の曜日でもない最初の日」。振替日は元の祝日の `key`・`names`・`confirmed` を引き継ぐ。日本(2007 年以降)・英国・シンガポールの同梱の提供元の振替日を再現することをテストで確かめている(内部の実装は置き換えていない)
17
+
18
+ ### Changed
19
+
20
+ - `docs/guide.md`・`docs/guide.ja.md`: 「Holiday keys」(祝日の識別子)の節と、繰り返しの規則の `keys` を足した。コード例の結果に `key` が増えた
21
+ - `docs/providers.md`・`docs/providers.ja.md`: `substituteDays` と、自作の提供元に `key` を付ける方法を足した
22
+ - README: 規則の表と機能の箇条書きに `key`・振替の計算を足し、クイックスタートのコメントの "Labour Thanksgiving Day" を `nameEn` の綴り "Labor Thanksgiving Day" に直した
23
+ - `RELEASING.md`: 区分の表で「対応年の拡大」を規則で計算する提供元(日本・英国)に限り、年ごとの表で持つ提供元(シンガポール)に告示済みの新しい年を足すことはパッチに置く、と線を引いた(これまでは 2 つの行が同時に当てはまる状態だった)。「毎年の作業(日本)」を「日本のデータの更新」に改め、監視の差分の種類と対応の表を足した
24
+ - `docs/decisions.md`: 46 番(祝日の識別子と絞り込み、`substituteDays`、内閣府 CSV の毎日の照合、区分の線引き)を追加し、37 番の理由(存在しない「将来の非純粋な判定関数」を前提にしていた)と 44 番の `asOf` の理由(`Date` を断る理由としてタイムゾーンは成り立たなかった)を改めた
25
+
26
+ ### 開発
27
+
28
+ - 監視のワークフロー `data-watch.yml` に内閣府「国民の祝日」CSV の照合を足した(issue のラベル `cao-data`。ハートビートは 3 つの照合がすべて動いたときだけ)。`scripts/verify-cao.ts` を英国・シンガポールと同じ形(`--report`・`--simulate`、終了コード 0・1・2、取得の再試行、「New years published」・フィクスチャの差分の節)にした。翌年の春分・秋分の日の公表が issue で届くので、2 月の作業を記憶に頼らない
29
+ - テスト: `test/holiday-key.test.ts` を追加し、`test/provider.test.ts` に `substituteDays`(同梱の 3 か国との同値・向き・名称・入力の検証)を足した。`test/types.check.ts`・利用者の型チェックに `keys`・既知の `key` の型・`substituteDays` を足した
30
+ - サイズの上限(`size-limits.json`)を上げた。`./provider` 4,500 → 6,500(`substituteDays` と入力の検証のため。測定 6,019)、`./diagnose` 32,000 → 33,000(`keys` の判定と診断のため。測定 31,645)(gzip)
31
+
7
32
  ## [0.17.1] - 2026-10-25
8
33
 
9
34
  ### Changed
@@ -469,7 +494,8 @@
469
494
 
470
495
  - 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
471
496
 
472
- [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.1...HEAD
497
+ [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.18.0...HEAD
498
+ [0.18.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.1...v0.18.0
473
499
  [0.17.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.17.0...v0.17.1
474
500
  [0.17.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.16.0...v0.17.0
475
501
  [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,
@@ -79,12 +79,12 @@ A calendar is built from the holiday provider and your closures: `closedWeekdays
79
79
  | `{ weekday: 3, nth: 3 }` | The 3rd Wednesday (`nth` is 1–5 or -1 for the last; an array for several) |
80
80
  | `{ weekday: 3, interval: 2, anchor: "2026-01-07" }` | Every other Wednesday, counted from the anchor |
81
81
  | `{ day: 20 }`, `{ day: -1 }` | The 20th, the last day of the month |
82
- | `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed by kind |
82
+ | `{ holiday: true, kinds: ["substitute"] }` | Holidays, optionally narrowed by kind or by key (`keys: ["vernal-equinox"]`) |
83
83
  | `{ dayAfterHoliday: true }`, `{ dayBeforeHoliday: true }` | The day after (or before) a holiday or a run of holidays |
84
84
  | `onHolidayRule: "after-holiday"` | "Closed on Mondays, or the next weekday when Monday is a holiday" (museum rule) |
85
85
  | `effectiveFrom`, `effectiveTo` | The period the rule is in effect |
86
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).
87
+ `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
88
 
89
89
  ### Due dates, schedules and combined calendars
90
90
 
@@ -393,7 +393,7 @@ function analyze(c, compiled, isCalendar) {
393
393
  ? source.providers.length === 0
394
394
  ? "This rule matches no day: there are no holidays without a holiday provider"
395
395
  : rule.holiday
396
- ? "This rule matches no day: no holiday of the selected kinds, weekdays and months within its effective period"
396
+ ? "This rule matches no day: no holiday of the selected kinds, keys, weekdays and months within its effective period"
397
397
  : rule.dayAfterHoliday
398
398
  ? "This rule matches no day: no day after a holiday on the selected weekdays and months within its effective period"
399
399
  : "This rule matches no day: no day before a holiday on the selected weekdays and months within its effective period"
@@ -402,13 +402,14 @@ function analyze(c, compiled, isCalendar) {
402
402
  : "This rule matches no day within its effective period");
403
403
  return;
404
404
  }
405
- // 祝日・祝日の翌日・祝前日の規則は祝日次第で年ごとに変わるので、形の包含は同じ形の規則どうし(種別・曜日・月の絞り込み)だけで調べる
405
+ // 祝日・祝日の翌日・祝前日の規則は祝日次第で年ごとに変わるので、形の包含は同じ形の規則どうし(種別・識別子・曜日・月の絞り込み)だけで調べる
406
406
  const k = recurring.findIndex((r, i) => i < j &&
407
407
  canCover(r, rule) &&
408
408
  periodCovers(r, rule) &&
409
409
  (rule.holiday
410
410
  ? r.holiday &&
411
411
  setCovers(r.kinds, rule.kinds) &&
412
+ setCovers(r.keys, rule.keys) &&
412
413
  setCovers(r.weekdays, rule.weekdays) &&
413
414
  setCovers(r.months, rule.months)
414
415
  : rule.dayAfterHoliday || rule.dayBeforeHoliday
@@ -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,7 @@ 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
26
  /**
27
27
  * 祝日の値の決め方(スケジュールの holiday の設定)。
28
28
  * @public
@@ -95,6 +95,7 @@ export interface WeekdayPattern {
95
95
  readonly holiday?: never;
96
96
  readonly weekdays?: never;
97
97
  readonly kinds?: never;
98
+ readonly keys?: never;
98
99
  }
99
100
  /**
100
101
  * 毎月の日の繰り返し(例: 毎月 20 日、毎月末日)。その月にない日(2 月 30 日など)は当てはまらない。
@@ -114,6 +115,7 @@ export interface DayOfMonthPattern {
114
115
  readonly holiday?: never;
115
116
  readonly weekdays?: never;
116
117
  readonly kinds?: never;
118
+ readonly keys?: never;
117
119
  }
118
120
  /**
119
121
  * 祝日の翌日(祝日が続く場合は連休が明けた日。その日自身は祝日でない)。「祝日」は提供元の祝日だけ。
@@ -134,6 +136,7 @@ export interface DayAfterHolidayPattern {
134
136
  readonly dayBeforeHoliday?: never;
135
137
  readonly holiday?: never;
136
138
  readonly kinds?: never;
139
+ readonly keys?: never;
137
140
  }
138
141
  /**
139
142
  * 祝前日(翌日が祝日で、その日自身は祝日でない日。連休なら連休の初日の前日)。「祝日」は提供元の祝日だけ。
@@ -155,9 +158,10 @@ export interface DayBeforeHolidayPattern {
155
158
  readonly dayAfterHoliday?: never;
156
159
  readonly holiday?: never;
157
160
  readonly kinds?: never;
161
+ readonly keys?: never;
158
162
  }
159
163
  /**
160
- * 祝日(提供元の祝日である日)。kinds で祝日の種別を絞れる(省略時はすべての祝日)。
164
+ * 祝日(提供元の祝日である日)。kinds で祝日の種別を、keys で個別の祝日を絞れる(省略時はすべての祝日)。
161
165
  * 「祝日の上」の段階(holidayPolicy・スケジュールの holiday より先)で判定し、祝日の既定の扱いの例外を書く
162
166
  * (例: 「2027 年 4 月から祝日も営業」「振替休日は診療」「5 月の祝日は特別営業時間」)。
163
167
  * 当てはまる日は祝日なので、onHolidayRule は指定できない(指定すると invalid-option)。
@@ -167,6 +171,14 @@ export interface HolidayPattern<Kind extends string = string> {
167
171
  readonly holiday: true;
168
172
  /** 当てはめる祝日の種別(Holiday.kind。省略時はすべて)。提供元の種別の型で補完が効く(型は開いているので他の文字列も通る) */
169
173
  readonly kinds?: readonly Kind[];
174
+ /**
175
+ * 当てはめる祝日の識別子(Holiday.key。省略時はすべて)。kinds・weekdays・months との AND。
176
+ * 春分の日・復活祭に連動する日・太陰暦の祝日のように月日や第 n 曜日では書けない祝日を個別に指す
177
+ * (例: 「春分の日・秋分の日は営業」は { holiday: true, keys: ["vernal-equinox", "autumnal-equinox"] })。
178
+ * 振替日は元の祝日の key を持つので「成人の日とその振替」を指せる。当日だけなら kinds を併せて指定する。
179
+ * key を持たない祝日(国民の休日・臨時の祝日)には当てはまらない
180
+ */
181
+ readonly keys?: readonly string[];
170
182
  /** 当てはめる曜日(省略時はすべての曜日。例: [6] で土曜の祝日だけ) */
171
183
  readonly weekdays?: readonly Weekday[];
172
184
  /** 当てはめる月(省略時はすべての月) */
@@ -417,6 +429,8 @@ export interface RecurringRuleCompiled<T> extends CompiledPeriod {
417
429
  readonly holiday: boolean;
418
430
  /** 祝日の規則で絞る種別(null はすべての祝日) */
419
431
  readonly kinds: ReadonlySet<string> | null;
432
+ /** 祝日の規則で絞る識別子(null はすべての祝日) */
433
+ readonly keys: ReadonlySet<string> | null;
420
434
  /** 祝日・祝日の翌日・祝前日の規則で絞る曜日(null はすべての曜日) */
421
435
  readonly weekdays: ReadonlySet<number> | null;
422
436
  /** 月の集合(null はすべての月) */
@@ -14,13 +14,14 @@
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
  */
21
21
  import { addDays, daysInMonth, 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
+ import { HOLIDAY_KEY_PATTERN } from "./types.js";
24
25
  // ------------------------------------------------------------
25
26
  // 入力チェック(誤りを集める。作成時は最初の誤りを例外にする)
26
27
  // ------------------------------------------------------------
@@ -318,6 +319,24 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
318
319
  c.error("invalid-option", `${p}.kinds`, '"kinds" can be used only with "holiday"');
319
320
  ok = false;
320
321
  }
322
+ let holidayKeys = null;
323
+ if ("keys" in entry) {
324
+ if (!hasHoliday) {
325
+ c.error("invalid-option", `${p}.keys`, '"keys" can be used only with "holiday"');
326
+ ok = false;
327
+ }
328
+ else if (!Array.isArray(entry["keys"])) {
329
+ c.error("invalid-option", `${p}.keys`, "must be a non-empty array of holiday keys (Holiday.key)");
330
+ ok = false;
331
+ }
332
+ else {
333
+ const set = checkSet(c, `${p}.keys`, entry["keys"], (v) => typeof v === "string" && HOLIDAY_KEY_PATTERN.test(v), "a holiday key (Holiday.key: lowercase letters, digits and hyphens)");
334
+ if (set)
335
+ holidayKeys = set;
336
+ else
337
+ ok = false;
338
+ }
339
+ }
321
340
  let weekdays = null;
322
341
  if ("weekdays" in entry) {
323
342
  if (!hasHoliday && !hasAfter && !hasBefore) {
@@ -369,6 +388,7 @@ export function compileRecurringRules(c, path, entries, valueOf, keys) {
369
388
  dayBeforeHoliday: hasBefore,
370
389
  holiday: hasHoliday,
371
390
  kinds,
391
+ keys: holidayKeys,
372
392
  weekdays,
373
393
  months,
374
394
  onHolidayRule: onHoliday,
@@ -396,7 +416,9 @@ export function matchPattern(rule, date, holidayOf) {
396
416
  return false;
397
417
  if (rule.holiday) {
398
418
  const h = holidayOf(date);
399
- return h !== undefined && (rule.kinds === null || rule.kinds.has(h.kind));
419
+ return (h !== undefined &&
420
+ (rule.kinds === null || rule.kinds.has(h.kind)) &&
421
+ (rule.keys === null || (h.key !== undefined && rule.keys.has(h.key))));
400
422
  }
401
423
  // 祝日の翌日は前日が祝日、祝前日は翌日が祝日(どちらもその日自身は祝日でない)
402
424
  if (holidayOf(date) !== undefined)
@@ -546,6 +568,12 @@ function loadProviderYear(provider, year) {
546
568
  throw providerDataError(provider, year, `item ${i} (${date}) names: ${problem}`);
547
569
  holiday.names = Object.freeze(Object.fromEntries(Object.entries(h["names"])));
548
570
  }
571
+ if (h["key"] !== undefined) {
572
+ if (typeof h["key"] !== "string" || !HOLIDAY_KEY_PATTERN.test(h["key"])) {
573
+ throw providerDataError(provider, year, `item ${i} (${date}) key must match ${String(HOLIDAY_KEY_PATTERN)}: ${JSON.stringify(h["key"])}`);
574
+ }
575
+ holiday.key = h["key"];
576
+ }
549
577
  return Object.freeze(holiday);
550
578
  });
551
579
  }
@@ -674,6 +702,7 @@ export const RECURRING_RULE_KEYS = [
674
702
  "dayBeforeHoliday",
675
703
  "holiday",
676
704
  "kinds",
705
+ "keys",
677
706
  "weekdays",
678
707
  "months",
679
708
  "name",
@@ -1,4 +1,6 @@
1
1
  import type { DateString } from "./date.ts";
2
+ /** @internal 祝日の識別子(Holiday.key)の形式 */
3
+ export declare const HOLIDAY_KEY_PATTERN: RegExp;
2
4
  /**
3
5
  * 祝日・休日 1 日分。
4
6
  * @public
@@ -17,6 +19,14 @@ export interface Holiday<Kind extends string = string> {
17
19
  readonly names?: Readonly<Record<string, string>>;
18
20
  /** 種別(国ごとに定義。日本は JapaneseHolidayKind) */
19
21
  readonly kind: Kind;
22
+ /**
23
+ * その日が観測している祝日の識別子(省略可能)。/^[a-z0-9][a-z0-9-]*$/ に限る。
24
+ * name が公式データの呼び方を写すのに対し、key は規則({ holiday: true, keys: [...] })から祝日を指すための安定した
25
+ * 識別子で、提供元が決め、版をまたいで変えない(日付の移動・公式の改名があっても同じ祝日なら同じ key)。
26
+ * 振替日は元の祝日の key を持つ。種別と同じ情報しか持たない日(日本の国民の休日)や一度きりの日(臨時の祝日)は持たない。
27
+ * 同じ年に同じ key が複数回現れることはある(振替日と元の祝日、年に 2 回ある振替休日)
28
+ */
29
+ readonly key?: string;
20
30
  /**
21
31
  * 公式に確定しているか。
22
32
  * 例: 日本の春分・秋分の日は前年 2 月の官報で確定するため、それより先の年は false(計算による予測)
@@ -29,7 +39,7 @@ export interface Holiday<Kind extends string = string> {
29
39
  * 他の国・地域の祝日はこのインターフェースを実装して createCalendar・createSchedule に渡す。
30
40
  * holidaysInYear が返すデータは、年ごとに初めて使うときに検証され、誤りがあれば
31
41
  * code: "invalid-provider-data" の例外になる。返した祝日は、契約の項目(date・name・nameEn・names・
32
- * kind・confirmed)だけを持つ変更できないオブジェクトに複製される。
42
+ * kind・confirmed・key)だけを持つ変更できないオブジェクトに複製される。
33
43
  *
34
44
  * 互換性の約束: このインターフェースに項目を足すときは、メジャー版以外では省略可能な項目に限る。
35
45
  * @public
@@ -1 +1,2 @@
1
- export {};
1
+ /** @internal 祝日の識別子(Holiday.key)の形式 */
2
+ export const HOLIDAY_KEY_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
@@ -14,7 +14,7 @@ import type { DateString } from "../core/date.ts";
14
14
  import type { CivilDateTimeParts, DateInput, DateTimeInput } from "../core/input.ts";
15
15
  import type { Holiday, HolidayProvider } from "../core/types.ts";
16
16
  import { type JapaneseHolidayKind } from "./rules.ts";
17
- export type { JapaneseHolidayKind, KnownJapaneseHolidayKind } from "./rules.ts";
17
+ export type { JapaneseHolidayKind, KnownJapaneseHolidayKey, KnownJapaneseHolidayKind, } from "./rules.ts";
18
18
  /**
19
19
  * 日本の祝日。
20
20
  * @public
@@ -5,6 +5,7 @@
5
5
  * - 皇室行事による休日: 特例法ごとの日付をテーブルで持つ
6
6
  * - 振替休日: 1973-04-12 以降。2007 年からは「その日後で最も近い国民の祝日でない日」
7
7
  * - 国民の休日: 1985-12-27 以降。前日と翌日が国民の祝日である日
8
+ * - 識別子(key): 祝日法の各祝日に 1 つ(KnownJapaneseHolidayKey)。振替休日は元の祝日の key、国民の休日・特例の休日は持たない
8
9
  * - 春分・秋分の日: 天文計算による近似式。官報公表済みの年(CONFIRMED_THROUGH)までは確定値と一致することを
9
10
  * 内閣府の CSV で検証している。それより後の年は confirmed: false
10
11
  */
@@ -28,6 +29,14 @@ export type KnownJapaneseHolidayKind =
28
29
  * @public
29
30
  */
30
31
  export type JapaneseHolidayKind = KnownJapaneseHolidayKind | (string & {});
32
+ /**
33
+ * 日本の国民の祝日の識別子(Holiday.key。既知の値)。祝日法の各祝日に 1 つで、日付の移動や改名があっても同じ祝日なら同じ値
34
+ * (天皇誕生日は 4/29・12/23・2/23 とも "emperors-birthday"、体育の日とスポーツの日は "sports")。
35
+ * 振替休日は元の祝日の key を持ち、国民の休日と特例の休日は key を持たない。
36
+ * 規則の keys に書くときの補完に使う(型は開いているので他の文字列も通る)。
37
+ * @beta
38
+ */
39
+ export type KnownJapaneseHolidayKey = "new-year" | "coming-of-age" | "national-foundation" | "emperors-birthday" | "vernal-equinox" | "showa" | "constitution-memorial" | "greenery" | "childrens" | "marine" | "mountain" | "respect-for-the-aged" | "autumnal-equinox" | "sports" | "culture" | "labor-thanksgiving";
31
40
  /** @internal 対応年 */
32
41
  export declare const SUPPORTED_YEARS: Readonly<{
33
42
  from: 1955;
package/dist/jp/rules.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * - 皇室行事による休日: 特例法ごとの日付をテーブルで持つ
6
6
  * - 振替休日: 1973-04-12 以降。2007 年からは「その日後で最も近い国民の祝日でない日」
7
7
  * - 国民の休日: 1985-12-27 以降。前日と翌日が国民の祝日である日
8
+ * - 識別子(key): 祝日法の各祝日に 1 つ(KnownJapaneseHolidayKey)。振替休日は元の祝日の key、国民の休日・特例の休日は持たない
8
9
  * - 春分・秋分の日: 天文計算による近似式。官報公表済みの年(CONFIRMED_THROUGH)までは確定値と一致することを
9
10
  * 内閣府の CSV で検証している。それより後の年は confirmed: false
10
11
  */
@@ -55,64 +56,64 @@ export function autumnalEquinoxDay(year) {
55
56
  /** 国民の祝日と特例の休日(振替休日・国民の休日を除く) */
56
57
  function baseHolidays(y) {
57
58
  const list = [];
58
- const add = (date, name, nameEn, equinox = false) => {
59
- list.push({ date, name, nameEn, kind: "national", equinox });
59
+ const add = (date, name, nameEn, key, equinox = false) => {
60
+ list.push({ date, name, nameEn, kind: "national", equinox, key });
60
61
  };
61
62
  const md = (month, day) => formatDate(y, month, day);
62
63
  const monday = (month, n) => nthWeekdayOfMonth(y, month, 1, n);
63
- add(md(1, 1), "元日", "New Year's Day");
64
- add(y <= 1999 ? md(1, 15) : monday(1, 2), "成人の日", "Coming of Age Day");
64
+ add(md(1, 1), "元日", "New Year's Day", "new-year");
65
+ add(y <= 1999 ? md(1, 15) : monday(1, 2), "成人の日", "Coming of Age Day", "coming-of-age");
65
66
  if (y >= 1967)
66
- add(md(2, 11), "建国記念の日", "National Foundation Day");
67
+ add(md(2, 11), "建国記念の日", "National Foundation Day", "national-foundation");
67
68
  if (y >= 2020)
68
- add(md(2, 23), "天皇誕生日", "The Emperor's Birthday");
69
- add(md(3, vernalEquinoxDay(y)), "春分の日", "Vernal Equinox Day", true);
69
+ add(md(2, 23), "天皇誕生日", "The Emperor's Birthday", "emperors-birthday");
70
+ add(md(3, vernalEquinoxDay(y)), "春分の日", "Vernal Equinox Day", "vernal-equinox", true);
70
71
  if (y <= 1988)
71
- add(md(4, 29), "天皇誕生日", "The Emperor's Birthday");
72
+ add(md(4, 29), "天皇誕生日", "The Emperor's Birthday", "emperors-birthday");
72
73
  else if (y <= 2006)
73
- add(md(4, 29), "みどりの日", "Greenery Day");
74
+ add(md(4, 29), "みどりの日", "Greenery Day", "greenery");
74
75
  else
75
- add(md(4, 29), "昭和の日", "Showa Day");
76
- add(md(5, 3), "憲法記念日", "Constitution Memorial Day");
76
+ add(md(4, 29), "昭和の日", "Showa Day", "showa");
77
+ add(md(5, 3), "憲法記念日", "Constitution Memorial Day", "constitution-memorial");
77
78
  if (y >= 2007)
78
- add(md(5, 4), "みどりの日", "Greenery Day");
79
- add(md(5, 5), "こどもの日", "Children's Day");
79
+ add(md(5, 4), "みどりの日", "Greenery Day", "greenery");
80
+ add(md(5, 5), "こどもの日", "Children's Day", "childrens");
80
81
  // 海の日(1996〜)。2020・2021 年は五輪特措法による移動
81
82
  if (y === 2020)
82
- add(md(7, 23), "海の日", "Marine Day");
83
+ add(md(7, 23), "海の日", "Marine Day", "marine");
83
84
  else if (y === 2021)
84
- add(md(7, 22), "海の日", "Marine Day");
85
+ add(md(7, 22), "海の日", "Marine Day", "marine");
85
86
  else if (y >= 2003)
86
- add(monday(7, 3), "海の日", "Marine Day");
87
+ add(monday(7, 3), "海の日", "Marine Day", "marine");
87
88
  else if (y >= 1996)
88
- add(md(7, 20), "海の日", "Marine Day");
89
+ add(md(7, 20), "海の日", "Marine Day", "marine");
89
90
  // スポーツの日(2020〜)。2020・2021 年は五輪特措法による移動
90
91
  if (y === 2020)
91
- add(md(7, 24), "スポーツの日", "Sports Day");
92
+ add(md(7, 24), "スポーツの日", "Sports Day", "sports");
92
93
  else if (y === 2021)
93
- add(md(7, 23), "スポーツの日", "Sports Day");
94
+ add(md(7, 23), "スポーツの日", "Sports Day", "sports");
94
95
  // 山の日(2016〜)。2020・2021 年は五輪特措法による移動
95
96
  if (y === 2020)
96
- add(md(8, 10), "山の日", "Mountain Day");
97
+ add(md(8, 10), "山の日", "Mountain Day", "mountain");
97
98
  else if (y === 2021)
98
- add(md(8, 8), "山の日", "Mountain Day");
99
+ add(md(8, 8), "山の日", "Mountain Day", "mountain");
99
100
  else if (y >= 2016)
100
- add(md(8, 11), "山の日", "Mountain Day");
101
+ add(md(8, 11), "山の日", "Mountain Day", "mountain");
101
102
  if (y >= 2003)
102
- add(monday(9, 3), "敬老の日", "Respect for the Aged Day");
103
+ add(monday(9, 3), "敬老の日", "Respect for the Aged Day", "respect-for-the-aged");
103
104
  else if (y >= 1966)
104
- add(md(9, 15), "敬老の日", "Respect for the Aged Day");
105
- add(md(9, autumnalEquinoxDay(y)), "秋分の日", "Autumnal Equinox Day", true);
105
+ add(md(9, 15), "敬老の日", "Respect for the Aged Day", "respect-for-the-aged");
106
+ add(md(9, autumnalEquinoxDay(y)), "秋分の日", "Autumnal Equinox Day", "autumnal-equinox", true);
106
107
  if (y >= 2022)
107
- add(monday(10, 2), "スポーツの日", "Sports Day");
108
+ add(monday(10, 2), "スポーツの日", "Sports Day", "sports");
108
109
  else if (y >= 2000 && y <= 2019)
109
- add(monday(10, 2), "体育の日", "Health and Sports Day");
110
+ add(monday(10, 2), "体育の日", "Health and Sports Day", "sports");
110
111
  else if (y >= 1966 && y <= 1999)
111
- add(md(10, 10), "体育の日", "Health and Sports Day");
112
- add(md(11, 3), "文化の日", "Culture Day");
113
- add(md(11, 23), "勤労感謝の日", "Labor Thanksgiving Day");
112
+ add(md(10, 10), "体育の日", "Health and Sports Day", "sports");
113
+ add(md(11, 3), "文化の日", "Culture Day", "culture");
114
+ add(md(11, 23), "勤労感謝の日", "Labor Thanksgiving Day", "labor-thanksgiving");
114
115
  if (y >= 1989 && y <= 2018)
115
- add(md(12, 23), "天皇誕生日", "The Emperor's Birthday");
116
+ add(md(12, 23), "天皇誕生日", "The Emperor's Birthday", "emperors-birthday");
116
117
  for (const s of SPECIAL_DAYS) {
117
118
  if (s.date.startsWith(`${y}-`))
118
119
  list.push({ ...s, kind: "special", equinox: false });
@@ -147,6 +148,7 @@ export function computeJapaneseHolidays(year) {
147
148
  nameEn: "Substitute Holiday",
148
149
  kind: "substitute",
149
150
  equinox: h.equinox,
151
+ ...(h.key === undefined ? {} : { key: h.key }),
150
152
  });
151
153
  }
152
154
  // 国民の休日(前日と翌日が国民の祝日である日)
@@ -177,5 +179,6 @@ export function computeJapaneseHolidays(year) {
177
179
  nameEn: e.nameEn,
178
180
  kind: e.kind,
179
181
  confirmed: year <= CONFIRMED_THROUGH || !e.equinox,
182
+ ...(e.key === undefined ? {} : { key: e.key }),
180
183
  }));
181
184
  }
@@ -4,11 +4,14 @@
4
4
  * 多くの国・地域の祝日は「月の第 n 何曜日」(米国の連邦祝日など)と「復活祭からの日数」(ヨーロッパの多くの国)で
5
5
  * 書ける。この 2 種類の計算と、月の日数・うるう年の判定を、ライブラリ内部で使っている実装のまま公開する
6
6
  * (復活祭の計算は英国の祝日で GOV.UK のデータと照合済み)。ルートの addDays・weekdayOf と組み合わせて使う。
7
+ * 週末に当たった祝日の振替(substituteDays)は、同梱の 3 つの提供元が使う計算と同じ定義で、同梱の提供元の
8
+ * 照合済みの結果と一致することをテストで確かめている(同梱の提供元の実装そのものは置き換えていない)。
7
9
  * 同梱の提供元の実装とは別に、アプリの本体に不要なものが入らないようサブパスに分けている。
8
10
  *
9
11
  * 公開する関数は @beta(マイナー版で変わりうる)。使われ方が固まったら @public にする。
10
12
  */
11
13
  import { type DateString, type Weekday } from "../core/date.ts";
14
+ import { type Holiday } from "../core/types.ts";
12
15
  /**
13
16
  * 復活祭の日曜日(グレゴリオ暦)。Good Friday は −2 日、Easter Monday は +1 日、Ascension は +39 日、
14
17
  * Whit Monday は +50 日、Corpus Christi は +60 日(ルートの addDays で求める)。
@@ -33,3 +36,57 @@ export declare function daysInMonth(year: number, month: number): number;
33
36
  * @beta
34
37
  */
35
38
  export declare function isLeapYear(year: number): boolean;
39
+ /**
40
+ * 振替の向き。"next" はその日より後で最初の振替先、"previous" はその日より前で最初の振替先。
41
+ * @beta
42
+ */
43
+ export type SubstituteDirection = "next" | "previous";
44
+ /**
45
+ * substituteDays の設定。
46
+ * @beta
47
+ */
48
+ export interface SubstituteDaysOptions<Kind extends string> {
49
+ /**
50
+ * 振り替える曜日と向き(0 = 日曜日〜6 = 土曜日)。書いた曜日に当たる祝日だけを振り替える。
51
+ * 例: 日本・シンガポールは { 0: "next" }、英国は { 6: "next", 0: "next" }、米国の連邦祝日は { 6: "previous", 0: "next" }。
52
+ * 振替先は、入力の祝日でも、それまでに決めた振替日でも、ここに書いた曜日でもない最初の日
53
+ */
54
+ readonly rules: Readonly<Partial<Record<Weekday, SubstituteDirection>>>;
55
+ /** 振替日に付ける種別(Holiday.kind。例: "substitute"、"observed") */
56
+ readonly kind: Kind;
57
+ /**
58
+ * 振替日の名称。省略時は元の祝日の name・nameEn(と names)を引き継ぐ(GOV.UK・MOM の流儀)。
59
+ * 変えるなら元の祝日を受け取って返す関数で(例: 日本の「振替休日」、米国の "(observed)")
60
+ */
61
+ readonly name?: (holiday: Holiday<Kind>) => {
62
+ readonly name: string;
63
+ readonly nameEn: string;
64
+ };
65
+ /**
66
+ * 元の祝日(振り替えた曜日に当たった日)を結果に残すか。"keep"(既定)は残す(日本・シンガポール)、
67
+ * "drop" は振替日だけにする(GOV.UK の一覧は週末の日を載せない)
68
+ */
69
+ readonly original?: "keep" | "drop";
70
+ }
71
+ /**
72
+ * 週末などに当たった祝日の振替日を計算し、入力の祝日に振替日を足して日付順に返す(提供元を実装するための道具)。
73
+ *
74
+ * 定義(英国の附則・シンガポールの Holidays Act・日本の祝日法と同じ): 入力を日付順に走査し、rules の曜日に当たる祝日ごとに、
75
+ * 指定の向きで「入力の祝日でも、それまでに決めた振替日でも、rules の曜日でもない最初の日」を振替日にする。祝日が続く
76
+ * 週末(Christmas Day が土曜日、Boxing Day が日曜日)は順に先へ送られる。振替日自身は別の祝日の振替の対象にならない。
77
+ * 振替日は元の祝日の key・names・confirmed を引き継ぎ、kind は options.kind、名称は省略時は元の祝日のもの。
78
+ *
79
+ * 入力は同じ年の祝日で、日付が有効・重複がないこと(誤りは invalid-argument・invalid-date・invalid-weekday の例外)。
80
+ * 振替日は年をまたぐことがある(12 月 31 日が祝日の国で日曜日なら翌年の 1 月 1 日)。提供元の holidaysInYear は
81
+ * その年の日付だけを返す契約なので、そうなる国では翌年の分として扱うこと。
82
+ * 国民の休日(2 つの祝日に挟まれた日)のような国に固有の規則は含まない。
83
+ *
84
+ * @example 米国の連邦祝日(土曜日は前の金曜日、日曜日は次の月曜日)
85
+ * substituteDays(federal, { rules: { 6: "previous", 0: "next" }, kind: "observed",
86
+ * name: (h) => ({ name: `${h.name} (observed)`, nameEn: `${h.nameEn} (observed)` }) });
87
+ * @example 日本の振替休日(日曜日の祝日を、その日の後で最も近い祝日でない日へ)
88
+ * substituteDays(holidays, { rules: { 0: "next" }, kind: "substitute",
89
+ * name: () => ({ name: "振替休日", nameEn: "Substitute Holiday" }) });
90
+ * @beta
91
+ */
92
+ export declare function substituteDays<Kind extends string>(holidays: readonly Holiday<Kind>[], options: SubstituteDaysOptions<Kind>): Holiday<Kind>[];
@@ -4,12 +4,15 @@
4
4
  * 多くの国・地域の祝日は「月の第 n 何曜日」(米国の連邦祝日など)と「復活祭からの日数」(ヨーロッパの多くの国)で
5
5
  * 書ける。この 2 種類の計算と、月の日数・うるう年の判定を、ライブラリ内部で使っている実装のまま公開する
6
6
  * (復活祭の計算は英国の祝日で GOV.UK のデータと照合済み)。ルートの addDays・weekdayOf と組み合わせて使う。
7
+ * 週末に当たった祝日の振替(substituteDays)は、同梱の 3 つの提供元が使う計算と同じ定義で、同梱の提供元の
8
+ * 照合済みの結果と一致することをテストで確かめている(同梱の提供元の実装そのものは置き換えていない)。
7
9
  * 同梱の提供元の実装とは別に、アプリの本体に不要なものが入らないようサブパスに分けている。
8
10
  *
9
11
  * 公開する関数は @beta(マイナー版で変わりうる)。使われ方が固まったら @public にする。
10
12
  */
11
- import { daysInMonth as daysInMonthOf, easterSunday as easterSundayOf, formatDate, isLeapYear as isLeapYearOf, nthWeekdayOfMonth as nthWeekdayOfMonthIn, weekdayOf, } from "../core/date.js";
12
- import { rangeError } from "../core/errors.js";
13
+ import { addDays, daysInMonth as daysInMonthOf, easterSunday as easterSundayOf, formatDate, isLeapYear as isLeapYearOf, isValidDate, nthWeekdayOfMonth as nthWeekdayOfMonthIn, parseDate, weekdayOf, } from "../core/date.js";
14
+ import { rangeError, typeError } from "../core/errors.js";
15
+ import { HOLIDAY_KEY_PATTERN } from "../core/types.js";
13
16
  function checkYear(year) {
14
17
  if (!Number.isInteger(year) || year < 1000 || year > 9999) {
15
18
  throw rangeError("invalid-argument", `year must be an integer from 1000 to 9999: ${String(year)}`);
@@ -74,3 +77,114 @@ export function isLeapYear(year) {
74
77
  checkYear(year);
75
78
  return isLeapYearOf(year);
76
79
  }
80
+ function checkHolidays(holidays) {
81
+ if (!Array.isArray(holidays)) {
82
+ throw typeError("invalid-argument", "holidays must be an array of Holiday");
83
+ }
84
+ let year;
85
+ const seen = new Set();
86
+ holidays.forEach((h, i) => {
87
+ if (typeof h !== "object" || h === null) {
88
+ throw typeError("invalid-argument", `holidays[${i}] is not an object`);
89
+ }
90
+ const { date, name, nameEn, kind, confirmed, key } = h;
91
+ if (!isValidDate(date)) {
92
+ throw rangeError("invalid-date", `holidays[${i}] has an invalid date ${JSON.stringify(date)}`);
93
+ }
94
+ const y = parseDate(date).year;
95
+ if (year === undefined)
96
+ year = y;
97
+ else if (y !== year) {
98
+ throw rangeError("invalid-argument", `holidays must be in one year: ${holidays[0].date} and ${date}`);
99
+ }
100
+ if (seen.has(date))
101
+ throw rangeError("invalid-argument", `holidays has a duplicate date ${date}`);
102
+ seen.add(date);
103
+ if (typeof name !== "string" || typeof nameEn !== "string" || typeof kind !== "string") {
104
+ throw typeError("invalid-argument", `holidays[${i}] (${date}) must have string name, nameEn and kind`);
105
+ }
106
+ if (typeof confirmed !== "boolean") {
107
+ throw typeError("invalid-argument", `holidays[${i}] (${date}) must have boolean confirmed`);
108
+ }
109
+ if (key !== undefined && (typeof key !== "string" || !HOLIDAY_KEY_PATTERN.test(key))) {
110
+ throw typeError("invalid-argument", `holidays[${i}] (${date}) key must match ${String(HOLIDAY_KEY_PATTERN)}: ${JSON.stringify(key)}`);
111
+ }
112
+ });
113
+ return holidays;
114
+ }
115
+ function checkOptions(options) {
116
+ if (typeof options !== "object" || options === null) {
117
+ throw typeError("invalid-argument", "options must be an object");
118
+ }
119
+ const { rules, kind, name, original } = options;
120
+ if (typeof rules !== "object" || rules === null) {
121
+ throw typeError("invalid-argument", 'options.rules must be an object of { [weekday]: "next" | "previous" }');
122
+ }
123
+ for (const [weekday, direction] of Object.entries(rules)) {
124
+ if (!/^[0-6]$/.test(weekday)) {
125
+ throw rangeError("invalid-weekday", `options.rules has a weekday that is not 0 (Sunday) to 6 (Saturday): ${JSON.stringify(weekday)}`);
126
+ }
127
+ if (direction !== "next" && direction !== "previous") {
128
+ throw rangeError("invalid-argument", `options.rules[${weekday}] must be "next" or "previous": ${JSON.stringify(direction)}`);
129
+ }
130
+ }
131
+ if (typeof kind !== "string" || kind === "") {
132
+ throw typeError("invalid-argument", "options.kind must be a non-empty string (Holiday.kind)");
133
+ }
134
+ if (name !== undefined && typeof name !== "function") {
135
+ throw typeError("invalid-argument", "options.name must be a function (holiday) => { name, nameEn }");
136
+ }
137
+ if (original !== undefined && original !== "keep" && original !== "drop") {
138
+ throw rangeError("invalid-argument", 'options.original must be "keep" or "drop"');
139
+ }
140
+ return options;
141
+ }
142
+ /**
143
+ * 週末などに当たった祝日の振替日を計算し、入力の祝日に振替日を足して日付順に返す(提供元を実装するための道具)。
144
+ *
145
+ * 定義(英国の附則・シンガポールの Holidays Act・日本の祝日法と同じ): 入力を日付順に走査し、rules の曜日に当たる祝日ごとに、
146
+ * 指定の向きで「入力の祝日でも、それまでに決めた振替日でも、rules の曜日でもない最初の日」を振替日にする。祝日が続く
147
+ * 週末(Christmas Day が土曜日、Boxing Day が日曜日)は順に先へ送られる。振替日自身は別の祝日の振替の対象にならない。
148
+ * 振替日は元の祝日の key・names・confirmed を引き継ぎ、kind は options.kind、名称は省略時は元の祝日のもの。
149
+ *
150
+ * 入力は同じ年の祝日で、日付が有効・重複がないこと(誤りは invalid-argument・invalid-date・invalid-weekday の例外)。
151
+ * 振替日は年をまたぐことがある(12 月 31 日が祝日の国で日曜日なら翌年の 1 月 1 日)。提供元の holidaysInYear は
152
+ * その年の日付だけを返す契約なので、そうなる国では翌年の分として扱うこと。
153
+ * 国民の休日(2 つの祝日に挟まれた日)のような国に固有の規則は含まない。
154
+ *
155
+ * @example 米国の連邦祝日(土曜日は前の金曜日、日曜日は次の月曜日)
156
+ * substituteDays(federal, { rules: { 6: "previous", 0: "next" }, kind: "observed",
157
+ * name: (h) => ({ name: `${h.name} (observed)`, nameEn: `${h.nameEn} (observed)` }) });
158
+ * @example 日本の振替休日(日曜日の祝日を、その日の後で最も近い祝日でない日へ)
159
+ * substituteDays(holidays, { rules: { 0: "next" }, kind: "substitute",
160
+ * name: () => ({ name: "振替休日", nameEn: "Substitute Holiday" }) });
161
+ * @beta
162
+ */
163
+ export function substituteDays(holidays, options) {
164
+ const input = checkHolidays(holidays);
165
+ const { rules, kind, name, original } = checkOptions(options);
166
+ const sorted = [...input].sort((a, b) => (a.date < b.date ? -1 : 1));
167
+ const taken = new Set(sorted.map((h) => h.date));
168
+ const result = sorted.filter((h) => original !== "drop" || rules[weekdayOf(h.date)] === undefined);
169
+ for (const h of sorted) {
170
+ const direction = rules[weekdayOf(h.date)];
171
+ if (direction === undefined)
172
+ continue;
173
+ const step = direction === "next" ? 1 : -1;
174
+ let day = addDays(h.date, step);
175
+ while (taken.has(day) || rules[weekdayOf(day)] !== undefined)
176
+ day = addDays(day, step);
177
+ taken.add(day);
178
+ const named = name === undefined ? { name: h.name, nameEn: h.nameEn } : name(h);
179
+ result.push(Object.freeze({
180
+ date: day,
181
+ name: named.name,
182
+ nameEn: named.nameEn,
183
+ ...(h.names === undefined ? {} : { names: h.names }),
184
+ kind,
185
+ confirmed: h.confirmed,
186
+ ...(h.key === undefined ? {} : { key: h.key }),
187
+ }));
188
+ }
189
+ return result.sort((a, b) => (a.date < b.date ? -1 : 1));
190
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { Holiday, HolidayProvider } from "../core/types.ts";
10
10
  import { type SgHolidayKind } from "./rules.ts";
11
- export type { KnownSgHolidayKind, SgHolidayKind } from "./rules.ts";
11
+ export type { KnownSgHolidayKey, KnownSgHolidayKind, SgHolidayKind } from "./rules.ts";
12
12
  /**
13
13
  * シンガポールの祝日。
14
14
  * @public
@@ -6,6 +6,7 @@
6
6
  * 振替だけは Holidays Act(1998 年版)第 4 条の規則「日曜日に当たる祝日は、その翌日のうちそれ自体が祝日でない
7
7
  * 最初の日を祝日とする」で計算し、表には書かない。土曜日に当たる祝日は振替にならない(雇用主が休みか手当を与える)。
8
8
  *
9
+ * 識別子(key)は 11 の祝日に 1 つずつ(KnownSgHolidayKey)。振替日は元の祝日の key、Polling Day は持たない。
9
10
  * 名称は MOM の報道発表の綴り(アポストロフィは ’)。data.gov.sg のコレクション 691(MOM)のデータで
10
11
  * 全日照合している(test/fixtures/datagovsg-public-holidays.json)。data.gov.sg の 2022 年分は MOM の改定後の告示と
11
12
  * 食い違う(scripts/verify-datagovsg.ts の KNOWN_DIFFERENCES)ため、告示を正としている。
@@ -28,6 +29,13 @@ export type KnownSgHolidayKind =
28
29
  * @public
29
30
  */
30
31
  export type SgHolidayKind = KnownSgHolidayKind | (string & {});
32
+ /**
33
+ * シンガポールの毎年の祝日の識別子(Holiday.key。既知の値)。11 の祝日に 1 つずつ。
34
+ * 振替日は元の祝日の key を持ち、臨時の祝日(Polling Day)は key を持たない。
35
+ * 規則の keys に書くときの補完に使う(型は開いているので他の文字列も通る)。
36
+ * @beta
37
+ */
38
+ export type KnownSgHolidayKey = "new-year" | "chinese-new-year" | "good-friday" | "labour-day" | "hari-raya-puasa" | "vesak" | "hari-raya-haji" | "national-day" | "deepavali" | "christmas";
31
39
  /** @internal 対応年(表のある年。それより後は予測できないので out-of-range) */
32
40
  export declare const SUPPORTED_YEARS: Readonly<{
33
41
  from: 2020;
package/dist/sg/rules.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * 振替だけは Holidays Act(1998 年版)第 4 条の規則「日曜日に当たる祝日は、その翌日のうちそれ自体が祝日でない
7
7
  * 最初の日を祝日とする」で計算し、表には書かない。土曜日に当たる祝日は振替にならない(雇用主が休みか手当を与える)。
8
8
  *
9
+ * 識別子(key)は 11 の祝日に 1 つずつ(KnownSgHolidayKey)。振替日は元の祝日の key、Polling Day は持たない。
9
10
  * 名称は MOM の報道発表の綴り(アポストロフィは ’)。data.gov.sg のコレクション 691(MOM)のデータで
10
11
  * 全日照合している(test/fixtures/datagovsg-public-holidays.json)。data.gov.sg の 2022 年分は MOM の改定後の告示と
11
12
  * 食い違う(scripts/verify-datagovsg.ts の KNOWN_DIFFERENCES)ため、告示を正としている。
@@ -29,6 +30,19 @@ const NATIONAL = "National Day";
29
30
  const DEEPAVALI = "Deepavali";
30
31
  const CHRISTMAS = "Christmas Day";
31
32
  const POLLING = "Polling Day";
33
+ /** 名称から識別子へ(Polling Day は持たない) */
34
+ const KEY_OF = {
35
+ [NEW_YEAR]: "new-year",
36
+ [CNY]: "chinese-new-year",
37
+ [GOOD_FRIDAY]: "good-friday",
38
+ [LABOUR]: "labour-day",
39
+ [PUASA]: "hari-raya-puasa",
40
+ [VESAK]: "vesak",
41
+ [HAJI]: "hari-raya-haji",
42
+ [NATIONAL]: "national-day",
43
+ [DEEPAVALI]: "deepavali",
44
+ [CHRISTMAS]: "christmas",
45
+ };
32
46
  /** 年ごとに動く祝日(月日と名前)。固定日の祝日(1/1・5/1・8/9・12/25)は表に書かず、下で足す */
33
47
  const MOVABLE = {
34
48
  2020: [
@@ -152,5 +166,15 @@ export function computeSgHolidays(year) {
152
166
  }
153
167
  return entries
154
168
  .sort((a, b) => (a.date < b.date ? -1 : 1))
155
- .map((e) => Object.freeze({ date: e.date, name: e.name, nameEn: e.name, kind: e.kind, confirmed: true }));
169
+ .map((e) => {
170
+ const key = KEY_OF[e.name];
171
+ return Object.freeze({
172
+ date: e.date,
173
+ name: e.name,
174
+ nameEn: e.name,
175
+ kind: e.kind,
176
+ confirmed: true,
177
+ ...(key === undefined ? {} : { key }),
178
+ });
179
+ });
156
180
  }
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { Holiday, HolidayProvider } from "../core/types.ts";
13
13
  import { type UkHolidayKind } from "./rules.ts";
14
- export type { KnownUkHolidayKind, UkHolidayKind } from "./rules.ts";
14
+ export type { KnownUkHolidayKey, KnownUkHolidayKind, UkHolidayKind } from "./rules.ts";
15
15
  /**
16
16
  * 英国の祝日。
17
17
  * @public
@@ -5,6 +5,7 @@
5
5
  * (England and Wales・Northern Ireland の Good Friday と Christmas Day は慣習法上の休日だが、GOV.UK の一覧と同じく含める)
6
6
  * - 振替: 土曜日・日曜日に当たる固定日の祝日は、その後で最も近い、祝日でない平日に振り替える(日付順に割り当てる)
7
7
  * - 臨時の祝日・移動: 布告ごとの日付をテーブルで持つ
8
+ * - 識別子(key): 毎年の祝日に GOV.UK の区分名から 1 つ(KnownUkHolidayKey)。振替日は元の祝日の key、臨時の祝日は持たない
8
9
  *
9
10
  * 対応年のすべての日付と名称が GOV.UK のデータと一致することを、テスト用フィクスチャ
10
11
  * (test/fixtures/govuk-bank-holidays.json)で検証している。CONFIRMED_THROUGH より後の年は、
@@ -28,6 +29,13 @@ export type KnownUkHolidayKind =
28
29
  * @public
29
30
  */
30
31
  export type UkHolidayKind = KnownUkHolidayKind | (string & {});
32
+ /**
33
+ * 英国の毎年の bank holiday の識別子(Holiday.key。既知の値)。GOV.UK の区分名から付け、布告で日付が移った年も同じ値。
34
+ * 振替日は元の祝日の key を持ち、臨時の祝日(special)は key を持たない。
35
+ * 規則の keys に書くときの補完に使う(型は開いているので他の文字列も通る)。
36
+ * @beta
37
+ */
38
+ export type KnownUkHolidayKey = "new-year" | "2nd-january" | "st-patricks" | "good-friday" | "easter-monday" | "early-may" | "spring" | "battle-of-the-boyne" | "summer" | "st-andrews" | "christmas" | "boxing-day";
31
39
  /** @internal GOV.UK の区分 */
32
40
  export type UkDivision = "england-and-wales" | "scotland" | "northern-ireland";
33
41
  /** @internal 対応年 */
package/dist/uk/rules.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * (England and Wales・Northern Ireland の Good Friday と Christmas Day は慣習法上の休日だが、GOV.UK の一覧と同じく含める)
6
6
  * - 振替: 土曜日・日曜日に当たる固定日の祝日は、その後で最も近い、祝日でない平日に振り替える(日付順に割り当てる)
7
7
  * - 臨時の祝日・移動: 布告ごとの日付をテーブルで持つ
8
+ * - 識別子(key): 毎年の祝日に GOV.UK の区分名から 1 つ(KnownUkHolidayKey)。振替日は元の祝日の key、臨時の祝日は持たない
8
9
  *
9
10
  * 対応年のすべての日付と名称が GOV.UK のデータと一致することを、テスト用フィクスチャ
10
11
  * (test/fixtures/govuk-bank-holidays.json)で検証している。CONFIRMED_THROUGH より後の年は、
@@ -59,8 +60,14 @@ const SPECIAL_DAYS = [
59
60
  * 同じ曜日の並びの 2012・2017 年は、附則どおり 1/2 が 2nd January、1/3 が New Year’s Day の振替
60
61
  */
61
62
  const RENAMED = [
62
- { division: "scotland", date: "2023-01-02", name: NEW_YEAR, kind: "substitute" },
63
- { division: "scotland", date: "2023-01-03", name: SECOND_JANUARY, kind: "substitute" },
63
+ { division: "scotland", date: "2023-01-02", name: NEW_YEAR, kind: "substitute", key: "new-year" },
64
+ {
65
+ division: "scotland",
66
+ date: "2023-01-03",
67
+ name: SECOND_JANUARY,
68
+ kind: "substitute",
69
+ key: "2nd-january",
70
+ },
64
71
  ];
65
72
  function lastMonday(year, month) {
66
73
  const last = formatDate(year, month, daysInMonth(year, month));
@@ -77,33 +84,40 @@ export function computeUkHolidays(division, year) {
77
84
  }
78
85
  const scotland = division === "scotland";
79
86
  const ni = division === "northern-ireland";
80
- const fixed = [{ month: 1, day: 1, name: NEW_YEAR }];
87
+ const fixed = [{ month: 1, day: 1, name: NEW_YEAR, key: "new-year" }];
81
88
  if (scotland)
82
- fixed.push({ month: 1, day: 2, name: SECOND_JANUARY });
89
+ fixed.push({ month: 1, day: 2, name: SECOND_JANUARY, key: "2nd-january" });
83
90
  if (ni)
84
- fixed.push({ month: 3, day: 17, name: ST_PATRICK });
91
+ fixed.push({ month: 3, day: 17, name: ST_PATRICK, key: "st-patricks" });
85
92
  if (ni)
86
- fixed.push({ month: 7, day: 12, name: BOYNE });
93
+ fixed.push({ month: 7, day: 12, name: BOYNE, key: "battle-of-the-boyne" });
87
94
  if (scotland)
88
- fixed.push({ month: 11, day: 30, name: ST_ANDREW });
89
- fixed.push({ month: 12, day: 25, name: CHRISTMAS }, { month: 12, day: 26, name: BOXING });
95
+ fixed.push({ month: 11, day: 30, name: ST_ANDREW, key: "st-andrews" });
96
+ fixed.push({ month: 12, day: 25, name: CHRISTMAS, key: "christmas" }, { month: 12, day: 26, name: BOXING, key: "boxing-day" });
90
97
  const easter = easterSunday(year);
91
98
  const movable = [
92
- { date: addDays(easter, -2), name: GOOD_FRIDAY, kind: "regular" },
93
- { date: nthWeekdayOfMonth(year, 5, 1, 1), name: EARLY_MAY, kind: "regular" },
94
- { date: lastMonday(year, 5), name: SPRING, kind: "regular" },
99
+ { date: addDays(easter, -2), name: GOOD_FRIDAY, kind: "regular", key: "good-friday" },
100
+ { date: nthWeekdayOfMonth(year, 5, 1, 1), name: EARLY_MAY, kind: "regular", key: "early-may" },
101
+ { date: lastMonday(year, 5), name: SPRING, kind: "regular", key: "spring" },
95
102
  {
96
103
  date: scotland ? nthWeekdayOfMonth(year, 8, 1, 1) : lastMonday(year, 8),
97
104
  name: SUMMER,
98
105
  kind: "regular",
106
+ key: "summer",
99
107
  },
100
108
  ];
101
- if (!scotland)
102
- movable.push({ date: addDays(easter, 1), name: EASTER_MONDAY, kind: "regular" });
109
+ if (!scotland) {
110
+ movable.push({
111
+ date: addDays(easter, 1),
112
+ name: EASTER_MONDAY,
113
+ kind: "regular",
114
+ key: "easter-monday",
115
+ });
116
+ }
103
117
  const entries = [];
104
118
  for (const e of movable) {
105
119
  const moved = MOVED.find((m) => m.year === year && m.from === e.name);
106
- entries.push(moved ? { date: moved.date, name: moved.name ?? e.name, kind: e.kind } : e);
120
+ entries.push(moved ? { ...e, date: moved.date, name: moved.name ?? e.name } : e);
107
121
  }
108
122
  for (const s of SPECIAL_DAYS) {
109
123
  if (s.date.startsWith(`${year}-`) && s.divisions.includes(division)) {
@@ -112,10 +126,14 @@ export function computeUkHolidays(division, year) {
112
126
  }
113
127
  // 平日に当たる固定日の祝日を先に置き、土曜日・日曜日に当たるものを日付順に振り替える
114
128
  const taken = new Set(entries.map((e) => e.date));
115
- const fixedDates = fixed.map((f) => ({ date: formatDate(year, f.month, f.day), name: f.name }));
129
+ const fixedDates = fixed.map((f) => ({
130
+ date: formatDate(year, f.month, f.day),
131
+ name: f.name,
132
+ key: f.key,
133
+ }));
116
134
  for (const f of fixedDates) {
117
135
  if (!isWeekend(f.date)) {
118
- entries.push({ date: f.date, name: f.name, kind: "regular" });
136
+ entries.push({ date: f.date, name: f.name, kind: "regular", key: f.key });
119
137
  taken.add(f.date);
120
138
  }
121
139
  }
@@ -125,7 +143,7 @@ export function computeUkHolidays(division, year) {
125
143
  let day = addDays(f.date, 1);
126
144
  while (isWeekend(day) || taken.has(day))
127
145
  day = addDays(day, 1);
128
- entries.push({ date: day, name: f.name, kind: "substitute" });
146
+ entries.push({ date: day, name: f.name, kind: "substitute", key: f.key });
129
147
  taken.add(day);
130
148
  }
131
149
  for (const r of RENAMED) {
@@ -133,7 +151,7 @@ export function computeUkHolidays(division, year) {
133
151
  continue;
134
152
  const i = entries.findIndex((e) => e.date === r.date);
135
153
  if (i >= 0)
136
- entries[i] = { date: r.date, name: r.name, kind: r.kind };
154
+ entries[i] = { date: r.date, name: r.name, kind: r.kind, key: r.key };
137
155
  }
138
156
  return entries
139
157
  .sort((a, b) => (a.date < b.date ? -1 : 1))
@@ -143,5 +161,6 @@ export function computeUkHolidays(division, year) {
143
161
  nameEn: e.name,
144
162
  kind: e.kind,
145
163
  confirmed: year <= CONFIRMED_THROUGH,
164
+ ...(e.key === undefined ? {} : { key: e.key }),
146
165
  }));
147
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bizdays-toolkit",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Holidays, business-day calendar and day-by-day schedules (e.g. store hours) with time zone support. Japanese holidays verified against the Cabinet Office data; UK bank holidays (England, Wales, Scotland, Northern Ireland) verified against GOV.UK; Singapore public holidays verified against data.gov.sg. / 祝日と営業日のカレンダー(タイムゾーン対応・日本の祝日は内閣府、英国の bank holidays は GOV.UK、シンガポールの祝日は data.gov.sg のデータで全日照合)",
5
5
  "keywords": [
6
6
  "bank-holidays",