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.
- package/CHANGELOG.md +43 -1
- package/README.md +60 -2
- package/dist/core/calendar.d.ts +68 -37
- package/dist/core/calendar.js +119 -173
- package/dist/core/date.d.ts +24 -6
- package/dist/core/date.js +24 -10
- package/dist/core/errors.d.ts +78 -0
- package/dist/core/errors.js +61 -0
- package/dist/core/input.d.ts +29 -9
- package/dist/core/input.js +48 -20
- package/dist/core/schedule.d.ts +224 -0
- package/dist/core/schedule.js +426 -0
- package/dist/core/types.d.ts +11 -3
- package/dist/index.d.ts +10 -4
- package/dist/index.js +9 -3
- package/dist/jp/index.d.ts +45 -11
- package/dist/jp/index.js +39 -9
- package/dist/jp/rules.d.ts +17 -8
- package/dist/jp/rules.js +8 -7
- package/package.json +6 -4
package/dist/core/calendar.js
CHANGED
|
@@ -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,
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
-
|
|
59
|
+
c.error("invalid-option", "maxSearchDays", "must be a positive integer");
|
|
95
60
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
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
|
|
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);
|
package/dist/core/date.d.ts
CHANGED
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
* 日付は常に "YYYY-MM-DD" 文字列で扱い、内部では 1970-01-01 からの日数(day number)に
|
|
5
5
|
* 変換して計算する。Date オブジェクトのローカル時刻は使わない。
|
|
6
6
|
*/
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* 暦日を表す "YYYY-MM-DD" 文字列。
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
8
11
|
export type DateString = string;
|
|
9
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* n 日後(負なら前)の日付。
|
|
40
|
+
* @public
|
|
41
|
+
*/
|
|
30
42
|
export declare function addDays(date: DateString, days: number): DateString;
|
|
31
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* from から to までの日数(to が後なら正)。
|
|
45
|
+
* @public
|
|
46
|
+
*/
|
|
32
47
|
export declare function diffDays(from: DateString, to: DateString): number;
|
|
33
|
-
/**
|
|
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
|
|
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
|
|
21
|
+
throw rangeError("invalid-date", `Invalid date: ${value}`);
|
|
21
22
|
}
|
|
22
23
|
return { year, month, day };
|
|
23
24
|
}
|
|
24
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* n 日後(負なら前)の日付。
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
69
76
|
export function addDays(date, days) {
|
|
70
|
-
if (!Number.isInteger(days))
|
|
71
|
-
throw
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
+
}
|