bizdays-toolkit 0.2.0 → 0.3.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,35 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.0] - 2026-10-15
8
+
9
+ 1.0.0 に向けて、将来の機能追加で利用者のコードが壊れにくい作りにしました。機能の追加はありません。
10
+
11
+ ### Added
12
+
13
+ - 例外に `code` を付けた(`invalid-date`・`invalid-range`・`no-business-day` など)。例外は従来どおり `RangeError`・`TypeError`・`Error` のインスタンス。`isBizdaysError()` で判定できる
14
+ - `KnownDayReason`・`KnownScheduleReason`・`KnownJapaneseHolidayKind`・`KnownErrorCode`: 既知の値だけの閉じた型(網羅を型で確かめたい場合に使う)
15
+ - 祝日の提供元(`HolidayProvider`)の形式を作る時点で、返したデータを年ごとに初めて使うときに検証する(誤りは `invalid-provider`・`invalid-provider-data`)
16
+
17
+ ### Changed
18
+
19
+ - `DayReason`・`ScheduleReason`・`JapaneseHolidayKind` を「開いた型」にした(将来の版で値が増えても、利用者のコードがコンパイルエラーにならない)。値を列挙して分岐しているコードは「その他」の分岐が必要になる
20
+ - 設定の誤りのメッセージに、設定の中の位置(例: `closedDates[1].date`)を入れた
21
+ - 設定は作る時点で複製するようにした(作ったあとに元の配列を書き換えても、結果が変わらない)
22
+ - 提供元が返した祝日は、契約の項目だけを持つ変更できないオブジェクトにして返す
23
+ - 別の実行環境(iframe・`vm`)で作られた `Date` も受け付けるようにした
24
+
25
+ ### Removed
26
+
27
+ - `vernalEquinoxDay`・`autumnalEquinoxDay`(`bizdays-toolkit/jp`): 祝日の計算の内部の手段のため非公開にした
28
+ - `nthWeekdayOfMonth`: 内部の関数のため非公開にした
29
+
30
+ ### 開発
31
+
32
+ - 品質の確認を追加: タイムゾーン(`TZ`)を変えたテスト、性質のテスト、型のテスト、サイズの上限(`size-limits.json`)、公開 API のスナップショット(`etc/api.md`)
33
+ - 互換性の約束と安定度の表示(`@public`・`@beta`)を RELEASING.md に、設計上の決定を `docs/decisions.md` に記載
34
+ - `SECURITY.md` を追加(脆弱性の非公開の報告先)
35
+
7
36
  ## [0.2.0] - 2026-10-14
8
37
 
9
38
  ### Added
@@ -50,7 +79,8 @@
50
79
 
51
80
  - 日本: 2026 年 2 月の官報(2027 年の春分・秋分の日)まで反映(`dataVersion: "2026-02"`、`confirmedThrough: 2027`)
52
81
 
