@sanguo-dev/calendar 0.0.0-stage → 1.0.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.
@@ -0,0 +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 };
@@ -0,0 +1,162 @@
1
+ import { Animals, nStr1, SolarMonth, TermInfo } from "../data/Calendar";
2
+ import { assertInteger, failRange } from "./Assert";
3
+
4
+ /** 公历年份下限,只为挡住明显错误的入参 */
5
+ const SOLAR_YEAR_MIN = 1;
6
+ const SOLAR_YEAR_MAX = 9999;
7
+
8
+ const MS_PER_DAY = 86400000;
9
+ /** 1970-01-01 是星期四,用作星期推算的基准 */
10
+ const EPOCH_WEEKDAY = 4;
11
+
12
+ /** 用毫秒数直接算星期,避免每次都 new Date */
13
+ const weekdayOf = function (y: number, m: number, d: number): number {
14
+ const days = Math.round(Date.UTC(y, m - 1, d) / MS_PER_DAY);
15
+ return (((days + EPOCH_WEEKDAY) % 7) + 7) % 7;
16
+ };
17
+
18
+ /** 按年缓存的 24 个节气日期,同一年只解析一次 */
19
+ const termCache = new Map<number, number[]>();
20
+
21
+ /** 解析某一年的节气表,返回 24 个日期数字 */
22
+ const parseTerms = function (y: number): number[] {
23
+ const table = TermInfo[y - 1900];
24
+ if (typeof table !== "string" || table.length < 30) {
25
+ failRange("getTerm(year)", `缺少 ${y} 年的节气数据`);
26
+ }
27
+
28
+ const days: number[] = [];
29
+ for (let i = 0; i < 6; i++) {
30
+ const chunk = parseInt(table.slice(i * 5, i * 5 + 5), 16).toString();
31
+ days.push(
32
+ Number(chunk.slice(0, 1)),
33
+ Number(chunk.slice(1, 3)),
34
+ Number(chunk.slice(3, 4)),
35
+ Number(chunk.slice(4, 6)),
36
+ );
37
+ }
38
+ return days;
39
+ };
40
+
41
+ /**
42
+ * 返回公历 y 年 m 月的天数(会自动处理闰年)
43
+ * @param y 公历年
44
+ * @param m 公历月(1-12)
45
+ * @return 28-31
46
+ */
47
+ const solarDays = function (y: number, m: number): number {
48
+ assertInteger("solarDays(year)", y, SOLAR_YEAR_MIN, SOLAR_YEAR_MAX);
49
+ assertInteger("solarDays(month)", m, 1, 12);
50
+ if (m === 2) {
51
+ return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0 ? 29 : 28;
52
+ }
53
+ return SolarMonth[m - 1];
54
+ };
55
+
56
+ /**
57
+ * 返回公历 y 年 m 月 1 日是星期几
58
+ * @param y 公历年
59
+ * @param m 公历月(1-12)
60
+ * @return 0-6,0 为周日
61
+ */
62
+ const solarFirstWeek = function (y: number, m: number): number {
63
+ assertInteger("solarFirstWeek(year)", y, SOLAR_YEAR_MIN, SOLAR_YEAR_MAX);
64
+ assertInteger("solarFirstWeek(month)", m, 1, 12);
65
+ return weekdayOf(y, m, 1);
66
+ };
67
+
68
+ /**
69
+ * 返回公历 y 年 m 月第 nth 个星期 weekday 是几号
70
+ * @param y 公历年
71
+ * @param m 公历月(1-12)
72
+ * @param weekday 0-6,0 为周日
73
+ * @param nth 第几个,从 1 开始
74
+ * @example nthWeekdayOfMonth(2026, 5, 0, 2) // 10,2026 年母亲节是 5 月 10 日
75
+ */
76
+ const nthWeekdayOfMonth = function (y: number, m: number, weekday: number, nth: number): number {
77
+ assertInteger("nthWeekdayOfMonth(weekday)", weekday, 0, 6);
78
+ assertInteger("nthWeekdayOfMonth(nth)", nth, 1, 5);
79
+ const firstWeekday = solarFirstWeek(y, m);
80
+ const day = 1 + ((weekday - firstWeekday + 7) % 7) + (nth - 1) * 7;
81
+ if (day > solarDays(y, m)) {
82
+ failRange("nthWeekdayOfMonth", `${y} 年 ${m} 月没有第 ${nth} 个星期${weekday},超出当月天数`);
83
+ }
84
+ return day;
85
+ };
86
+
87
+ /**
88
+ * 星座名称,每个星座占两个字符,末尾补一个摩羯座用于 12 月下旬切片
89
+ */
90
+ const ASTRO_NAMES =
91
+ "\u6469\u7faf\u6c34\u74f6\u53cc\u9c7c\u767d\u7f8a\u91d1\u725b\u53cc\u5b50\u5de8\u87f9\u72ee\u5b50\u5904\u5973\u5929\u79e4\u5929\u874e\u5c04\u624b\u6469\u7faf";
92
+
93
+ /** 每个月切换星座的日期,当天大于等于该值即属于后一个星座 */
94
+ const ASTRO_CUSP = [20, 19, 21, 20, 21, 22, 23, 23, 23, 24, 23, 22];
95
+
96
+ /**
97
+ * 公历月、日判断所属星座
98
+ * @param m 公历月(1-12)
99
+ * @param d 公历日
100
+ * @example toAstro(4, 19) // "白羊座"
101
+ */
102
+ const toAstro = function (m: number, d: number): string {
103
+ assertInteger("toAstro(month)", m, 1, 12);
104
+ assertInteger("toAstro(day)", d, 1, 31);
105
+ const start = m * 2 - (d < ASTRO_CUSP[m - 1] ? 2 : 0);
106
+ return ASTRO_NAMES.slice(start, start + 2) + "\u5ea7";
107
+ };
108
+
109
+ /**
110
+ * 传入公历年获得该年第 n 个节气所在公历月的日期
111
+ * @param y 公历年(1900-2100)
112
+ * @param n 第几个节气(1-24),1 为小寒
113
+ * @return 日期数字
114
+ * @example getTerm(1987, 3) // 4,1987 年 2 月 4 日立春
115
+ */
116
+ const getTerm = function (y: number, n: number): number {
117
+ assertInteger("getTerm(year)", y, 1900, 2100);
118
+ assertInteger("getTerm(n)", n, 1, 24);
119
+
120
+ let days = termCache.get(y);
121
+ if (days === undefined) {
122
+ days = parseTerms(y);
123
+ termCache.set(y, days);
124
+ }
125
+
126
+ const day = days[n - 1];
127
+ if (!Number.isInteger(day)) {
128
+ failRange("getTerm", `${y} 年第 ${n} 个节气解析失败`);
129
+ }
130
+ return day;
131
+ };
132
+
133
+ /**
134
+ * 年份转生肖,以农历年为准
135
+ * @param y 年份
136
+ * @example getAnimal(1987) // "兔"
137
+ */
138
+ const getAnimal = function (y: number): string {
139
+ assertInteger("getAnimal(year)", y, SOLAR_YEAR_MIN, SOLAR_YEAR_MAX);
140
+ return Animals[(((y - 4) % 12) + 12) % 12];
141
+ };
142
+
143
+ /**
144
+ * 数字转中文,0-10
145
+ * @param num 0-10
146
+ * @example toChinaNum(3) // "三"
147
+ */
148
+ const toChinaNum = function (num: number): string {
149
+ assertInteger("toChinaNum(num)", num, 0, 10);
150
+ return nStr1[num];
151
+ };
152
+
153
+ export {
154
+ solarDays,
155
+ toChinaNum,
156
+ solarFirstWeek,
157
+ weekdayOf,
158
+ nthWeekdayOfMonth,
159
+ toAstro,
160
+ getTerm,
161
+ getAnimal,
162
+ };