@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.
@@ -1,215 +1,215 @@
1
- import { LunarInfo, Gan, Zhi, GanZhiHour, SolarTerm, nStr3, nStr2, nStr1 } from "../data/Calendar";
2
- import { assertInteger, failRange } from "./Assert";
3
-
4
- /** 农历数据表覆盖的年份 */
5
- const LUNAR_YEAR_MIN = 1900;
6
- const LUNAR_YEAR_MAX = 2100;
7
-
8
- /**
9
- * 校验农历年份是否在数据表范围内,越界直接抛 RangeError
10
- */
11
- const assertLunarYear = function (y: number, path: string): number {
12
- return assertInteger(path, y, LUNAR_YEAR_MIN, LUNAR_YEAR_MAX);
13
- };
14
-
15
- /**
16
- * 返回农历 y 年一整年的总天数
17
- * @param y 农历年(1900-2100)
18
- * @return 348-385
19
- */
20
- const lYearDays = function (y: number): number {
21
- assertLunarYear(y, "lYearDays(year)");
22
- let sum = 348;
23
- for (let i = 0x8000; i > 0x8; i >>= 1) {
24
- sum += LunarInfo[y - 1900] & i ? 1 : 0;
25
- }
26
- return sum + leapDays(y);
27
- };
28
-
29
- /**
30
- * 返回农历 y 年闰月是哪个月;没有闰月返回 0
31
- * @param y 农历年(1900-2100)
32
- */
33
- const leapMonth = function (y: number): number {
34
- assertLunarYear(y, "leapMonth(year)");
35
- return LunarInfo[y - 1900] & 0xf;
36
- };
37
-
38
- /**
39
- * 返回农历 y 年闰月的天数;没有闰月返回 0
40
- * @param y 农历年(1900-2100)
41
- */
42
- const leapDays = function (y: number): number {
43
- if (leapMonth(y)) {
44
- return LunarInfo[y - 1900] & 0x10000 ? 30 : 29;
45
- }
46
- return 0;
47
- };
48
-
49
- /**
50
- * 返回农历 y 年 m 月(非闰月)的天数,闰月天数请用 leapDays
51
- * @param y 农历年(1900-2100)
52
- * @param m 农历月(1-12)
53
- */
54
- const lMonthDays = function (y: number, m: number): number {
55
- assertLunarYear(y, "lMonthDays(year)");
56
- assertInteger("lMonthDays(month)", m, 1, 12);
57
- return LunarInfo[y - 1900] & (0x10000 >> m) ? 30 : 29;
58
- };
59
-
60
- /** 农历每年正月初一距 1900-01-31 的天数;长度 202,最后一项是 2101 正月初一 */
61
- const YEAR_START: number[] = (function () {
62
- const starts: number[] = [0];
63
- for (let y = LUNAR_YEAR_MIN; y <= LUNAR_YEAR_MAX; y++) {
64
- starts.push((starts[starts.length - 1] as number) + lYearDays(y));
65
- }
66
- return starts;
67
- })();
68
-
69
- /** 某年正月初一距 1900-01-31 的天数 */
70
- const yearStartOf = function (year: number): number {
71
- assertLunarYear(year, "yearStartOf(year)");
72
- return YEAR_START[year - LUNAR_YEAR_MIN] as number;
73
- };
74
-
75
- /** 农历某年的一个“月份段”,闰月算独立一段 */
76
- export interface LunarMonthSegment {
77
- month: number;
78
- isLeap: boolean;
79
- /** 该段 1 号距当年正月初一的天数 */
80
- start: number;
81
- }
82
-
83
- const segmentCache = new Map<number, LunarMonthSegment[]>();
84
-
85
- /** 某年的月份段表(按时间顺序),第一次构建后缓存 */
86
- const monthSegmentsOf = function (year: number): LunarMonthSegment[] {
87
- const cached = segmentCache.get(year);
88
- if (cached) return cached;
89
-
90
- const leap = leapMonth(year);
91
- const segments: LunarMonthSegment[] = [];
92
- let start = 0;
93
-
94
- for (let m = 1; m <= 12; m++) {
95
- segments.push({ month: m, isLeap: false, start });
96
- start += lMonthDays(year, m);
97
- if (leap === m) {
98
- segments.push({ month: m, isLeap: true, start });
99
- start += leapDays(year);
100
- }
101
- }
102
-
103
- segmentCache.set(year, segments);
104
- return segments;
105
- };
106
-
107
- /** 把“距 1900-01-31 的天数”换算成农历年与当年第几天(二分查找,替代逐年累减) */
108
- const resolveLunarYear = function (offset: number): { year: number; offsetInYear: number } {
109
- let low = 0;
110
- let high = LUNAR_YEAR_MAX - LUNAR_YEAR_MIN;
111
-
112
- while (low < high) {
113
- const mid = (low + high + 1) >>> 1;
114
- if ((YEAR_START[mid] as number) <= offset) low = mid;
115
- else high = mid - 1;
116
- }
117
-
118
- return {
119
- year: LUNAR_YEAR_MIN + low,
120
- offsetInYear: offset - (YEAR_START[low] as number),
121
- };
122
- };
123
-
124
- /**
125
- * 农历年转换为干支纪年
126
- * @param lYear 农历年,允许 1899 这类边界年(只做算术,不查表)
127
- */
128
- const toGanZhiYear = function (lYear: number): string {
129
- if (typeof lYear !== "number" || !Number.isInteger(lYear)) {
130
- failRange("toGanZhiYear(year)", `必须是整数,收到 ${lYear}`);
131
- }
132
- let ganKey = (lYear - 3) % 10;
133
- let zhiKey = (lYear - 3) % 12;
134
- if (ganKey <= 0) ganKey += 10;
135
- if (zhiKey <= 0) zhiKey += 12;
136
- return Gan[ganKey - 1] + Zhi[zhiKey - 1];
137
- };
138
-
139
- /**
140
- * 传入相对甲子的偏移量返回干支
141
- * @param offset 可以为负数
142
- */
143
- const toGanZhi = function (offset: number): string {
144
- if (typeof offset !== "number" || !Number.isInteger(offset)) {
145
- failRange("toGanZhi(offset)", `必须是整数,收到 ${offset}`);
146
- }
147
- const gan = ((offset % 10) + 10) % 10;
148
- const zhi = ((offset % 12) + 12) % 12;
149
- return Gan[gan] + Zhi[zhi];
150
- };
151
-
152
- /**
153
- * 传入当天的干支偏移量,返回当天十二个时辰的干支
154
- * @param offset 相对甲子的偏移量(与 gzDay 用同一个偏移量)
155
- */
156
- const toHourGanZhi = function (offset: number): string[] {
157
- if (typeof offset !== "number" || !Number.isInteger(offset)) {
158
- failRange("toHourGanZhi(offset)", `必须是整数,收到 ${offset}`);
159
- }
160
- const index = ((offset % 10) + 10) % 10;
161
- return GanZhiHour[index].split("-");
162
- };
163
-
164
- /**
165
- * 农历数字月份转中文,例如 1 -> "正月"、12 -> "腊月"
166
- * @param m 农历月(1-12)
167
- */
168
- const toChinaMonth = function (m: number): string {
169
- assertInteger("toChinaMonth(month)", m, 1, 12);
170
- return nStr3[m - 1] + "月";
171
- };
172
-
173
- /**
174
- * 农历日期数字转中文,例如 1 -> "初一"、21 -> "廿一"
175
- * @param d 农历日(1-30)
176
- */
177
- const toChinaDay = function (d: number): string {
178
- assertInteger("toChinaDay(day)", d, 1, 30);
179
- switch (d) {
180
- case 10:
181
- return "\u521d\u5341";
182
- case 20:
183
- return "\u4e8c\u5341";
184
- case 30:
185
- return "\u4e09\u5341";
186
- default:
187
- return nStr2[Math.floor(d / 10)] + nStr1[d % 10];
188
- }
189
- };
190
-
191
- /**
192
- * 取第 n 个节气名(n 从 0 开始,0 为小寒)
193
- * @param n 0-23
194
- */
195
- const getSolarTerm = function (n: number): string {
196
- assertInteger("getSolarTerm(n)", n, 0, 23);
197
- return SolarTerm[n];
198
- };
199
-
200
- export {
201
- lYearDays,
202
- leapMonth,
203
- leapDays,
204
- lMonthDays,
205
- yearStartOf,
206
- monthSegmentsOf,
207
- resolveLunarYear,
208
- toGanZhiYear,
209
- toGanZhi,
210
- toHourGanZhi,
211
- toChinaMonth,
212
- toChinaDay,
213
- getSolarTerm,
214
- assertLunarYear,
215
- };
1
+ import { LunarInfo, Gan, Zhi, GanZhiHour, SolarTerm, nStr3, nStr2, nStr1 } from "../data/Calendar";
2
+ import { assertInteger, failRange } from "./Assert";
3
+
4
+ /** 农历数据表覆盖的年份 */
5
+ const LUNAR_YEAR_MIN = 1900;
6
+ const LUNAR_YEAR_MAX = 2100;
7
+
8
+ /**
9
+ * 校验农历年份是否在数据表范围内,越界直接抛 RangeError
10
+ */
11
+ const assertLunarYear = function (y: number, path: string): number {
12
+ return assertInteger(path, y, LUNAR_YEAR_MIN, LUNAR_YEAR_MAX);
13
+ };
14
+
15
+ /**
16
+ * 返回农历 y 年一整年的总天数
17
+ * @param y 农历年(1900-2100)
18
+ * @return 348-385
19
+ */
20
+ const lYearDays = function (y: number): number {
21
+ assertLunarYear(y, "lYearDays(year)");
22
+ let sum = 348;
23
+ for (let i = 0x8000; i > 0x8; i >>= 1) {
24
+ sum += LunarInfo[y - 1900] & i ? 1 : 0;
25
+ }
26
+ return sum + leapDays(y);
27
+ };
28
+
29
+ /**
30
+ * 返回农历 y 年闰月是哪个月;没有闰月返回 0
31
+ * @param y 农历年(1900-2100)
32
+ */
33
+ const leapMonth = function (y: number): number {
34
+ assertLunarYear(y, "leapMonth(year)");
35
+ return LunarInfo[y - 1900] & 0xf;
36
+ };
37
+
38
+ /**
39
+ * 返回农历 y 年闰月的天数;没有闰月返回 0
40
+ * @param y 农历年(1900-2100)
41
+ */
42
+ const leapDays = function (y: number): number {
43
+ if (leapMonth(y)) {
44
+ return LunarInfo[y - 1900] & 0x10000 ? 30 : 29;
45
+ }
46
+ return 0;
47
+ };
48
+
49
+ /**
50
+ * 返回农历 y 年 m 月(非闰月)的天数,闰月天数请用 leapDays
51
+ * @param y 农历年(1900-2100)
52
+ * @param m 农历月(1-12)
53
+ */
54
+ const lMonthDays = function (y: number, m: number): number {
55
+ assertLunarYear(y, "lMonthDays(year)");
56
+ assertInteger("lMonthDays(month)", m, 1, 12);
57
+ return LunarInfo[y - 1900] & (0x10000 >> m) ? 30 : 29;
58
+ };
59
+
60
+ /** 农历每年正月初一距 1900-01-31 的天数;长度 202,最后一项是 2101 正月初一 */
61
+ const YEAR_START: number[] = (function () {
62
+ const starts: number[] = [0];
63
+ for (let y = LUNAR_YEAR_MIN; y <= LUNAR_YEAR_MAX; y++) {
64
+ starts.push((starts[starts.length - 1] as number) + lYearDays(y));
65
+ }
66
+ return starts;
67
+ })();
68
+
69
+ /** 某年正月初一距 1900-01-31 的天数 */
70
+ const yearStartOf = function (year: number): number {
71
+ assertLunarYear(year, "yearStartOf(year)");
72
+ return YEAR_START[year - LUNAR_YEAR_MIN] as number;
73
+ };
74
+
75
+ /** 农历某年的一个“月份段”,闰月算独立一段 */
76
+ export interface LunarMonthSegment {
77
+ month: number;
78
+ isLeap: boolean;
79
+ /** 该段 1 号距当年正月初一的天数 */
80
+ start: number;
81
+ }
82
+
83
+ const segmentCache = new Map<number, LunarMonthSegment[]>();
84
+
85
+ /** 某年的月份段表(按时间顺序),第一次构建后缓存 */
86
+ const monthSegmentsOf = function (year: number): LunarMonthSegment[] {
87
+ const cached = segmentCache.get(year);
88
+ if (cached) return cached;
89
+
90
+ const leap = leapMonth(year);
91
+ const segments: LunarMonthSegment[] = [];
92
+ let start = 0;
93
+
94
+ for (let m = 1; m <= 12; m++) {
95
+ segments.push({ month: m, isLeap: false, start });
96
+ start += lMonthDays(year, m);
97
+ if (leap === m) {
98
+ segments.push({ month: m, isLeap: true, start });
99
+ start += leapDays(year);
100
+ }
101
+ }
102
+
103
+ segmentCache.set(year, segments);
104
+ return segments;
105
+ };
106
+
107
+ /** 把“距 1900-01-31 的天数”换算成农历年与当年第几天(二分查找,替代逐年累减) */
108
+ const resolveLunarYear = function (offset: number): { year: number; offsetInYear: number } {
109
+ let low = 0;
110
+ let high = LUNAR_YEAR_MAX - LUNAR_YEAR_MIN;
111
+
112
+ while (low < high) {
113
+ const mid = (low + high + 1) >>> 1;
114
+ if ((YEAR_START[mid] as number) <= offset) low = mid;
115
+ else high = mid - 1;
116
+ }
117
+
118
+ return {
119
+ year: LUNAR_YEAR_MIN + low,
120
+ offsetInYear: offset - (YEAR_START[low] as number),
121
+ };
122
+ };
123
+
124
+ /**
125
+ * 农历年转换为干支纪年
126
+ * @param lYear 农历年,允许 1899 这类边界年(只做算术,不查表)
127
+ */
128
+ const toGanZhiYear = function (lYear: number): string {
129
+ if (typeof lYear !== "number" || !Number.isInteger(lYear)) {
130
+ failRange("toGanZhiYear(year)", `必须是整数,收到 ${lYear}`);
131
+ }
132
+ let ganKey = (lYear - 3) % 10;
133
+ let zhiKey = (lYear - 3) % 12;
134
+ if (ganKey <= 0) ganKey += 10;
135
+ if (zhiKey <= 0) zhiKey += 12;
136
+ return Gan[ganKey - 1] + Zhi[zhiKey - 1];
137
+ };
138
+
139
+ /**
140
+ * 传入相对甲子的偏移量返回干支
141
+ * @param offset 可以为负数
142
+ */
143
+ const toGanZhi = function (offset: number): string {
144
+ if (typeof offset !== "number" || !Number.isInteger(offset)) {
145
+ failRange("toGanZhi(offset)", `必须是整数,收到 ${offset}`);
146
+ }
147
+ const gan = ((offset % 10) + 10) % 10;
148
+ const zhi = ((offset % 12) + 12) % 12;
149
+ return Gan[gan] + Zhi[zhi];
150
+ };
151
+
152
+ /**
153
+ * 传入当天的干支偏移量,返回当天十二个时辰的干支
154
+ * @param offset 相对甲子的偏移量(与 gzDay 用同一个偏移量)
155
+ */
156
+ const toHourGanZhi = function (offset: number): string[] {
157
+ if (typeof offset !== "number" || !Number.isInteger(offset)) {
158
+ failRange("toHourGanZhi(offset)", `必须是整数,收到 ${offset}`);
159
+ }
160
+ const index = ((offset % 10) + 10) % 10;
161
+ return GanZhiHour[index].split("-");
162
+ };
163
+
164
+ /**
165
+ * 农历数字月份转中文,例如 1 -> "正月"、12 -> "腊月"
166
+ * @param m 农历月(1-12)
167
+ */
168
+ const toChinaMonth = function (m: number): string {
169
+ assertInteger("toChinaMonth(month)", m, 1, 12);
170
+ return nStr3[m - 1] + "月";
171
+ };
172
+
173
+ /**
174
+ * 农历日期数字转中文,例如 1 -> "初一"、21 -> "廿一"
175
+ * @param d 农历日(1-30)
176
+ */
177
+ const toChinaDay = function (d: number): string {
178
+ assertInteger("toChinaDay(day)", d, 1, 30);
179
+ switch (d) {
180
+ case 10:
181
+ return "\u521d\u5341";
182
+ case 20:
183
+ return "\u4e8c\u5341";
184
+ case 30:
185
+ return "\u4e09\u5341";
186
+ default:
187
+ return nStr2[Math.floor(d / 10)] + nStr1[d % 10];
188
+ }
189
+ };
190
+
191
+ /**
192
+ * 取第 n 个节气名(n 从 0 开始,0 为小寒)
193
+ * @param n 0-23
194
+ */
195
+ const getSolarTerm = function (n: number): string {
196
+ assertInteger("getSolarTerm(n)", n, 0, 23);
197
+ return SolarTerm[n];
198
+ };
199
+
200
+ export {
201
+ lYearDays,
202
+ leapMonth,
203
+ leapDays,
204
+ lMonthDays,
205
+ yearStartOf,
206
+ monthSegmentsOf,
207
+ resolveLunarYear,
208
+ toGanZhiYear,
209
+ toGanZhi,
210
+ toHourGanZhi,
211
+ toChinaMonth,
212
+ toChinaDay,
213
+ getSolarTerm,
214
+ assertLunarYear,
215
+ };
@@ -1,40 +1,40 @@
1
- import { mmddOf } from "./Assert";
2
- import type { FestivalMap } from "../types";
3
-
4
- /** 没有节日时共用的空数组,冻结后不怕调用方误改 */
5
- const NO_FESTIVAL: readonly string[] = Object.freeze([]) as readonly string[];
6
-
7
- /**
8
- * 把两份节日表按 key 合并并去重,不修改入参
9
- */
10
- const mergeFestivalMap = function (base: FestivalMap, extra?: FestivalMap): FestivalMap {
11
- if (!extra) return base;
12
- const result: FestivalMap = { ...base };
13
- Object.keys(extra).forEach(function (key) {
14
- const merged = result[key] ? result[key].concat(extra[key]) : extra[key];
15
- result[key] = Array.from(new Set(merged));
16
- });
17
- return result;
18
- };
19
-
20
- /**
21
- * 冻结节日表,防止调用方后续误改
22
- */
23
- const freezeFestivalMap = function (map: FestivalMap): FestivalMap {
24
- Object.keys(map).forEach(function (key) {
25
- Object.freeze(map[key]);
26
- });
27
- return Object.freeze(map);
28
- };
29
-
30
- /**
31
- * 取某一天(MMDD)的节日名称,没有则返回共享的空数组。
32
- *
33
- * 返回的是只读数组:配置里的数组已被冻结,调用方要追加节日必须自己 copy。
34
- */
35
- const festivalList = function (map: FestivalMap, m: number, d: number): readonly string[] {
36
- return map[mmddOf(m, d)] ?? NO_FESTIVAL;
37
- };
38
-
39
- export { mergeFestivalMap, freezeFestivalMap, festivalList, NO_FESTIVAL };
40
- export type { FestivalMap };
1
+ import { mmddOf } from "./Assert";
2
+ import type { FestivalMap } from "../types";
3
+
4
+ /** 没有节日时共用的空数组,冻结后不怕调用方误改 */
5
+ const NO_FESTIVAL: readonly string[] = Object.freeze([]) as readonly string[];
6
+
7
+ /**
8
+ * 把两份节日表按 key 合并并去重,不修改入参
9
+ */
10
+ const mergeFestivalMap = function (base: FestivalMap, extra?: FestivalMap): FestivalMap {
11
+ if (!extra) return base;
12
+ const result: FestivalMap = { ...base };
13
+ Object.keys(extra).forEach(function (key) {
14
+ const merged = result[key] ? result[key].concat(extra[key]) : extra[key];
15
+ result[key] = Array.from(new Set(merged));
16
+ });
17
+ return result;
18
+ };
19
+
20
+ /**
21
+ * 冻结节日表,防止调用方后续误改
22
+ */
23
+ const freezeFestivalMap = function (map: FestivalMap): FestivalMap {
24
+ Object.keys(map).forEach(function (key) {
25
+ Object.freeze(map[key]);
26
+ });
27
+ return Object.freeze(map);
28
+ };
29
+
30
+ /**
31
+ * 取某一天(MMDD)的节日名称,没有则返回共享的空数组。
32
+ *
33
+ * 返回的是只读数组:配置里的数组已被冻结,调用方要追加节日必须自己 copy。
34
+ */
35
+ const festivalList = function (map: FestivalMap, m: number, d: number): readonly string[] {
36
+ return map[mmddOf(m, d)] ?? NO_FESTIVAL;
37
+ };
38
+
39
+ export { mergeFestivalMap, freezeFestivalMap, festivalList, NO_FESTIVAL };
40
+ export type { FestivalMap };
@@ -1,84 +1,84 @@
1
- import { mmddOf } from "./Assert";
2
- import type { RestMap } from "../types";
3
-
4
- /** 预建索引,避免每次查询都做数组 includes */
5
- export interface RestIndexEntry {
6
- xiu: Set<string>;
7
- ban: Set<string>;
8
- }
9
- export type RestIndex = Record<string, RestIndexEntry>;
10
-
11
- /**
12
- * 某一天的调休状态
13
- *
14
- * 放假和补班来自同一份国家法定节假日数据,同一天不会既是放假又是补班;
15
- * 既不在 xiu 也不在 ban 的普通日返回 undefined。
16
- */
17
- export type RestState = "xiu" | "ban";
18
-
19
- /**
20
- * 按年、按列表合并调休数据,不修改入参
21
- */
22
- const mergeRestMap = function (base: RestMap, extra?: RestMap): RestMap {
23
- if (!extra) return base;
24
- const result: RestMap = {};
25
- const years = new Set(Object.keys(base).concat(Object.keys(extra)));
26
- years.forEach(function (year) {
27
- const left = base[year];
28
- const right = extra[year];
29
- if (left && right) {
30
- result[year] = {
31
- xiu: Array.from(new Set(left.xiu.concat(right.xiu))),
32
- ban: Array.from(new Set(left.ban.concat(right.ban))),
33
- };
34
- } else if (left) {
35
- result[year] = left;
36
- } else if (right) {
37
- result[year] = right;
38
- }
39
- });
40
- return result;
41
- };
42
-
43
- /**
44
- * 冻结调休表
45
- */
46
- const freezeRestMap = function (map: RestMap): RestMap {
47
- Object.keys(map).forEach(function (year) {
48
- Object.freeze(map[year].xiu);
49
- Object.freeze(map[year].ban);
50
- Object.freeze(map[year]);
51
- });
52
- return Object.freeze(map);
53
- };
54
-
55
- /**
56
- * 建立年份 -> Set 的索引
57
- */
58
- const buildRestIndex = function (map: RestMap): RestIndex {
59
- const index: RestIndex = {};
60
- Object.keys(map).forEach(function (year) {
61
- index[year] = {
62
- xiu: new Set(map[year].xiu),
63
- ban: new Set(map[year].ban),
64
- };
65
- });
66
- return index;
67
- };
68
-
69
- /**
70
- * 查询某天的调休状态
71
- *
72
- * 只返回一个字符串,不为每天分配 { isHoliday, isBan } 对象。
73
- */
74
- const restOf = function (index: RestIndex, y: number, m: number, d: number): RestState | undefined {
75
- const entry = index[y];
76
- if (!entry) return undefined;
77
-
78
- const key = mmddOf(m, d);
79
- if (entry.ban.has(key)) return "ban";
80
- if (entry.xiu.has(key)) return "xiu";
81
- return undefined;
82
- };
83
-
84
- export { mergeRestMap, freezeRestMap, buildRestIndex, restOf };
1
+ import { mmddOf } from "./Assert";
2
+ import type { RestMap } from "../types";
3
+
4
+ /** 预建索引,避免每次查询都做数组 includes */
5
+ export interface RestIndexEntry {
6
+ xiu: Set<string>;
7
+ ban: Set<string>;
8
+ }
9
+ export type RestIndex = Record<string, RestIndexEntry>;
10
+
11
+ /**
12
+ * 某一天的调休状态
13
+ *
14
+ * 放假和补班来自同一份国家法定节假日数据,同一天不会既是放假又是补班;
15
+ * 既不在 xiu 也不在 ban 的普通日返回 undefined。
16
+ */
17
+ export type RestState = "xiu" | "ban";
18
+
19
+ /**
20
+ * 按年、按列表合并调休数据,不修改入参
21
+ */
22
+ const mergeRestMap = function (base: RestMap, extra?: RestMap): RestMap {
23
+ if (!extra) return base;
24
+ const result: RestMap = {};
25
+ const years = new Set(Object.keys(base).concat(Object.keys(extra)));
26
+ years.forEach(function (year) {
27
+ const left = base[year];
28
+ const right = extra[year];
29
+ if (left && right) {
30
+ result[year] = {
31
+ xiu: Array.from(new Set(left.xiu.concat(right.xiu))),
32
+ ban: Array.from(new Set(left.ban.concat(right.ban))),
33
+ };
34
+ } else if (left) {
35
+ result[year] = left;
36
+ } else if (right) {
37
+ result[year] = right;
38
+ }
39
+ });
40
+ return result;
41
+ };
42
+
43
+ /**
44
+ * 冻结调休表
45
+ */
46
+ const freezeRestMap = function (map: RestMap): RestMap {
47
+ Object.keys(map).forEach(function (year) {
48
+ Object.freeze(map[year].xiu);
49
+ Object.freeze(map[year].ban);
50
+ Object.freeze(map[year]);
51
+ });
52
+ return Object.freeze(map);
53
+ };
54
+
55
+ /**
56
+ * 建立年份 -> Set 的索引
57
+ */
58
+ const buildRestIndex = function (map: RestMap): RestIndex {
59
+ const index: RestIndex = {};
60
+ Object.keys(map).forEach(function (year) {
61
+ index[year] = {
62
+ xiu: new Set(map[year].xiu),
63
+ ban: new Set(map[year].ban),
64
+ };
65
+ });
66
+ return index;
67
+ };
68
+
69
+ /**
70
+ * 查询某天的调休状态
71
+ *
72
+ * 只返回一个字符串,不为每天分配 { isHoliday, isBan } 对象。
73
+ */
74
+ const restOf = function (index: RestIndex, y: number, m: number, d: number): RestState | undefined {
75
+ const entry = index[y];
76
+ if (!entry) return undefined;
77
+
78
+ const key = mmddOf(m, d);
79
+ if (entry.ban.has(key)) return "ban";
80
+ if (entry.xiu.has(key)) return "xiu";
81
+ return undefined;
82
+ };
83
+
84
+ export { mergeRestMap, freezeRestMap, buildRestIndex, restOf };