@sanguo-dev/calendar 1.0.0 → 1.0.1
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/LICENSE +21 -21
- package/README.md +229 -229
- package/package.json +68 -68
- package/src/config.ts +198 -198
- package/src/data/Calendar.ts +599 -599
- package/src/data/festival.json +195 -195
- package/src/data/rest.json +74 -74
- package/src/index.ts +317 -317
- package/src/types.ts +146 -146
- package/src/utils/Assert.ts +230 -230
- package/src/utils/LunarFunction.ts +215 -215
- package/src/utils/SetFestival.ts +40 -40
- package/src/utils/SetRest.ts +84 -84
- package/src/utils/SolarFunction.ts +162 -162
package/src/utils/Assert.ts
CHANGED
|
@@ -1,230 +1,230 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 统一的参数错误、类型守卫,以及节日 / 调休配置的校验与规范化
|
|
3
|
-
*/
|
|
4
|
-
import type { FestivalMap, RestMap } from "../types";
|
|
5
|
-
|
|
6
|
-
/** 四位 "MMDD",月份 01-12、日期 01-31 */
|
|
7
|
-
const MMDD_RE = /^(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])$/;
|
|
8
|
-
|
|
9
|
-
/** 预生成的 MMDD 查表键(12 × 31),避免每次查节日 / 调休都拼字符串 */
|
|
10
|
-
const MMDD: string[][] = Array.from({ length: 13 }, () => Array.from({ length: 32 }, () => ""));
|
|
11
|
-
|
|
12
|
-
for (let m = 1; m <= 12; m++) {
|
|
13
|
-
for (let d = 1; d <= 31; d++) {
|
|
14
|
-
(MMDD[m] as string[])[d] = (m < 10 ? "0" + m : String(m)) + (d < 10 ? "0" + d : String(d));
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** 取四位 MMDD 字符串,例如 mmddOf(1, 1) → "0101" */
|
|
19
|
-
export const mmddOf = function (m: number, d: number): string {
|
|
20
|
-
return (MMDD[m] as string[])[d] as string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/** 调休年份:1900-2100 */
|
|
24
|
-
const YEAR_RE = /^(19\d{2}|20\d{2}|2100)$/;
|
|
25
|
-
|
|
26
|
-
/** 公历每个月的最大天数,2 月按闰年放宽到 29 */
|
|
27
|
-
const SOLAR_MAX_DAY = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
28
|
-
|
|
29
|
-
/** 调休表每年的字段 */
|
|
30
|
-
const REST_FIELDS = ["xiu", "ban"];
|
|
31
|
-
|
|
32
|
-
/** 抛出带位置信息的 TypeError,用于配置格式错误 */
|
|
33
|
-
export function failType(path: string, message: string): never {
|
|
34
|
-
throw new TypeError(`[calendar] ${path} ${message}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** 抛出带位置信息的 RangeError,用于数值越界 */
|
|
38
|
-
export function failRange(path: string, message: string): never {
|
|
39
|
-
throw new RangeError(`[calendar] ${path} ${message}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 是否为普通对象(非 null、非数组) */
|
|
43
|
-
export const isPlainObject = function (value: unknown): value is Record<string, unknown> {
|
|
44
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/** 校验是普通对象,否则按 path 抛 TypeError;断言之后可以直接当字典用 */
|
|
48
|
-
export function assertPlainObject(
|
|
49
|
-
value: unknown,
|
|
50
|
-
path: string,
|
|
51
|
-
message = "必须是对象",
|
|
52
|
-
): asserts value is Record<string, unknown> {
|
|
53
|
-
if (!isPlainObject(value)) failType(path, `${message},收到 ${describe(value)}`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** 校验对象里没有白名单之外的字段 */
|
|
57
|
-
export const assertKnownKeys = function (
|
|
58
|
-
value: object,
|
|
59
|
-
allowed: readonly string[],
|
|
60
|
-
path: string,
|
|
61
|
-
): void {
|
|
62
|
-
Object.keys(value).forEach(function (key) {
|
|
63
|
-
if (allowed.indexOf(key) === -1) {
|
|
64
|
-
failType(`${path}.${key}`, `不认识的字段,只支持 ${allowed.join(" 与 ")}`);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/** 把任意值格式化成便于排查的短字符串 */
|
|
70
|
-
export const describe = function (value: unknown): string {
|
|
71
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
72
|
-
if (
|
|
73
|
-
typeof value === "number" ||
|
|
74
|
-
typeof value === "boolean" ||
|
|
75
|
-
value === null ||
|
|
76
|
-
value === undefined
|
|
77
|
-
) {
|
|
78
|
-
return String(value);
|
|
79
|
-
}
|
|
80
|
-
if (Array.isArray(value)) return `Array(${value.length})`;
|
|
81
|
-
if (typeof value === "object") {
|
|
82
|
-
const keys = Object.keys(value as Record<string, unknown>);
|
|
83
|
-
return `Object(keys: ${keys.slice(0, 5).join(", ")}${keys.length > 5 ? ", …" : ""})`;
|
|
84
|
-
}
|
|
85
|
-
return typeof value;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/** 校验一个整数并返回它 */
|
|
89
|
-
export const assertInteger = function (
|
|
90
|
-
path: string,
|
|
91
|
-
value: unknown,
|
|
92
|
-
min: number,
|
|
93
|
-
max: number,
|
|
94
|
-
): number {
|
|
95
|
-
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
96
|
-
return failRange(path, `必须是整数,收到 ${describe(value)}`);
|
|
97
|
-
}
|
|
98
|
-
if (value < min || value > max) {
|
|
99
|
-
return failRange(path, `必须在 ${min}-${max} 之间,收到 ${value}`);
|
|
100
|
-
}
|
|
101
|
-
return value;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/** 公历 MMDD 是否对应真实存在的日期(例如 0231、0431 为 false) */
|
|
105
|
-
const isSolarMonthDay = function (m: number, d: number): boolean {
|
|
106
|
-
return d >= 1 && d <= (SOLAR_MAX_DAY[m - 1] ?? 0);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
/** 农历 MMDD:每个月最多 30 天 */
|
|
110
|
-
const isLunarMonthDay = function (m: number, d: number): boolean {
|
|
111
|
-
return d >= 1 && d <= 30;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 校验并解析四位 MMDD:节日 key 与调休日期共用同一套格式和日期检查
|
|
116
|
-
*
|
|
117
|
-
* @param value 待校验值,必须是 "MMDD" 字符串
|
|
118
|
-
* @param path 出错时用于定位的路径
|
|
119
|
-
* @param options.lunar 按农历校验(每月最多 30 天),默认公历
|
|
120
|
-
* @param options.label 报错信息里的主语,例如 "节日 key"
|
|
121
|
-
*/
|
|
122
|
-
export function assertMMDD(
|
|
123
|
-
value: unknown,
|
|
124
|
-
path: string,
|
|
125
|
-
options: { lunar?: boolean; label?: string } = {},
|
|
126
|
-
): { month: number; day: number; text: string } {
|
|
127
|
-
const { lunar = false, label } = options;
|
|
128
|
-
const prefix = label === undefined ? "" : `${label} `;
|
|
129
|
-
const parsed =
|
|
130
|
-
typeof value === "string" && MMDD_RE.test(value)
|
|
131
|
-
? { month: Number(value.slice(0, 2)), day: Number(value.slice(2)), text: value }
|
|
132
|
-
: null;
|
|
133
|
-
|
|
134
|
-
if (parsed === null) {
|
|
135
|
-
failType(path, `${prefix}必须是四位 "MMDD"(如 "0101"),收到 ${describe(value)}`);
|
|
136
|
-
}
|
|
137
|
-
const valid = lunar
|
|
138
|
-
? isLunarMonthDay(parsed.month, parsed.day)
|
|
139
|
-
: isSolarMonthDay(parsed.month, parsed.day);
|
|
140
|
-
if (!valid) {
|
|
141
|
-
failType(path, `${lunar ? "农历" : "公历"}不存在 ${parsed.month} 月 ${parsed.day} 日`);
|
|
142
|
-
}
|
|
143
|
-
return parsed;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 校验并规范化节日表
|
|
148
|
-
*
|
|
149
|
-
* @param input 待校验的节日表
|
|
150
|
-
* @param path 出错时用于定位的路径,例如 "createCalendar.setFestival.sFtv"
|
|
151
|
-
* @param lunar 农历节日(true)还是公历节日(false)
|
|
152
|
-
*/
|
|
153
|
-
const normalizeFestivalMap = function (input: unknown, path: string, lunar = false): FestivalMap {
|
|
154
|
-
assertPlainObject(input, path);
|
|
155
|
-
const result: FestivalMap = {};
|
|
156
|
-
Object.keys(input).forEach(function (key) {
|
|
157
|
-
assertMMDD(key, `${path}.${key}`, { lunar, label: "节日 key" });
|
|
158
|
-
const value = input[key];
|
|
159
|
-
if (!Array.isArray(value)) {
|
|
160
|
-
throw failType(`${path}.${key}`, `节日名称必须是字符串数组,收到 ${describe(value)}`);
|
|
161
|
-
}
|
|
162
|
-
const names: string[] = [];
|
|
163
|
-
value.forEach(function (name: unknown, index: number) {
|
|
164
|
-
if (typeof name !== "string" || name.trim() === "") {
|
|
165
|
-
throw failType(
|
|
166
|
-
`${path}.${key}[${index}]`,
|
|
167
|
-
`节日名称必须是非空字符串,收到 ${describe(name)}`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
names.push(name);
|
|
171
|
-
});
|
|
172
|
-
result[key] = names;
|
|
173
|
-
});
|
|
174
|
-
return result;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
/** 校验并去重一组调休日期(MMDD 字符串) */
|
|
178
|
-
const normalizeDayList = function (input: unknown, path: string): string[] {
|
|
179
|
-
if (!Array.isArray(input)) {
|
|
180
|
-
throw failType(path, `必须是 "MMDD" 字符串数组,收到 ${describe(input)}`);
|
|
181
|
-
}
|
|
182
|
-
const days: string[] = [];
|
|
183
|
-
input.forEach(function (day: unknown, index: number) {
|
|
184
|
-
days.push(assertMMDD(day, `${path}[${index}]`).text);
|
|
185
|
-
});
|
|
186
|
-
return Array.from(new Set(days));
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* 校验并规范化调休表,key 为年份
|
|
191
|
-
*
|
|
192
|
-
* @param input 待校验的调休表
|
|
193
|
-
* @param path 出错时用于定位的路径
|
|
194
|
-
*/
|
|
195
|
-
const normalizeRestMap = function (input: unknown, path: string): RestMap {
|
|
196
|
-
assertPlainObject(input, path);
|
|
197
|
-
const result: RestMap = {};
|
|
198
|
-
Object.keys(input).forEach(function (yearKey) {
|
|
199
|
-
if (!YEAR_RE.test(yearKey)) {
|
|
200
|
-
throw failType(
|
|
201
|
-
`${path}.${yearKey}`,
|
|
202
|
-
`年份必须是 1900-2100 的四位数字,收到 ${describe(yearKey)}`,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
const entry = input[yearKey];
|
|
206
|
-
assertPlainObject(entry, `${path}.${yearKey}`, "必须是 { xiu: [], ban: [] }");
|
|
207
|
-
assertKnownKeys(entry, REST_FIELDS, `${path}.${yearKey}`);
|
|
208
|
-
|
|
209
|
-
const xiu =
|
|
210
|
-
entry.xiu === undefined ? [] : normalizeDayList(entry.xiu, `${path}.${yearKey}.xiu`);
|
|
211
|
-
const ban =
|
|
212
|
-
entry.ban === undefined ? [] : normalizeDayList(entry.ban, `${path}.${yearKey}.ban`);
|
|
213
|
-
|
|
214
|
-
// 放假和补班来自同一份法定节假日安排,同一天不可能既放假又补班
|
|
215
|
-
const conflict = xiu.filter(function (day) {
|
|
216
|
-
return ban.indexOf(day) !== -1;
|
|
217
|
-
});
|
|
218
|
-
if (conflict.length > 0) {
|
|
219
|
-
throw failType(
|
|
220
|
-
`${path}.${yearKey}`,
|
|
221
|
-
`同一天不能既是放假日又是补班日:${conflict.join("、")}`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
result[yearKey] = { xiu, ban };
|
|
226
|
-
});
|
|
227
|
-
return result;
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
export { normalizeFestivalMap, normalizeRestMap };
|
|
1
|
+
/**
|
|
2
|
+
* 统一的参数错误、类型守卫,以及节日 / 调休配置的校验与规范化
|
|
3
|
+
*/
|
|
4
|
+
import type { FestivalMap, RestMap } from "../types";
|
|
5
|
+
|
|
6
|
+
/** 四位 "MMDD",月份 01-12、日期 01-31 */
|
|
7
|
+
const MMDD_RE = /^(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])$/;
|
|
8
|
+
|
|
9
|
+
/** 预生成的 MMDD 查表键(12 × 31),避免每次查节日 / 调休都拼字符串 */
|
|
10
|
+
const MMDD: string[][] = Array.from({ length: 13 }, () => Array.from({ length: 32 }, () => ""));
|
|
11
|
+
|
|
12
|
+
for (let m = 1; m <= 12; m++) {
|
|
13
|
+
for (let d = 1; d <= 31; d++) {
|
|
14
|
+
(MMDD[m] as string[])[d] = (m < 10 ? "0" + m : String(m)) + (d < 10 ? "0" + d : String(d));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 取四位 MMDD 字符串,例如 mmddOf(1, 1) → "0101" */
|
|
19
|
+
export const mmddOf = function (m: number, d: number): string {
|
|
20
|
+
return (MMDD[m] as string[])[d] as string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** 调休年份:1900-2100 */
|
|
24
|
+
const YEAR_RE = /^(19\d{2}|20\d{2}|2100)$/;
|
|
25
|
+
|
|
26
|
+
/** 公历每个月的最大天数,2 月按闰年放宽到 29 */
|
|
27
|
+
const SOLAR_MAX_DAY = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
28
|
+
|
|
29
|
+
/** 调休表每年的字段 */
|
|
30
|
+
const REST_FIELDS = ["xiu", "ban"];
|
|
31
|
+
|
|
32
|
+
/** 抛出带位置信息的 TypeError,用于配置格式错误 */
|
|
33
|
+
export function failType(path: string, message: string): never {
|
|
34
|
+
throw new TypeError(`[calendar] ${path} ${message}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 抛出带位置信息的 RangeError,用于数值越界 */
|
|
38
|
+
export function failRange(path: string, message: string): never {
|
|
39
|
+
throw new RangeError(`[calendar] ${path} ${message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 是否为普通对象(非 null、非数组) */
|
|
43
|
+
export const isPlainObject = function (value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** 校验是普通对象,否则按 path 抛 TypeError;断言之后可以直接当字典用 */
|
|
48
|
+
export function assertPlainObject(
|
|
49
|
+
value: unknown,
|
|
50
|
+
path: string,
|
|
51
|
+
message = "必须是对象",
|
|
52
|
+
): asserts value is Record<string, unknown> {
|
|
53
|
+
if (!isPlainObject(value)) failType(path, `${message},收到 ${describe(value)}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 校验对象里没有白名单之外的字段 */
|
|
57
|
+
export const assertKnownKeys = function (
|
|
58
|
+
value: object,
|
|
59
|
+
allowed: readonly string[],
|
|
60
|
+
path: string,
|
|
61
|
+
): void {
|
|
62
|
+
Object.keys(value).forEach(function (key) {
|
|
63
|
+
if (allowed.indexOf(key) === -1) {
|
|
64
|
+
failType(`${path}.${key}`, `不认识的字段,只支持 ${allowed.join(" 与 ")}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** 把任意值格式化成便于排查的短字符串 */
|
|
70
|
+
export const describe = function (value: unknown): string {
|
|
71
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
72
|
+
if (
|
|
73
|
+
typeof value === "number" ||
|
|
74
|
+
typeof value === "boolean" ||
|
|
75
|
+
value === null ||
|
|
76
|
+
value === undefined
|
|
77
|
+
) {
|
|
78
|
+
return String(value);
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(value)) return `Array(${value.length})`;
|
|
81
|
+
if (typeof value === "object") {
|
|
82
|
+
const keys = Object.keys(value as Record<string, unknown>);
|
|
83
|
+
return `Object(keys: ${keys.slice(0, 5).join(", ")}${keys.length > 5 ? ", …" : ""})`;
|
|
84
|
+
}
|
|
85
|
+
return typeof value;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** 校验一个整数并返回它 */
|
|
89
|
+
export const assertInteger = function (
|
|
90
|
+
path: string,
|
|
91
|
+
value: unknown,
|
|
92
|
+
min: number,
|
|
93
|
+
max: number,
|
|
94
|
+
): number {
|
|
95
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
96
|
+
return failRange(path, `必须是整数,收到 ${describe(value)}`);
|
|
97
|
+
}
|
|
98
|
+
if (value < min || value > max) {
|
|
99
|
+
return failRange(path, `必须在 ${min}-${max} 之间,收到 ${value}`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** 公历 MMDD 是否对应真实存在的日期(例如 0231、0431 为 false) */
|
|
105
|
+
const isSolarMonthDay = function (m: number, d: number): boolean {
|
|
106
|
+
return d >= 1 && d <= (SOLAR_MAX_DAY[m - 1] ?? 0);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** 农历 MMDD:每个月最多 30 天 */
|
|
110
|
+
const isLunarMonthDay = function (m: number, d: number): boolean {
|
|
111
|
+
return d >= 1 && d <= 30;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 校验并解析四位 MMDD:节日 key 与调休日期共用同一套格式和日期检查
|
|
116
|
+
*
|
|
117
|
+
* @param value 待校验值,必须是 "MMDD" 字符串
|
|
118
|
+
* @param path 出错时用于定位的路径
|
|
119
|
+
* @param options.lunar 按农历校验(每月最多 30 天),默认公历
|
|
120
|
+
* @param options.label 报错信息里的主语,例如 "节日 key"
|
|
121
|
+
*/
|
|
122
|
+
export function assertMMDD(
|
|
123
|
+
value: unknown,
|
|
124
|
+
path: string,
|
|
125
|
+
options: { lunar?: boolean; label?: string } = {},
|
|
126
|
+
): { month: number; day: number; text: string } {
|
|
127
|
+
const { lunar = false, label } = options;
|
|
128
|
+
const prefix = label === undefined ? "" : `${label} `;
|
|
129
|
+
const parsed =
|
|
130
|
+
typeof value === "string" && MMDD_RE.test(value)
|
|
131
|
+
? { month: Number(value.slice(0, 2)), day: Number(value.slice(2)), text: value }
|
|
132
|
+
: null;
|
|
133
|
+
|
|
134
|
+
if (parsed === null) {
|
|
135
|
+
failType(path, `${prefix}必须是四位 "MMDD"(如 "0101"),收到 ${describe(value)}`);
|
|
136
|
+
}
|
|
137
|
+
const valid = lunar
|
|
138
|
+
? isLunarMonthDay(parsed.month, parsed.day)
|
|
139
|
+
: isSolarMonthDay(parsed.month, parsed.day);
|
|
140
|
+
if (!valid) {
|
|
141
|
+
failType(path, `${lunar ? "农历" : "公历"}不存在 ${parsed.month} 月 ${parsed.day} 日`);
|
|
142
|
+
}
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 校验并规范化节日表
|
|
148
|
+
*
|
|
149
|
+
* @param input 待校验的节日表
|
|
150
|
+
* @param path 出错时用于定位的路径,例如 "createCalendar.setFestival.sFtv"
|
|
151
|
+
* @param lunar 农历节日(true)还是公历节日(false)
|
|
152
|
+
*/
|
|
153
|
+
const normalizeFestivalMap = function (input: unknown, path: string, lunar = false): FestivalMap {
|
|
154
|
+
assertPlainObject(input, path);
|
|
155
|
+
const result: FestivalMap = {};
|
|
156
|
+
Object.keys(input).forEach(function (key) {
|
|
157
|
+
assertMMDD(key, `${path}.${key}`, { lunar, label: "节日 key" });
|
|
158
|
+
const value = input[key];
|
|
159
|
+
if (!Array.isArray(value)) {
|
|
160
|
+
throw failType(`${path}.${key}`, `节日名称必须是字符串数组,收到 ${describe(value)}`);
|
|
161
|
+
}
|
|
162
|
+
const names: string[] = [];
|
|
163
|
+
value.forEach(function (name: unknown, index: number) {
|
|
164
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
165
|
+
throw failType(
|
|
166
|
+
`${path}.${key}[${index}]`,
|
|
167
|
+
`节日名称必须是非空字符串,收到 ${describe(name)}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
names.push(name);
|
|
171
|
+
});
|
|
172
|
+
result[key] = names;
|
|
173
|
+
});
|
|
174
|
+
return result;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** 校验并去重一组调休日期(MMDD 字符串) */
|
|
178
|
+
const normalizeDayList = function (input: unknown, path: string): string[] {
|
|
179
|
+
if (!Array.isArray(input)) {
|
|
180
|
+
throw failType(path, `必须是 "MMDD" 字符串数组,收到 ${describe(input)}`);
|
|
181
|
+
}
|
|
182
|
+
const days: string[] = [];
|
|
183
|
+
input.forEach(function (day: unknown, index: number) {
|
|
184
|
+
days.push(assertMMDD(day, `${path}[${index}]`).text);
|
|
185
|
+
});
|
|
186
|
+
return Array.from(new Set(days));
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 校验并规范化调休表,key 为年份
|
|
191
|
+
*
|
|
192
|
+
* @param input 待校验的调休表
|
|
193
|
+
* @param path 出错时用于定位的路径
|
|
194
|
+
*/
|
|
195
|
+
const normalizeRestMap = function (input: unknown, path: string): RestMap {
|
|
196
|
+
assertPlainObject(input, path);
|
|
197
|
+
const result: RestMap = {};
|
|
198
|
+
Object.keys(input).forEach(function (yearKey) {
|
|
199
|
+
if (!YEAR_RE.test(yearKey)) {
|
|
200
|
+
throw failType(
|
|
201
|
+
`${path}.${yearKey}`,
|
|
202
|
+
`年份必须是 1900-2100 的四位数字,收到 ${describe(yearKey)}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const entry = input[yearKey];
|
|
206
|
+
assertPlainObject(entry, `${path}.${yearKey}`, "必须是 { xiu: [], ban: [] }");
|
|
207
|
+
assertKnownKeys(entry, REST_FIELDS, `${path}.${yearKey}`);
|
|
208
|
+
|
|
209
|
+
const xiu =
|
|
210
|
+
entry.xiu === undefined ? [] : normalizeDayList(entry.xiu, `${path}.${yearKey}.xiu`);
|
|
211
|
+
const ban =
|
|
212
|
+
entry.ban === undefined ? [] : normalizeDayList(entry.ban, `${path}.${yearKey}.ban`);
|
|
213
|
+
|
|
214
|
+
// 放假和补班来自同一份法定节假日安排,同一天不可能既放假又补班
|
|
215
|
+
const conflict = xiu.filter(function (day) {
|
|
216
|
+
return ban.indexOf(day) !== -1;
|
|
217
|
+
});
|
|
218
|
+
if (conflict.length > 0) {
|
|
219
|
+
throw failType(
|
|
220
|
+
`${path}.${yearKey}`,
|
|
221
|
+
`同一天不能既是放假日又是补班日:${conflict.join("、")}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
result[yearKey] = { xiu, ban };
|
|
226
|
+
});
|
|
227
|
+
return result;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export { normalizeFestivalMap, normalizeRestMap };
|