53
- [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.2.0...HEAD
82
+ [Unreleased]: https://github.com/inohan77/bizdays-toolkit/compare/v0.3.0...HEAD
83
+ [0.3.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.2.0...v0.3.0
54
84
  [0.2.0]: https://github.com/inohan77/bizdays-toolkit/compare/v0.1.2...v0.2.0
55
85
  [0.1.2]: https://github.com/inohan77/bizdays-toolkit/compare/v0.1.1...v0.1.2
56
86
  [0.1.1]: https://github.com/inohan77/bizdays-toolkit/compare/v0.1.0...v0.1.1
package/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  - **日ごとの値(営業時間など)**: 「火曜定休、祝日は日曜の営業時間、元日は休業、大晦日は短縮営業」のように、曜日・祝日・特定日で変わる値を同じ規則で引けます
21
21
  - **営業日の計算**: 定休日の曜日・毎年の休業日(年末年始のような年をまたぐ期間も可)・臨時休業日・臨時営業日と祝日を組み合わせ、「N 営業日後」「締め時刻を過ぎたら翌営業日」などを計算します
22
22
  - **タイムゾーンを間違えない**: `Date` などの瞬間は、カレンダーのタイムゾーン(日本は `Asia/Tokyo`)で暦日に変換してから判定します。UTC で動くサーバーでも、日本時間 0〜9 時が前日扱いになりません
23
- - **軽量・依存ゼロ**: minify + gzip で約 4KB(日本の祝日込みで約 5.5KB)。ESM・型定義同梱・`sideEffects: false` で、Node.js・ブラウザー・Edge ランタイムで動作します
23
+ - **軽量・依存ゼロ**: minify + gzip で約 5.5KB(日本の祝日込みで約 7KB)。ESM・型定義同梱・`sideEffects: false` で、Node.js・ブラウザー・Edge ランタイムで動作します
24
24
  - **他の国・地域に拡張できる**: 祝日の提供元はインターフェース(`HolidayProvider`)で差し替えられます。現在は日本のみ同梱しています
25
25
 
26
26
  [English](#english)
@@ -169,6 +169,24 @@ const calendar = createCalendar({ holidays: myCountry, closedWeekdays: [0, 6] })
169
169
 
170
170
  祝日の提供元の対応年(日本は 1955〜2099 年)を外れた日付を判定すると `RangeError` になります。祝日がない日として扱いたい場合は `outOfRange: "ignore"` を指定してください。
171
171
 
172
+ ### エラー
173
+
174
+ 設定や入力の誤りは、従来どおり `RangeError`・`TypeError` などの例外になり、`code` を持ちます。メッセージの文言は変わることがあるので、判定には `code` を使ってください。
175
+
176
+ ```ts
177
+ import { createCalendar, isBizdaysError } from "bizdays-toolkit";
178
+
179
+ try {
180
+ createCalendar({ closedDates: ["2026-12-32"] });
181
+ } catch (error) {
182
+ if (isBizdaysError(error) && error.code === "invalid-date") {
183
+ // 日付の誤り(メッセージには設定の中の位置 "closedDates[0].date" が入る)
184
+ }
185
+ }
186
+ ```
187
+
188
+ 主なコード: `invalid-date`・`invalid-month-day`・`invalid-time`・`invalid-range`・`invalid-weekday`・`invalid-time-zone`・`invalid-option`・`out-of-range`・`invalid-provider`・`invalid-provider-data`・`no-business-day`。将来の版でコードが増えることがあります。
189
+
172
190
  ## バージョン番号とデータの更新
173
191
 
174
192
  [Semantic Versioning](https://semver.org/lang/ja/) に従います。詳しくは [RELEASING.md](./RELEASING.md) を参照してください。
@@ -177,6 +195,7 @@ const calendar = createCalendar({ holidays: myCountry, closedWeekdays: [0, 6] })
177
195
  - 祝日の判定結果を固定したい場合は、版を完全一致で指定してください(例: `"bizdays-toolkit": "0.1.0"`)
178
196
  - 反映済みのデータは `dataVersion`(日本は反映済みの官報の年月)と `confirmedThrough` で確認できます
179
197
  - 1.0.0 までは、マイナー版で破壊的変更を行うことがあります
198
+ - 判定の理由(`reason`)・祝日の種別(`kind`)・エラーコードは、将来の版で値が増えることがあります。値を列挙して分岐するときは「その他」も扱ってください([RELEASING.md](./RELEASING.md) の「互換性の約束」)
180
199
 
181
200
  ## 出典
182
201
 
@@ -196,7 +215,7 @@ A business-day calendar that combines public holidays with company or store clos
196
215
  - **Day-by-day schedules**: `createSchedule` returns any value per day (e.g. store hours) from weekly values, a holiday rule (`"weekday"`, `{ sameAs: 0 }`, or `{ value }`), annual dates and specific dates
197
216
  - **Business-day arithmetic**: closed weekdays, annual closures (including ranges across the new year), specific closed/open dates, holiday policy, `addBusinessDays`, cut-off times, and more
198
217
  - **Time-zone safe**: instants (`Date`, ISO strings with offsets, Temporal) are converted to a civil date in the calendar's time zone before evaluation
199
- - **Lightweight, zero dependencies**: about 4 KB min+gzip (about 5.5 KB with Japanese holidays), ESM, bundled types, `sideEffects: false`
218
+ - **Lightweight, zero dependencies**: about 5.5 KB min+gzip (about 7 KB with Japanese holidays), ESM, bundled types, `sideEffects: false`
200
219
  - **Extensible** through the `HolidayProvider` interface (only Japan is bundled for now)
201
220
 
202
221
  ```ts
@@ -12,33 +12,53 @@
12
12
  * 内部では、営業日かどうか(真偽値)を返すスケジュール(createSchedule)として作っている。
13
13
  */
14
14
  import { type DateString, type Weekday } from "./date.ts";
15
+ import { IssueCollector } from "./errors.ts";
15
16
  import { type DateInput } from "./input.ts";
16
- import { type HolidayLookup, type HolidaySourceOptions } from "./schedule.ts";
17
+ import { type CompiledSchedule, type HolidayLookup, type HolidaySourceOptions } from "./schedule.ts";
17
18
  import type { Holiday } from "./types.ts";
18
- /** 祝日の扱い */
19
+ /**
20
+ * 祝日の扱い。
21
+ * @public
22
+ */
19
23
  export type HolidayPolicy = "closed" | "open" | "ignore";
20
- /** 毎年の月日 "MM-DD"(名前付きも可) */
24
+ /**
25
+ * 毎年の月日 "MM-DD"(名前付きも可)。
26
+ * @public
27
+ */
21
28
  export type AnnualDate = string | {
22
29
  readonly date: string;
23
30
  readonly name?: string;
24
31
  };
25
- /** 毎年の月日の期間(from > to なら年をまたぐ。例: 12-31 〜 01-03) */
32
+ /**
33
+ * 毎年の月日の期間(from > to なら年をまたぐ。例: 12-31 〜 01-03)。
34
+ * @public
35
+ */
26
36
  export interface AnnualRange {
27
37
  readonly from: string;
28
38
  readonly to: string;
29
39
  readonly name?: string;
30
40
  }
31
- /** 個別の日付 "YYYY-MM-DD"(名前付きも可) */
41
+ /**
42
+ * 個別の日付 "YYYY-MM-DD"(名前付きも可)。
43
+ * @public
44
+ */
32
45
  export type SpecificDate = DateString | {
33
46
  readonly date: DateString;
34
47
  readonly name?: string;
35
48
  };
36
- /** 個別の日付の期間(両端を含む) */
49
+ /**
50
+ * 個別の日付の期間(両端を含む)。
51
+ * @public
52
+ */
37
53
  export interface DateRange {
38
54
  readonly from: DateString;
39
55
  readonly to: DateString;
40
56
  readonly name?: string;
41
57
  }
58
+ /**
59
+ * 営業日カレンダーの設定。
60
+ * @public
61
+ */
42
62
  export interface CalendarOptions<Kind extends string = string> extends HolidaySourceOptions<Kind> {
43
63
  /** 定休日の曜日(0 = 日曜日) */
44
64
  readonly closedWeekdays?: readonly Weekday[];
@@ -53,9 +73,21 @@ export interface CalendarOptions<Kind extends string = string> extends HolidaySo
53
73
  /** 営業日を探すときに調べる最大日数(既定: 3660)。超えたら Error */
54
74
  readonly maxSearchDays?: number;
55
75
  }
56
- /** 営業・休業を決めた規則 */
57
- export type DayReason = "open-date" | "closed-date" | "closed-annual" | "holiday" | "closed-weekday" | "regular";
58
- /** 1 日分の判定結果 */
76
+ /**
77
+ * 営業・休業を決めた規則(既知の値)。
78
+ * @public
79
+ */
80
+ export type KnownDayReason = "open-date" | "closed-date" | "closed-annual" | "holiday" | "closed-weekday" | "regular";
81
+ /**
82
+ * 営業・休業を決めた規則。将来の版で値が増えることがあるので、分岐では「その他」も扱うこと。
83
+ * すべての値を網羅したことを型で確かめたい場合は KnownDayReason を使う。
84
+ * @public
85
+ */
86
+ export type DayReason = KnownDayReason | (string & {});
87
+ /**
88
+ * 1 日分の判定結果。
89
+ * @public
90
+ */
59
91
  export interface DayInfo<Kind extends string = string> {
60
92
  readonly date: DateString;
61
93
  readonly weekday: Weekday;
@@ -67,6 +99,10 @@ export interface DayInfo<Kind extends string = string> {
67
99
  /** その日が祝日なら祝日(holidayPolicy に関係なく付く) */
68
100
  readonly holiday?: Holiday<Kind>;
69
101
  }
102
+ /**
103
+ * 営業日を探すときのオプション。
104
+ * @public
105
+ */
70
106
  export interface BusinessDayOptions {
71
107
  /**
72
108
  * 締め時刻 "HH:mm"(カレンダーのタイムゾーン)。
@@ -74,6 +110,10 @@ export interface BusinessDayOptions {
74
110
  */
75
111
  readonly cutoff?: string;
76
112
  }
113
+ /**
114
+ * 営業日カレンダー。
115
+ * @public
116
+ */
77
117
  export interface Calendar<Kind extends string = string> extends HolidayLookup<Kind> {
78
118
  /** 1 日分の判定結果(営業日か・理由・祝日) */
79
119
  dayInfo(input: DateInput): DayInfo<Kind>;
@@ -97,5 +137,18 @@ export interface Calendar<Kind extends string = string> extends HolidayLookup<Ki
97
137
  /** 期間内の営業日(両端を含む) */
98
138
  listBusinessDays(from: DateInput, to: DateInput): readonly DateString[];
99
139
  }
100
- /** 祝日と休業日の規則から営業日カレンダーを作る */
140
+ interface CompiledCalendar<Kind extends string> {
141
+ readonly schedule: CompiledSchedule<boolean, Kind>;
142
+ readonly maxSearchDays: number;
143
+ }
144
+ /** @internal 営業日カレンダーの設定を検証して取り込む(誤りは c に集める) */
145
+ export declare function compileCalendarOptions<Kind extends string>(c: IssueCollector, options: unknown): CompiledCalendar<Kind> | undefined;
146
+ /**
147
+ * 祝日と休業日の規則から営業日カレンダーを作る。
148
+ *
149
+ * 設定は作る時点で取り込まれるので、あとから元の配列を書き換えても結果は変わらない。
150
+ * 設定の誤りは code 付きの RangeError になる。
151
+ * @public
152
+ */
101
153
  export declare function createCalendar<Kind extends string = string>(options?: CalendarOptions<Kind>): Calendar<Kind>;
154
+ export {};
@@ -12,59 +12,86 @@
12
12
  * 内部では、営業日かどうか(真偽値)を返すスケジュール(createSchedule)として作っている。
13
13
  */
14
14
  import { addDays, fromDayNumber, toDayNumber } from "./date.js";
15
+ import { checkUnknownKeys, IssueCollector, plainError, rangeError } from "./errors.js";
15
16
  import { parseTimeOfDay, toCivil } from "./input.js";
16
- import { createSchedule, parseMonthDay, } from "./schedule.js";
17
- function toDateValues(entries, value) {
18
- return (entries ?? []).map((e) => {
19
- if (typeof e === "string")
20
- return { date: e, value };
21
- if ("date" in e)
22
- return e.name === undefined ? { date: e.date, value } : { ...e, value };
23
- return e.name === undefined ? { from: e.from, to: e.to, value } : { ...e, value };
24
- });
25
- }
26
- /** 祝日と休業日の規則から営業日カレンダーを作る */
27
- export function createCalendar(options = {}) {
17
+ import { buildSchedule, compileAnnualRules, compileDateRules, compileHolidaySource, isWeekday, } from "./schedule.js";
18
+ const CALENDAR_OPTION_KEYS = [
19
+ "holidays",
20
+ "timeZone",
21
+ "outOfRange",
22
+ "closedWeekdays",
23
+ "closedAnnual",
24
+ "closedDates",
25
+ "openDates",
26
+ "holidayPolicy",
27
+ "maxSearchDays",
28
+ ];
29
+ /** @internal 営業日カレンダーの設定を検証して取り込む(誤りは c に集める) */
30
+ export function compileCalendarOptions(c, options) {
31
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
32
+ c.error("invalid-option", "", "options must be an object");
33
+ return undefined;
34
+ }
35
+ const o = options;
36
+ checkUnknownKeys(c, "", o, CALENDAR_OPTION_KEYS);
37
+ const source = compileHolidaySource(c, o);
28
38
  const closedWeekdays = new Set();
29
- for (const w of options.closedWeekdays ?? []) {
30
- if (!Number.isInteger(w) || w < 0 || w > 6)
31
- throw new RangeError(`Invalid weekday: ${w}`);
32
- closedWeekdays.add(w);
39
+ const rawWeekdays = o["closedWeekdays"];
40
+ if (rawWeekdays !== undefined) {
41
+ if (!Array.isArray(rawWeekdays)) {
42
+ c.error("invalid-option", "closedWeekdays", "must be an array");
43
+ }
44
+ else {
45
+ rawWeekdays.forEach((w, i) => {
46
+ if (isWeekday(w))
47
+ closedWeekdays.add(w);
48
+ else
49
+ c.error("invalid-weekday", `closedWeekdays[${i}]`, "must be an integer from 0 (Sunday) to 6 (Saturday)");
50
+ });
51
+ }
33
52
  }
34
- const holidayPolicy = options.holidayPolicy ?? "closed";
35
- if (!["closed", "open", "ignore"].includes(holidayPolicy)) {
36
- throw new RangeError(`Invalid holidayPolicy: ${String(holidayPolicy)}`);
53
+ const holidayPolicy = o["holidayPolicy"] ?? "closed";
54
+ if (holidayPolicy !== "closed" && holidayPolicy !== "open" && holidayPolicy !== "ignore") {
55
+ c.error("invalid-option", "holidayPolicy", 'must be "closed", "open" or "ignore"');
37
56
  }
38
- const maxSearchDays = options.maxSearchDays ?? 3660;
57
+ const maxSearchDays = o["maxSearchDays"] ?? 3660;
39
58
  if (!Number.isInteger(maxSearchDays) || maxSearchDays < 1) {
40
- throw new RangeError(`Invalid maxSearchDays: ${maxSearchDays}`);
59
+ c.error("invalid-option", "maxSearchDays", "must be a positive integer");
41
60
  }
42
- const weekly = [0, 1, 2, 3, 4, 5, 6].map((w) => !closedWeekdays.has(w));
43
- const sourceOptions = {
44
- ...(options.holidays !== undefined && { holidays: options.holidays }),
45
- ...(options.timeZone !== undefined && { timeZone: options.timeZone }),
46
- ...(options.outOfRange !== undefined && { outOfRange: options.outOfRange }),
61
+ const annual = compileAnnualRules(c, "closedAnnual", o["closedAnnual"], () => false, true);
62
+ const open = compileDateRules(c, "openDates", o["openDates"], () => true, true);
63
+ const closed = compileDateRules(c, "closedDates", o["closedDates"], () => false, true);
64
+ return {
65
+ schedule: {
66
+ source,
67
+ weekly: Object.freeze([0, 1, 2, 3, 4, 5, 6].map((w) => !closedWeekdays.has(w))),
68
+ holiday: holidayPolicy === "ignore" ? "weekday" : { value: holidayPolicy === "open" },
69
+ annual,
70
+ // 臨時営業日を先に並べて、臨時休業日より優先する
71
+ dates: [...open, ...closed],
72
+ },
73
+ maxSearchDays: Number.isInteger(maxSearchDays) ? maxSearchDays : 3660,
47
74
  };
48
- const schedule = createSchedule({
49
- ...sourceOptions,
50
- weekly,
51
- holiday: holidayPolicy === "ignore" ? "weekday" : { value: holidayPolicy === "open" },
52
- annual: (options.closedAnnual ?? []).map((e) => {
53
- if (typeof e === "string")
54
- return { date: parseMonthDay(e), value: false };
55
- if ("date" in e)
56
- return e.name === undefined ? { date: e.date, value: false } : { ...e, value: false };
57
- return e.name === undefined
58
- ? { from: e.from, to: e.to, value: false }
59
- : { ...e, value: false };
60
- }),
61
- // 臨時営業日を先に並べて、臨時休業日より優先する
62
- dates: [...toDateValues(options.openDates, true), ...toDateValues(options.closedDates, false)],
63
- });
75
+ }
76
+ /**
77
+ * 祝日と休業日の規則から営業日カレンダーを作る。
78
+ *
79
+ * 設定は作る時点で取り込まれるので、あとから元の配列を書き換えても結果は変わらない。
80
+ * 設定の誤りは code 付きの RangeError になる。
81
+ * @public
82
+ */
83
+ export function createCalendar(options = {}) {
84
+ const c = new IssueCollector();
85
+ const compiled = compileCalendarOptions(c, options);
86
+ c.throwFirstError();
87
+ const { maxSearchDays } = compiled;
88
+ const schedule = buildSchedule(compiled.schedule);
64
89
  function info(date) {
65
90
  const day = schedule.dayInfo(date);
66
91
  let reason;
67
- switch (day.reason) {
92
+ // 内部では閉じた型(KnownScheduleReason)で分岐し、値が増えたときの対応漏れを型で検出する
93
+ const known = day.reason;
94
+ switch (known) {
68
95
  case "date":
69
96
  reason = day.value ? "open-date" : "closed-date";
70
97
  break;
@@ -77,6 +104,10 @@ export function createCalendar(options = {}) {
77
104
  case "weekday":
78
105
  reason = day.value ? "regular" : "closed-weekday";
79
106
  break;
107
+ default: {
108
+ const unreachable = known;
109
+ throw plainError("invalid-option", `Unexpected schedule reason: ${String(unreachable)}`);
110
+ }
80
111
  }
81
112
  const result = {
82
113
  date: day.date,
@@ -100,7 +131,7 @@ export function createCalendar(options = {}) {
100
131
  if (isOpen(date))
101
132
  return date;
102
133
  }
103
- throw new Error(`No business day found within ${maxSearchDays} days from ${start}`);
134
+ throw plainError("no-business-day", `No business day found within ${maxSearchDays} days from ${start}`);
104
135
  }
105
136
  function countOpen(from, toExclusive) {
106
137
  let count = 0;
@@ -136,7 +167,7 @@ export function createCalendar(options = {}) {
136
167
  businessDayOnOrBefore: (input) => search(dateOf(input), -1),
137
168
  addBusinessDays(input, n) {
138
169
  if (!Number.isInteger(n))
139
- throw new RangeError(`n must be an integer: ${n}`);
170
+ throw rangeError("invalid-argument", `n must be an integer: ${n}`);
140
171
  let date = dateOf(input);
141
172
  if (n === 0)
142
173
  return search(date, 1);
@@ -155,7 +186,7 @@ export function createCalendar(options = {}) {
155
186
  const a = dateOf(from);
156
187
  const b = dateOf(to);
157
188
  if (a > b)
158
- throw new RangeError(`"from" must not be after "to" (${a} > ${b})`);
189
+ throw rangeError("invalid-range", `"from" must not be after "to" (${a} > ${b})`);
159
190
  const result = [];
160
191
  for (let n = toDayNumber(a); n <= toDayNumber(b); n++) {
161
192
  const date = fromDayNumber(n);
@@ -4,9 +4,15 @@
4
4
  * 日付は常に "YYYY-MM-DD" 文字列で扱い、内部では 1970-01-01 からの日数(day number)に
5
5
  * 変換して計算する。Date オブジェクトのローカル時刻は使わない。
6
6
  */
7
- /** 暦日を表す "YYYY-MM-DD" 文字列 */
7
+ /**
8
+ * 暦日を表す "YYYY-MM-DD" 文字列。
9
+ * @public
10
+ */
8
11
  export type DateString = string;
9
- /** 曜日(0 = 日曜日 … 6 = 土曜日。Date#getDay と同じ) */
12
+ /**
13
+ * 曜日(0 = 日曜日 … 6 = 土曜日。Date#getDay と同じ)。
14
+ * @public
15
+ */
10
16
  export type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6;
11
17
  /** 年月日に分解する。形式または暦として不正なら RangeError */
12
18
  export declare function parseDate(value: string): {
@@ -14,7 +20,10 @@ export declare function parseDate(value: string): {
14
20
  month: number;
15
21
  day: number;
16
22
  };
17
- /** "YYYY-MM-DD" として正しい暦日か */
23
+ /**
24
+ * "YYYY-MM-DD" として正しい暦日か。
25
+ * @public
26
+ */
18
27
  export declare function isValidDate(value: unknown): value is DateString;
19
28
  /** 年月日から "YYYY-MM-DD" を作る(範囲外なら RangeError) */
20
29
  export declare function formatDate(year: number, month: number, day: number): DateString;
@@ -26,11 +35,20 @@ export declare function daysInMonth(year: number, month: number): number;
26
35
  export declare function toDayNumber(date: DateString): number;
27
36
  /** 1970-01-01 からの日数を "YYYY-MM-DD" に戻す */
28
37
  export declare function fromDayNumber(dayNumber: number): DateString;
29
- /** n 日後(負なら前)の日付 */
38
+ /**
39
+ * n 日後(負なら前)の日付。
40
+ * @public
41
+ */
30
42
  export declare function addDays(date: DateString, days: number): DateString;
31
- /** from から to までの日数(to が後なら正) */
43
+ /**
44
+ * from から to までの日数(to が後なら正)。
45
+ * @public
46
+ */
32
47
  export declare function diffDays(from: DateString, to: DateString): number;
33
- /** 曜日(0 = 日曜日) */
48
+ /**
49
+ * 曜日(0 = 日曜日)。
50
+ * @public
51
+ */
34
52
  export declare function weekdayOf(date: DateString): Weekday;
35
53
  /** その月の第 n 何曜日(n は 1 始まり) */
36
54
  export declare function nthWeekdayOfMonth(year: number, month: number, weekday: Weekday, n: number): DateString;
package/dist/core/date.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * 日付は常に "YYYY-MM-DD" 文字列で扱い、内部では 1970-01-01 からの日数(day number)に
5
5
  * 変換して計算する。Date オブジェクトのローカル時刻は使わない。
6
6
  */
7
+ import { rangeError } from "./errors.js";
7
8
  const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
8
9
  const MS_PER_DAY = 86_400_000;
9
10
  const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
@@ -11,17 +12,20 @@ const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
11
12
  export function parseDate(value) {
12
13
  const m = DATE_RE.exec(value);
13
14
  if (!m) {
14
- throw new RangeError(`Invalid date string: ${JSON.stringify(value)} (expected YYYY-MM-DD)`);
15
+ throw rangeError("invalid-date", `Invalid date string: ${JSON.stringify(value)} (expected YYYY-MM-DD)`);
15
16
  }
16
17
  const year = Number(m[1]);
17
18
  const month = Number(m[2]);
18
19
  const day = Number(m[3]);
19
20
  if (year < 1000 || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {
20
- throw new RangeError(`Invalid date: ${value}`);
21
+ throw rangeError("invalid-date", `Invalid date: ${value}`);
21
22
  }
22
23
  return { year, month, day };
23
24
  }
24
- /** "YYYY-MM-DD" として正しい暦日か */
25
+ /**
26
+ * "YYYY-MM-DD" として正しい暦日か。
27
+ * @public
28
+ */
25
29
  export function isValidDate(value) {
26
30
  if (typeof value !== "string")
27
31
  return false;
@@ -36,7 +40,7 @@ export function isValidDate(value) {
36
40
  /** 年月日から "YYYY-MM-DD" を作る(範囲外なら RangeError) */
37
41
  export function formatDate(year, month, day) {
38
42
  if (!Number.isInteger(year) || year < 1000 || year > 9999) {
39
- throw new RangeError(`Year out of range (1000-9999): ${year}`);
43
+ throw rangeError("out-of-range", `Year out of range (1000-9999): ${year}`);
40
44
  }
41
45
  const s = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
42
46
  parseDate(s);
@@ -52,7 +56,7 @@ export function daysInMonth(year, month) {
52
56
  return isLeapYear(year) ? 29 : 28;
53
57
  const days = MONTH_DAYS[month - 1];
54
58
  if (days === undefined)
55
- throw new RangeError(`Invalid month: ${month}`);
59
+ throw rangeError("invalid-date", `Invalid month: ${month}`);
56
60
  return days;
57
61
  }
58
62
  /** 1970-01-01 からの日数 */
@@ -65,17 +69,27 @@ export function fromDayNumber(dayNumber) {
65
69
  const d = new Date(dayNumber * MS_PER_DAY);
66
70
  return formatDate(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
67
71
  }
68
- /** n 日後(負なら前)の日付 */
72
+ /**
73
+ * n 日後(負なら前)の日付。
74
+ * @public
75
+ */
69
76
  export function addDays(date, days) {
70
- if (!Number.isInteger(days))
71
- throw new RangeError(`days must be an integer: ${days}`);
77
+ if (!Number.isInteger(days)) {
78
+ throw rangeError("invalid-argument", `days must be an integer: ${days}`);
79
+ }
72
80
  return fromDayNumber(toDayNumber(date) + days);
73
81
  }
74
- /** from から to までの日数(to が後なら正) */
82
+ /**
83
+ * from から to までの日数(to が後なら正)。
84
+ * @public
85
+ */
75
86
  export function diffDays(from, to) {
76
87
  return toDayNumber(to) - toDayNumber(from);
77
88
  }
78
- /** 曜日(0 = 日曜日) */
89
+ /**
90
+ * 曜日(0 = 日曜日)。
91
+ * @public
92
+ */
79
93
  export function weekdayOf(date) {
80
94
  // 1970-01-01 は木曜日(4)
81
95
  return ((((toDayNumber(date) + 4) % 7) + 7) % 7);
@@ -0,0 +1,78 @@
1
+ /**
2
+ * コード付きの例外と、入力の問題を集める仕組み。
3
+ *
4
+ * 例外は従来どおり RangeError・TypeError・Error のインスタンスで、code プロパティを持つ。
5
+ * 例外のメッセージの文言は API に含まない(変わることがある)。判定には code を使う。
6
+ */
7
+ /**
8
+ * 既知のエラーコード(増えることがある)。
9
+ * @public
10
+ */
11
+ export type KnownErrorCode =
12
+ /** 日付 "YYYY-MM-DD" の形式・値の誤り */
13
+ "invalid-date"
14
+ /** 毎年の月日 "MM-DD" の形式・値の誤り */
15
+ | "invalid-month-day"
16
+ /** 時刻・日時の形式・値の誤り */
17
+ | "invalid-time"
18
+ /** 受け付けない種類の入力 */
19
+ | "invalid-input"
20
+ /** IANA タイムゾーン名の誤り */
21
+ | "invalid-time-zone"
22
+ /** 期間の開始が終了より後 */
23
+ | "invalid-range"
24
+ /** 曜日の番号(0〜6)の誤り */
25
+ | "invalid-weekday"
26
+ /** 引数の値の誤り(整数でない日数など) */
27
+ | "invalid-argument"
28
+ /** 設定の値の誤り */
29
+ | "invalid-option"
30
+ /** 祝日の提供元どうしのタイムゾーンが異なり、timeZone の指定が必要 */
31
+ | "time-zone-conflict"
32
+ /** 対応している年の範囲の外 */
33
+ | "out-of-range"
34
+ /** 祝日の提供元の形式の誤り */
35
+ | "invalid-provider"
36
+ /** 祝日の提供元が返したデータの誤り */
37
+ | "invalid-provider-data"
38
+ /** 指定した日数の範囲に営業日がない */
39
+ | "no-business-day";
40
+ /**
41
+ * エラーコード。将来の版で値が増えることがある。
42
+ * @public
43
+ */
44
+ export type ErrorCode = KnownErrorCode | (string & {});
45
+ /**
46
+ * このライブラリが投げた例外(code 付き)か。
47
+ * @public
48
+ */
49
+ export declare function isBizdaysError(error: unknown): error is Error & {
50
+ readonly code: ErrorCode;
51
+ };
52
+ /** @internal */
53
+ export declare function rangeError(code: KnownErrorCode, message: string): RangeError;
54
+ /** @internal */
55
+ export declare function typeError(code: KnownErrorCode, message: string): TypeError;
56
+ /** @internal */
57
+ export declare function plainError(code: KnownErrorCode, message: string): Error;
58
+ /** @internal 問題の重さ */
59
+ export type IssueSeverity = "error" | "warning" | "info";
60
+ /** @internal 設定の問題 1 件 */
61
+ export interface Issue {
62
+ readonly severity: IssueSeverity;
63
+ readonly code: string;
64
+ /** 設定の中の位置(例: "dates[1].from") */
65
+ readonly path: string;
66
+ readonly message: string;
67
+ }
68
+ /** @internal 問題を集める */
69
+ export declare class IssueCollector {
70
+ readonly issues: Issue[];
71
+ error(code: KnownErrorCode, path: string, message: string): void;
72
+ warning(code: string, path: string, message: string): void;
73
+ get hasErrors(): boolean;
74
+ /** 最初の誤りを例外にして投げる(誤りがなければ何もしない) */
75
+ throwFirstError(): void;
76
+ }
77
+ /** @internal 設定のオブジェクトに、知らないキーがあれば警告として記録する */
78
+ export declare function checkUnknownKeys(collector: IssueCollector, path: string, value: object, known: readonly string[]): void;