bizdays-toolkit 0.1.2 → 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.
@@ -8,160 +8,122 @@
8
8
  * 4. 祝日 … holidayPolicy に従う(closed: 休業 / open: 営業 / ignore: 判定に使わない)
9
9
  * 5. closedWeekdays … 定休日の曜日
10
10
  * 6. それ以外は営業日
11
+ *
12
+ * 内部では、営業日かどうか(真偽値)を返すスケジュール(createSchedule)として作っている。
11
13
  */
12
- import { addDays, fromDayNumber, parseDate, toDayNumber, weekdayOf, } from "./date.js";
13
- import { instantToCivil, parseTimeOfDay, toCivil, validateTimeZone, } from "./input.js";
14
- const MONTH_DAY_RE = /^(\d{2})-(\d{2})$/;
15
- const MAX_DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
16
- function parseMonthDay(value) {
17
- const m = MONTH_DAY_RE.exec(value);
18
- const month = Number(m?.[1]);
19
- const day = Number(m?.[2]);
20
- if (!m || month < 1 || month > 12 || day < 1 || day > MAX_DAYS_IN_MONTH[month - 1]) {
21
- throw new RangeError(`Invalid month-day: ${JSON.stringify(value)} (expected MM-DD)`);
14
+ import { addDays, fromDayNumber, toDayNumber } from "./date.js";
15
+ import { checkUnknownKeys, IssueCollector, plainError, rangeError } from "./errors.js";
16
+ import { parseTimeOfDay, toCivil } from "./input.js";
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;
22
34
  }
23
- return value;
24
- }
25
- function compileDates(entries, label) {
26
- return (entries ?? []).map((e) => {
27
- if (typeof e === "string") {
28
- const n = toDayNumber(e);
29
- return { from: n, to: n, name: undefined };
30
- }
31
- if ("date" in e) {
32
- const n = toDayNumber(e.date);
33
- return { from: n, to: n, name: e.name };
34
- }
35
- const from = toDayNumber(e.from);
36
- const to = toDayNumber(e.to);
37
- if (from > to)
38
- throw new RangeError(`${label}: "from" must not be after "to" (${e.from} > ${e.to})`);
39
- return { from, to, name: e.name };
40
- });
41
- }
42
- function compileAnnual(entries) {
43
- return (entries ?? []).map((e) => {
44
- if (typeof e === "string") {
45
- const md = parseMonthDay(e);
46
- return { from: md, to: md, name: undefined };
47
- }
48
- if ("date" in e) {
49
- const md = parseMonthDay(e.date);
50
- return { from: md, to: md, name: e.name };
35
+ const o = options;
36
+ checkUnknownKeys(c, "", o, CALENDAR_OPTION_KEYS);
37
+ const source = compileHolidaySource(c, o);
38
+ const closedWeekdays = new Set();
39
+ const rawWeekdays = o["closedWeekdays"];
40
+ if (rawWeekdays !== undefined) {
41
+ if (!Array.isArray(rawWeekdays)) {
42
+ c.error("invalid-option", "closedWeekdays", "must be an array");
51
43
  }
52
- return { from: parseMonthDay(e.from), to: parseMonthDay(e.to), name: e.name };
53
- });
54
- }
55
- function matchAnnual(rule, monthDay) {
56
- return rule.from <= rule.to
57
- ? monthDay >= rule.from && monthDay <= rule.to
58
- : monthDay >= rule.from || monthDay <= rule.to;
59
- }
60
- /** 祝日と休業日の規則から営業日カレンダーを作る */
61
- export function createCalendar(options = {}) {
62
- const providers = Object.freeze(options.holidays === undefined
63
- ? []
64
- : Array.isArray(options.holidays)
65
- ? [...options.holidays]
66
- : [options.holidays]);
67
- let timeZone;
68
- if (options.timeZone !== undefined) {
69
- timeZone = validateTimeZone(options.timeZone);
70
- }
71
- else {
72
- const zones = [...new Set(providers.map((p) => validateTimeZone(p.timeZone)))];
73
- if (zones.length > 1) {
74
- throw new RangeError(`Holiday providers use different time zones (${zones.join(", ")}); specify options.timeZone`);
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
+ });
75
51
  }
76
- timeZone = zones[0] ?? "UTC";
77
52
  }
78
- const closedWeekdays = new Set();
79
- for (const w of options.closedWeekdays ?? []) {
80
- if (!Number.isInteger(w) || w < 0 || w > 6)
81
- throw new RangeError(`Invalid weekday: ${w}`);
82
- closedWeekdays.add(w);
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"');
83
56
  }
84
- const closedAnnual = compileAnnual(options.closedAnnual);
85
- const closedDates = compileDates(options.closedDates, "closedDates");
86
- const openDates = compileDates(options.openDates, "openDates");
87
- const holidayPolicy = options.holidayPolicy ?? "closed";
88
- if (!["closed", "open", "ignore"].includes(holidayPolicy)) {
89
- throw new RangeError(`Invalid holidayPolicy: ${String(holidayPolicy)}`);
90
- }
91
- const outOfRange = options.outOfRange ?? "throw";
92
- const maxSearchDays = options.maxSearchDays ?? 3660;
57
+ const maxSearchDays = o["maxSearchDays"] ?? 3660;
93
58
  if (!Number.isInteger(maxSearchDays) || maxSearchDays < 1) {
94
- throw new RangeError(`Invalid maxSearchDays: ${maxSearchDays}`);
59
+ c.error("invalid-option", "maxSearchDays", "must be a positive integer");
95
60
  }
96
- const yearCache = new Map();
97
- function providerYears(provider, year) {
98
- const { from, to } = provider.supportedYears;
99
- if (year < from || year > to) {
100
- if (outOfRange === "ignore")
101
- return [];
102
- throw new RangeError(`Holidays for ${provider.id} are supported for ${from}-${to}; requested ${year}`);
103
- }
104
- return provider.holidaysInYear(year);
105
- }
106
- function holidayMap(year) {
107
- let map = yearCache.get(year);
108
- if (!map) {
109
- map = new Map();
110
- for (const provider of providers) {
111
- for (const h of providerYears(provider, year)) {
112
- // 同じ日に複数の提供元の祝日がある場合は、先に渡した提供元を優先する
113
- if (!map.has(h.date))
114
- map.set(h.date, h);
115
- }
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,
74
+ };
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);
89
+ function info(date) {
90
+ const day = schedule.dayInfo(date);
91
+ let reason;
92
+ // 内部では閉じた型(KnownScheduleReason)で分岐し、値が増えたときの対応漏れを型で検出する
93
+ const known = day.reason;
94
+ switch (known) {
95
+ case "date":
96
+ reason = day.value ? "open-date" : "closed-date";
97
+ break;
98
+ case "annual":
99
+ reason = "closed-annual";
100
+ break;
101
+ case "holiday":
102
+ reason = "holiday";
103
+ break;
104
+ case "weekday":
105
+ reason = day.value ? "regular" : "closed-weekday";
106
+ break;
107
+ default: {
108
+ const unreachable = known;
109
+ throw plainError("invalid-option", `Unexpected schedule reason: ${String(unreachable)}`);
116
110
  }
117
- yearCache.set(year, map);
118
111
  }
119
- return map;
120
- }
121
- const dateOf = (input) => toCivil(input, timeZone).date;
122
- function holidayOn(date) {
123
- if (providers.length === 0)
124
- return undefined;
125
- return holidayMap(parseDate(date).year).get(date);
126
- }
127
- function findRule(rules, dayNumber) {
128
- return rules.find((r) => dayNumber >= r.from && dayNumber <= r.to);
129
- }
130
- function info(date) {
131
- const dayNumber = toDayNumber(date);
132
- const weekday = weekdayOf(date);
133
- const holiday = holidayOn(date);
134
- const make = (businessDay, reason, name) => {
135
- const result = {
136
- date,
137
- weekday,
138
- businessDay,
139
- reason,
140
- };
141
- if (name !== undefined)
142
- result.name = name;
143
- if (holiday !== undefined)
144
- result.holiday = holiday;
145
- return Object.freeze(result);
112
+ const result = {
113
+ date: day.date,
114
+ weekday: day.weekday,
115
+ businessDay: day.value,
116
+ reason,
146
117
  };
147
- const open = findRule(openDates, dayNumber);
148
- if (open)
149
- return make(true, "open-date", open.name);
150
- const closed = findRule(closedDates, dayNumber);
151
- if (closed)
152
- return make(false, "closed-date", closed.name);
153
- const monthDay = date.slice(5);
154
- const annual = closedAnnual.find((r) => matchAnnual(r, monthDay));
155
- if (annual)
156
- return make(false, "closed-annual", annual.name);
157
- if (holiday && holidayPolicy !== "ignore") {
158
- return make(holidayPolicy === "open", "holiday", holiday.name);
159
- }
160
- if (closedWeekdays.has(weekday))
161
- return make(false, "closed-weekday", undefined);
162
- return make(true, "regular", undefined);
118
+ // 曜日で決まった日には名前を付けない(祝日名は holiday で分かる)
119
+ if (day.name !== undefined && day.reason !== "weekday")
120
+ result.name = day.name;
121
+ if (day.holiday !== undefined)
122
+ result.holiday = day.holiday;
123
+ return Object.freeze(result);
163
124
  }
164
- const isOpen = (date) => info(date).businessDay;
125
+ const isOpen = (date) => schedule.valueOn(date);
126
+ const dateOf = schedule.dateOf;
165
127
  function search(start, step) {
166
128
  let n = toDayNumber(start);
167
129
  for (let i = 0; i < maxSearchDays; i++, n += step) {
@@ -169,14 +131,7 @@ export function createCalendar(options = {}) {
169
131
  if (isOpen(date))
170
132
  return date;
171
133
  }
172
- throw new Error(`No business day found within ${maxSearchDays} days from ${start}`);
173
- }
174
- function ordered(from, to) {
175
- const a = dateOf(from);
176
- const b = dateOf(to);
177
- if (a > b)
178
- throw new RangeError(`"from" must not be after "to" (${a} > ${b})`);
179
- return [a, b];
134
+ throw plainError("no-business-day", `No business day found within ${maxSearchDays} days from ${start}`);
180
135
  }
181
136
  function countOpen(from, toExclusive) {
182
137
  let count = 0;
@@ -186,33 +141,21 @@ export function createCalendar(options = {}) {
186
141
  return count;
187
142
  }
188
143
  const calendar = {
189
- timeZone,
190
- providers,
144
+ timeZone: schedule.timeZone,
145
+ providers: schedule.providers,
191
146
  dateOf,
192
- today: (now = new Date()) => instantToCivil(now.getTime(), timeZone).date,
193
- isHoliday: (input) => holidayOn(dateOf(input)) !== undefined,
194
- getHoliday: (input) => holidayOn(dateOf(input)),
195
- holidaysInYear(year) {
196
- if (!Number.isInteger(year))
197
- throw new RangeError(`Invalid year: ${year}`);
198
- return Object.freeze([...holidayMap(year).values()].sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
199
- },
200
- holidaysBetween(from, to) {
201
- const [a, b] = ordered(from, to);
202
- const result = [];
203
- for (let y = parseDate(a).year; y <= parseDate(b).year; y++) {
204
- for (const h of calendar.holidaysInYear(y))
205
- if (h.date >= a && h.date <= b)
206
- result.push(h);
207
- }
208
- return Object.freeze(result);
209
- },
147
+ civilOf: schedule.civilOf,
148
+ today: schedule.today,
149
+ isHoliday: schedule.isHoliday,
150
+ getHoliday: schedule.getHoliday,
151
+ holidaysInYear: schedule.holidaysInYear,
152
+ holidaysBetween: schedule.holidaysBetween,
210
153
  dayInfo: (input) => info(dateOf(input)),
211
154
  isBusinessDay: (input) => isOpen(dateOf(input)),
212
155
  nextBusinessDay: (input) => search(addDays(dateOf(input), 1), 1),
213
156
  previousBusinessDay: (input) => search(addDays(dateOf(input), -1), -1),
214
157
  businessDayOnOrAfter(input, opts = {}) {
215
- const civil = toCivil(input, timeZone);
158
+ const civil = toCivil(input, schedule.timeZone);
216
159
  let start = civil.date;
217
160
  if (opts.cutoff !== undefined) {
218
161
  const cutoff = parseTimeOfDay(opts.cutoff);
@@ -224,7 +167,7 @@ export function createCalendar(options = {}) {
224
167
  businessDayOnOrBefore: (input) => search(dateOf(input), -1),
225
168
  addBusinessDays(input, n) {
226
169
  if (!Number.isInteger(n))
227
- throw new RangeError(`n must be an integer: ${n}`);
170
+ throw rangeError("invalid-argument", `n must be an integer: ${n}`);
228
171
  let date = dateOf(input);
229
172
  if (n === 0)
230
173
  return search(date, 1);
@@ -240,7 +183,10 @@ export function createCalendar(options = {}) {
240
183
  return a <= b ? countOpen(a, b) : -countOpen(b, a);
241
184
  },
242
185
  listBusinessDays(from, to) {
243
- const [a, b] = ordered(from, to);
186
+ const a = dateOf(from);
187
+ const b = dateOf(to);
188
+ if (a > b)
189
+ throw rangeError("invalid-range", `"from" must not be after "to" (${a} > ${b})`);
244
190
  const result = [];
245
191
  for (let n = toDayNumber(a); n <= toDayNumber(b); n++) {
246
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;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * コード付きの例外と、入力の問題を集める仕組み。
3
+ *
4
+ * 例外は従来どおり RangeError・TypeError・Error のインスタンスで、code プロパティを持つ。
5
+ * 例外のメッセージの文言は API に含まない(変わることがある)。判定には code を使う。
6
+ */
7
+ /**
8
+ * このライブラリが投げた例外(code 付き)か。
9
+ * @public
10
+ */
11
+ export function isBizdaysError(error) {
12
+ return (error instanceof Error &&
13
+ typeof error.code === "string" &&
14
+ error.bizdays === true);
15
+ }
16
+ function withCode(error, code) {
17
+ Object.defineProperty(error, "code", { value: code, enumerable: true });
18
+ Object.defineProperty(error, "bizdays", { value: true, enumerable: false });
19
+ return error;
20
+ }
21
+ /** @internal */
22
+ export function rangeError(code, message) {
23
+ return withCode(new RangeError(message), code);
24
+ }
25
+ /** @internal */
26
+ export function typeError(code, message) {
27
+ return withCode(new TypeError(message), code);
28
+ }
29
+ /** @internal */
30
+ export function plainError(code, message) {
31
+ return withCode(new Error(message), code);
32
+ }
33
+ /** @internal 問題を集める */
34
+ export class IssueCollector {
35
+ issues = [];
36
+ error(code, path, message) {
37
+ this.issues.push({ severity: "error", code, path, message });
38
+ }
39
+ warning(code, path, message) {
40
+ this.issues.push({ severity: "warning", code, path, message });
41
+ }
42
+ get hasErrors() {
43
+ return this.issues.some((i) => i.severity === "error");
44
+ }
45
+ /** 最初の誤りを例外にして投げる(誤りがなければ何もしない) */
46
+ throwFirstError() {
47
+ const first = this.issues.find((i) => i.severity === "error");
48
+ if (!first)
49
+ return;
50
+ const message = first.path ? `${first.path}: ${first.message}` : first.message;
51
+ throw rangeError(first.code, message);
52
+ }
53
+ }
54
+ /** @internal 設定のオブジェクトに、知らないキーがあれば警告として記録する */
55
+ export function checkUnknownKeys(collector, path, value, known) {
56
+ for (const key of Object.keys(value)) {
57
+ if (!known.includes(key)) {
58
+ collector.warning("unknown-option", path ? `${path}.${key}` : key, `Unknown option ${JSON.stringify(key)} is ignored`);
59
+ }
60
+ }
61
+ }