@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/index.ts
CHANGED
|
@@ -1,317 +1,317 @@
|
|
|
1
|
-
import {
|
|
2
|
-
lYearDays,
|
|
3
|
-
leapMonth,
|
|
4
|
-
leapDays,
|
|
5
|
-
lMonthDays,
|
|
6
|
-
yearStartOf,
|
|
7
|
-
monthSegmentsOf,
|
|
8
|
-
resolveLunarYear,
|
|
9
|
-
toGanZhiYear,
|
|
10
|
-
toGanZhi,
|
|
11
|
-
toHourGanZhi,
|
|
12
|
-
toChinaMonth,
|
|
13
|
-
toChinaDay,
|
|
14
|
-
getSolarTerm,
|
|
15
|
-
} from "./utils/LunarFunction";
|
|
16
|
-
import type { LunarMonthSegment } from "./utils/LunarFunction";
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
toAstro,
|
|
20
|
-
getTerm,
|
|
21
|
-
getAnimal,
|
|
22
|
-
solarDays,
|
|
23
|
-
solarFirstWeek,
|
|
24
|
-
weekdayOf,
|
|
25
|
-
toChinaNum,
|
|
26
|
-
nthWeekdayOfMonth,
|
|
27
|
-
} from "./utils/SolarFunction";
|
|
28
|
-
|
|
29
|
-
import { assertInteger, assertPlainObject, describe, failRange, failType } from "./utils/Assert";
|
|
30
|
-
import { festivalList, NO_FESTIVAL } from "./utils/SetFestival";
|
|
31
|
-
import { restOf } from "./utils/SetRest";
|
|
32
|
-
import { createCalendarConfig, snapshotOf } from "./config";
|
|
33
|
-
import type { ResolvedConfig } from "./config";
|
|
34
|
-
import type { Calendar, HolidayConfig, LunarResult, SetHolidayOptions } from "./types";
|
|
35
|
-
|
|
36
|
-
export type {
|
|
37
|
-
LunarResult,
|
|
38
|
-
Calendar,
|
|
39
|
-
FestivalMap,
|
|
40
|
-
FestivalConfig,
|
|
41
|
-
RestEntry,
|
|
42
|
-
RestConfig,
|
|
43
|
-
HolidayConfig,
|
|
44
|
-
HolidaySnapshot,
|
|
45
|
-
GetHolidaysOptions,
|
|
46
|
-
SetHolidayOptions,
|
|
47
|
-
} from "./types";
|
|
48
|
-
|
|
49
|
-
/** 支持的公历 / 农历年份范围 */
|
|
50
|
-
const MIN_YEAR = 1900;
|
|
51
|
-
const MAX_YEAR = 2100;
|
|
52
|
-
const DAY_MS = 86400000;
|
|
53
|
-
/** 农历数据起点:1900-01-31,即农历 1900 年正月初一 */
|
|
54
|
-
const MIN_SOLAR_UTC = Date.UTC(MIN_YEAR, 0, 31);
|
|
55
|
-
/** 支持的最后一个公历日 */
|
|
56
|
-
const MAX_SOLAR_UTC = Date.UTC(MAX_YEAR, 11, 31);
|
|
57
|
-
|
|
58
|
-
/** 把公历年月日校验成合法的、在支持范围内的日期 */
|
|
59
|
-
const validateSolar = function (
|
|
60
|
-
y: unknown,
|
|
61
|
-
m: unknown,
|
|
62
|
-
d: unknown,
|
|
63
|
-
): { year: number; month: number; day: number } {
|
|
64
|
-
const year = assertInteger("solarToLunar(year)", y, MIN_YEAR, MAX_YEAR);
|
|
65
|
-
const month = assertInteger("solarToLunar(month)", m, 1, 12);
|
|
66
|
-
const day = assertInteger("solarToLunar(day)", d, 1, solarDays(year, month));
|
|
67
|
-
const utc = Date.UTC(year, month - 1, day);
|
|
68
|
-
if (utc < MIN_SOLAR_UTC) {
|
|
69
|
-
failRange("solarToLunar", `${year}-${month}-${day} 早于 1900-01-31,超出农历数据范围`);
|
|
70
|
-
}
|
|
71
|
-
if (utc > MAX_SOLAR_UTC) {
|
|
72
|
-
failRange("solarToLunar", `${year}-${month}-${day} 晚于 2100-12-31,超出农历数据范围`);
|
|
73
|
-
}
|
|
74
|
-
return { year, month, day };
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/** 某一天的公历节日,包含按规则计算的浮动节日 */
|
|
78
|
-
const solarFestivals = function (
|
|
79
|
-
y: number,
|
|
80
|
-
m: number,
|
|
81
|
-
d: number,
|
|
82
|
-
sFtv: Record<string, string[]>,
|
|
83
|
-
): readonly string[] {
|
|
84
|
-
const base = festivalList(sFtv, m, d);
|
|
85
|
-
const extra: string[] = [];
|
|
86
|
-
|
|
87
|
-
const add = function (name: string) {
|
|
88
|
-
if (base.indexOf(name) === -1 && extra.indexOf(name) === -1) extra.push(name);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
if (m === 5 && d === nthWeekdayOfMonth(y, 5, 0, 2)) add("母亲节");
|
|
92
|
-
if (m === 6 && d === nthWeekdayOfMonth(y, 6, 0, 3)) add("父亲节");
|
|
93
|
-
if (m === 11 && d === nthWeekdayOfMonth(y, 11, 4, 4)) add("感恩节");
|
|
94
|
-
|
|
95
|
-
// 没有浮动节日时直接返回配置里的只读数组,省掉一次拷贝
|
|
96
|
-
return extra.length === 0 ? base : Object.freeze(base.concat(extra));
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 某一天的农历节日,除夕按“腊月最后一天”计算,不写死 12-30
|
|
101
|
-
*
|
|
102
|
-
* 闰月不过节:闰五月初五不算端午、闰八月十五不算中秋。
|
|
103
|
-
*/
|
|
104
|
-
const lunarFestivals = function (
|
|
105
|
-
lYear: number,
|
|
106
|
-
lMonth: number,
|
|
107
|
-
lDay: number,
|
|
108
|
-
isLeap: boolean,
|
|
109
|
-
lFtv: Record<string, string[]>,
|
|
110
|
-
): readonly string[] {
|
|
111
|
-
if (isLeap) return NO_FESTIVAL;
|
|
112
|
-
|
|
113
|
-
const base = festivalList(lFtv, lMonth, lDay);
|
|
114
|
-
if (lMonth === 12 && lDay === lMonthDays(lYear, 12) && base.indexOf("除夕") === -1) {
|
|
115
|
-
return Object.freeze(base.concat("除夕"));
|
|
116
|
-
}
|
|
117
|
-
return base;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* 核心换算,入参必须是已经校验过、且在支持范围内的公历日期
|
|
122
|
-
*/
|
|
123
|
-
const computeLunar = function (
|
|
124
|
-
config: ResolvedConfig,
|
|
125
|
-
sy: number,
|
|
126
|
-
sm: number,
|
|
127
|
-
sd: number,
|
|
128
|
-
): LunarResult {
|
|
129
|
-
const utc = Date.UTC(sy, sm - 1, sd);
|
|
130
|
-
const offset = Math.round((utc - MIN_SOLAR_UTC) / DAY_MS);
|
|
131
|
-
const { year, offsetInYear } = resolveLunarYear(offset);
|
|
132
|
-
|
|
133
|
-
// 月份段(闰月单独一段)按顺序扫描,最多 13 段
|
|
134
|
-
const segments = monthSegmentsOf(year);
|
|
135
|
-
let segment = segments[0] as LunarMonthSegment;
|
|
136
|
-
for (const item of segments) {
|
|
137
|
-
if (item.start > offsetInYear) break;
|
|
138
|
-
segment = item;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const month = segment.month;
|
|
142
|
-
const day = offsetInYear - segment.start + 1;
|
|
143
|
-
const isLeap = segment.isLeap;
|
|
144
|
-
|
|
145
|
-
const dayOfWeek = weekdayOf(sy, sm, sd);
|
|
146
|
-
const firstTermDay = getTerm(sy, sm * 2 - 1);
|
|
147
|
-
const secondTermDay = getTerm(sy, sm * 2);
|
|
148
|
-
|
|
149
|
-
let term: string | null = null;
|
|
150
|
-
if (firstTermDay === sd) term = getSolarTerm(sm * 2 - 2);
|
|
151
|
-
if (secondTermDay === sd) term = getSolarTerm(sm * 2 - 1);
|
|
152
|
-
|
|
153
|
-
// xiu / ban 只由 rest.json 决定,与周末无关;同一天不会既放假又补班,两种状态互斥
|
|
154
|
-
const restState = restOf(config.restIndex, sy, sm, sd);
|
|
155
|
-
const isXiu = restState === "xiu";
|
|
156
|
-
const isBan = restState === "ban";
|
|
157
|
-
|
|
158
|
-
const dayCyclical = utc / DAY_MS - (sd - 1) + 25567 + 10;
|
|
159
|
-
const gzDayOffset = dayCyclical + sd - 1;
|
|
160
|
-
// 只有 2 月才需要立春当天的日期,其它月份不去查节气表
|
|
161
|
-
const ganzhiYear = sm > 2 || (sm === 2 && sd >= getTerm(sy, 3)) ? sy : sy - 1;
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
xiu: isXiu,
|
|
165
|
-
ban: isBan,
|
|
166
|
-
gljr: solarFestivals(sy, sm, sd, config.sFtv),
|
|
167
|
-
nljr: lunarFestivals(year, month, day, isLeap, config.lFtv),
|
|
168
|
-
lYear: year,
|
|
169
|
-
lMonth: month,
|
|
170
|
-
lDay: day,
|
|
171
|
-
animal: getAnimal(year),
|
|
172
|
-
lMonthCn: (isLeap ? "闰" : "") + toChinaMonth(month),
|
|
173
|
-
lDayCn: toChinaDay(day),
|
|
174
|
-
cYear: sy,
|
|
175
|
-
cMonth: sm,
|
|
176
|
-
cDay: sd,
|
|
177
|
-
gzYear: toGanZhiYear(ganzhiYear),
|
|
178
|
-
gzYearLunar: toGanZhiYear(year),
|
|
179
|
-
gzMonth: toGanZhi((sy - 1900) * 12 + sm + (sd >= firstTermDay ? 12 : 11)),
|
|
180
|
-
gzDay: toGanZhi(gzDayOffset),
|
|
181
|
-
gzHour: toHourGanZhi(gzDayOffset),
|
|
182
|
-
isToday: (function () {
|
|
183
|
-
const now = new Date();
|
|
184
|
-
return now.getFullYear() === sy && now.getMonth() + 1 === sm && now.getDate() === sd;
|
|
185
|
-
})(),
|
|
186
|
-
isLeap,
|
|
187
|
-
nWeek: dayOfWeek === 0 ? 7 : dayOfWeek,
|
|
188
|
-
dayOfWeek,
|
|
189
|
-
ncWeek: "星期" + toChinaNum(dayOfWeek),
|
|
190
|
-
term,
|
|
191
|
-
astro: toAstro(sm, sd),
|
|
192
|
-
};
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
/** 公历转农历的内部实现,节假日配置由调用方固定 */
|
|
196
|
-
const solarToLunarWith = function (
|
|
197
|
-
config: ResolvedConfig,
|
|
198
|
-
y?: number,
|
|
199
|
-
m?: number,
|
|
200
|
-
d?: number,
|
|
201
|
-
): LunarResult {
|
|
202
|
-
if (y === undefined && m === undefined && d === undefined) {
|
|
203
|
-
const now = new Date();
|
|
204
|
-
const today = validateSolar(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
|
205
|
-
return computeLunar(config, today.year, today.month, today.day);
|
|
206
|
-
}
|
|
207
|
-
if (y === undefined || m === undefined || d === undefined) {
|
|
208
|
-
failRange(
|
|
209
|
-
"solarToLunar",
|
|
210
|
-
`年、月、日必须同时传入,或全部不传以取今天(收到 year=${String(y)}, month=${String(m)}, day=${String(d)})`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
const date = validateSolar(y, m, d);
|
|
214
|
-
return computeLunar(config, date.year, date.month, date.day);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
/** 农历转公历的内部实现,节假日配置由调用方固定 */
|
|
218
|
-
const lunarToSolarWith = function (
|
|
219
|
-
config: ResolvedConfig,
|
|
220
|
-
y: number,
|
|
221
|
-
m: number,
|
|
222
|
-
d: number,
|
|
223
|
-
isLeapMonth?: boolean,
|
|
224
|
-
): LunarResult {
|
|
225
|
-
const year = assertInteger("lunarToSolar(year)", y, MIN_YEAR, MAX_YEAR);
|
|
226
|
-
const month = assertInteger("lunarToSolar(month)", m, 1, 12);
|
|
227
|
-
if (isLeapMonth !== undefined && typeof isLeapMonth !== "boolean") {
|
|
228
|
-
throw failType("lunarToSolar(isLeapMonth)", `必须是布尔值,收到 ${describe(isLeapMonth)}`);
|
|
229
|
-
}
|
|
230
|
-
const leap = leapMonth(year);
|
|
231
|
-
const wantLeap = isLeapMonth === true;
|
|
232
|
-
if (wantLeap && leap !== month) {
|
|
233
|
-
failRange(
|
|
234
|
-
"lunarToSolar(isLeapMonth)",
|
|
235
|
-
`${year} 年${leap === 0 ? "没有闰月" : `的闰月是闰${leap}月`},不能按闰${month}月换算`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
const maxDay = wantLeap ? leapDays(year) : lMonthDays(year, month);
|
|
239
|
-
const day = assertInteger("lunarToSolar(day)", d, 1, maxDay);
|
|
240
|
-
|
|
241
|
-
// 直接查表定位到该月第一天的绝对偏移,不再逐年逐月累加
|
|
242
|
-
const segments = monthSegmentsOf(year);
|
|
243
|
-
const segment = segments.find((item) => item.month === month && item.isLeap === wantLeap);
|
|
244
|
-
if (!segment) {
|
|
245
|
-
return failRange("lunarToSolar", `${year} 年${wantLeap ? "闰" : ""}${month}月不存在`);
|
|
246
|
-
}
|
|
247
|
-
const offset = yearStartOf(year) + segment.start;
|
|
248
|
-
|
|
249
|
-
const utc = MIN_SOLAR_UTC + (offset + day - 1) * DAY_MS;
|
|
250
|
-
if (utc > MAX_SOLAR_UTC) {
|
|
251
|
-
failRange("lunarToSolar", `农历 ${year}-${month}-${day} 换算后晚于 2100-12-31,超出支持范围`);
|
|
252
|
-
}
|
|
253
|
-
const date = new Date(utc);
|
|
254
|
-
return computeLunar(config, date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate());
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* 创建固定节假日配置的日历实例
|
|
259
|
-
*
|
|
260
|
-
* 不读取也不修改任何全局状态;配置在创建时校验并冻结,
|
|
261
|
-
* 多个实例之间互不影响。不传配置时使用内置数据。
|
|
262
|
-
*
|
|
263
|
-
* @param config 节假日 / 调休配置
|
|
264
|
-
* @param options replace 为 true 时忽略内置数据
|
|
265
|
-
* @throws TypeError 配置格式不合法(含具体字段路径)
|
|
266
|
-
* @example
|
|
267
|
-
* const cc = createCalendar({ setRest: { 2027: { xiu: ["0101"] } } });
|
|
268
|
-
* cc.solarToLunar(2027, 1, 1).xiu; // true
|
|
269
|
-
*/
|
|
270
|
-
const createCalendar = function (
|
|
271
|
-
config: HolidayConfig = {},
|
|
272
|
-
options: SetHolidayOptions = {},
|
|
273
|
-
): Calendar {
|
|
274
|
-
// 用户配置只校验一次;只有一份可查询的配置,own 只在 getHolidays 需要时生成快照
|
|
275
|
-
const { merged, snapshotOwn } = createCalendarConfig(config, options);
|
|
276
|
-
|
|
277
|
-
return {
|
|
278
|
-
/** 公历转农历,不传参返回今天 */
|
|
279
|
-
solarToLunar: (y, m, d) => solarToLunarWith(merged, y, m, d),
|
|
280
|
-
/** 农历转公历 */
|
|
281
|
-
lunarToSolar: (y, m, d, isLeapMonth) => lunarToSolarWith(merged, y, m, d, isLeapMonth),
|
|
282
|
-
/** 当前配置的深拷贝;merged 为 false 时只导出用户传入的部分 */
|
|
283
|
-
getHolidays: (getOptions) => {
|
|
284
|
-
if (getOptions !== undefined) assertPlainObject(getOptions, "getHolidays(options)");
|
|
285
|
-
if (getOptions?.merged !== undefined && typeof getOptions.merged !== "boolean") {
|
|
286
|
-
throw failType(
|
|
287
|
-
"getHolidays(options).merged",
|
|
288
|
-
`必须是布尔值,收到 ${describe(getOptions.merged)}`,
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
return getOptions?.merged === false ? snapshotOwn() : snapshotOf(merged);
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const utils = {
|
|
297
|
-
lYearDays,
|
|
298
|
-
leapMonth,
|
|
299
|
-
leapDays,
|
|
300
|
-
lMonthDays,
|
|
301
|
-
toChinaMonth,
|
|
302
|
-
toChinaDay,
|
|
303
|
-
toChinaNum,
|
|
304
|
-
solarDays,
|
|
305
|
-
solarFirstWeek,
|
|
306
|
-
nthWeekdayOfMonth,
|
|
307
|
-
toAstro,
|
|
308
|
-
getAnimal,
|
|
309
|
-
getTerm,
|
|
310
|
-
getSolarTerm,
|
|
311
|
-
toGanZhi,
|
|
312
|
-
toGanZhiYear,
|
|
313
|
-
toHourGanZhi,
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
export { createCalendar, utils };
|
|
317
|
-
export default createCalendar;
|
|
1
|
+
import {
|
|
2
|
+
lYearDays,
|
|
3
|
+
leapMonth,
|
|
4
|
+
leapDays,
|
|
5
|
+
lMonthDays,
|
|
6
|
+
yearStartOf,
|
|
7
|
+
monthSegmentsOf,
|
|
8
|
+
resolveLunarYear,
|
|
9
|
+
toGanZhiYear,
|
|
10
|
+
toGanZhi,
|
|
11
|
+
toHourGanZhi,
|
|
12
|
+
toChinaMonth,
|
|
13
|
+
toChinaDay,
|
|
14
|
+
getSolarTerm,
|
|
15
|
+
} from "./utils/LunarFunction";
|
|
16
|
+
import type { LunarMonthSegment } from "./utils/LunarFunction";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
toAstro,
|
|
20
|
+
getTerm,
|
|
21
|
+
getAnimal,
|
|
22
|
+
solarDays,
|
|
23
|
+
solarFirstWeek,
|
|
24
|
+
weekdayOf,
|
|
25
|
+
toChinaNum,
|
|
26
|
+
nthWeekdayOfMonth,
|
|
27
|
+
} from "./utils/SolarFunction";
|
|
28
|
+
|
|
29
|
+
import { assertInteger, assertPlainObject, describe, failRange, failType } from "./utils/Assert";
|
|
30
|
+
import { festivalList, NO_FESTIVAL } from "./utils/SetFestival";
|
|
31
|
+
import { restOf } from "./utils/SetRest";
|
|
32
|
+
import { createCalendarConfig, snapshotOf } from "./config";
|
|
33
|
+
import type { ResolvedConfig } from "./config";
|
|
34
|
+
import type { Calendar, HolidayConfig, LunarResult, SetHolidayOptions } from "./types";
|
|
35
|
+
|
|
36
|
+
export type {
|
|
37
|
+
LunarResult,
|
|
38
|
+
Calendar,
|
|
39
|
+
FestivalMap,
|
|
40
|
+
FestivalConfig,
|
|
41
|
+
RestEntry,
|
|
42
|
+
RestConfig,
|
|
43
|
+
HolidayConfig,
|
|
44
|
+
HolidaySnapshot,
|
|
45
|
+
GetHolidaysOptions,
|
|
46
|
+
SetHolidayOptions,
|
|
47
|
+
} from "./types";
|
|
48
|
+
|
|
49
|
+
/** 支持的公历 / 农历年份范围 */
|
|
50
|
+
const MIN_YEAR = 1900;
|
|
51
|
+
const MAX_YEAR = 2100;
|
|
52
|
+
const DAY_MS = 86400000;
|
|
53
|
+
/** 农历数据起点:1900-01-31,即农历 1900 年正月初一 */
|
|
54
|
+
const MIN_SOLAR_UTC = Date.UTC(MIN_YEAR, 0, 31);
|
|
55
|
+
/** 支持的最后一个公历日 */
|
|
56
|
+
const MAX_SOLAR_UTC = Date.UTC(MAX_YEAR, 11, 31);
|
|
57
|
+
|
|
58
|
+
/** 把公历年月日校验成合法的、在支持范围内的日期 */
|
|
59
|
+
const validateSolar = function (
|
|
60
|
+
y: unknown,
|
|
61
|
+
m: unknown,
|
|
62
|
+
d: unknown,
|
|
63
|
+
): { year: number; month: number; day: number } {
|
|
64
|
+
const year = assertInteger("solarToLunar(year)", y, MIN_YEAR, MAX_YEAR);
|
|
65
|
+
const month = assertInteger("solarToLunar(month)", m, 1, 12);
|
|
66
|
+
const day = assertInteger("solarToLunar(day)", d, 1, solarDays(year, month));
|
|
67
|
+
const utc = Date.UTC(year, month - 1, day);
|
|
68
|
+
if (utc < MIN_SOLAR_UTC) {
|
|
69
|
+
failRange("solarToLunar", `${year}-${month}-${day} 早于 1900-01-31,超出农历数据范围`);
|
|
70
|
+
}
|
|
71
|
+
if (utc > MAX_SOLAR_UTC) {
|
|
72
|
+
failRange("solarToLunar", `${year}-${month}-${day} 晚于 2100-12-31,超出农历数据范围`);
|
|
73
|
+
}
|
|
74
|
+
return { year, month, day };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** 某一天的公历节日,包含按规则计算的浮动节日 */
|
|
78
|
+
const solarFestivals = function (
|
|
79
|
+
y: number,
|
|
80
|
+
m: number,
|
|
81
|
+
d: number,
|
|
82
|
+
sFtv: Record<string, string[]>,
|
|
83
|
+
): readonly string[] {
|
|
84
|
+
const base = festivalList(sFtv, m, d);
|
|
85
|
+
const extra: string[] = [];
|
|
86
|
+
|
|
87
|
+
const add = function (name: string) {
|
|
88
|
+
if (base.indexOf(name) === -1 && extra.indexOf(name) === -1) extra.push(name);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (m === 5 && d === nthWeekdayOfMonth(y, 5, 0, 2)) add("母亲节");
|
|
92
|
+
if (m === 6 && d === nthWeekdayOfMonth(y, 6, 0, 3)) add("父亲节");
|
|
93
|
+
if (m === 11 && d === nthWeekdayOfMonth(y, 11, 4, 4)) add("感恩节");
|
|
94
|
+
|
|
95
|
+
// 没有浮动节日时直接返回配置里的只读数组,省掉一次拷贝
|
|
96
|
+
return extra.length === 0 ? base : Object.freeze(base.concat(extra));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 某一天的农历节日,除夕按“腊月最后一天”计算,不写死 12-30
|
|
101
|
+
*
|
|
102
|
+
* 闰月不过节:闰五月初五不算端午、闰八月十五不算中秋。
|
|
103
|
+
*/
|
|
104
|
+
const lunarFestivals = function (
|
|
105
|
+
lYear: number,
|
|
106
|
+
lMonth: number,
|
|
107
|
+
lDay: number,
|
|
108
|
+
isLeap: boolean,
|
|
109
|
+
lFtv: Record<string, string[]>,
|
|
110
|
+
): readonly string[] {
|
|
111
|
+
if (isLeap) return NO_FESTIVAL;
|
|
112
|
+
|
|
113
|
+
const base = festivalList(lFtv, lMonth, lDay);
|
|
114
|
+
if (lMonth === 12 && lDay === lMonthDays(lYear, 12) && base.indexOf("除夕") === -1) {
|
|
115
|
+
return Object.freeze(base.concat("除夕"));
|
|
116
|
+
}
|
|
117
|
+
return base;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 核心换算,入参必须是已经校验过、且在支持范围内的公历日期
|
|
122
|
+
*/
|
|
123
|
+
const computeLunar = function (
|
|
124
|
+
config: ResolvedConfig,
|
|
125
|
+
sy: number,
|
|
126
|
+
sm: number,
|
|
127
|
+
sd: number,
|
|
128
|
+
): LunarResult {
|
|
129
|
+
const utc = Date.UTC(sy, sm - 1, sd);
|
|
130
|
+
const offset = Math.round((utc - MIN_SOLAR_UTC) / DAY_MS);
|
|
131
|
+
const { year, offsetInYear } = resolveLunarYear(offset);
|
|
132
|
+
|
|
133
|
+
// 月份段(闰月单独一段)按顺序扫描,最多 13 段
|
|
134
|
+
const segments = monthSegmentsOf(year);
|
|
135
|
+
let segment = segments[0] as LunarMonthSegment;
|
|
136
|
+
for (const item of segments) {
|
|
137
|
+
if (item.start > offsetInYear) break;
|
|
138
|
+
segment = item;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const month = segment.month;
|
|
142
|
+
const day = offsetInYear - segment.start + 1;
|
|
143
|
+
const isLeap = segment.isLeap;
|
|
144
|
+
|
|
145
|
+
const dayOfWeek = weekdayOf(sy, sm, sd);
|
|
146
|
+
const firstTermDay = getTerm(sy, sm * 2 - 1);
|
|
147
|
+
const secondTermDay = getTerm(sy, sm * 2);
|
|
148
|
+
|
|
149
|
+
let term: string | null = null;
|
|
150
|
+
if (firstTermDay === sd) term = getSolarTerm(sm * 2 - 2);
|
|
151
|
+
if (secondTermDay === sd) term = getSolarTerm(sm * 2 - 1);
|
|
152
|
+
|
|
153
|
+
// xiu / ban 只由 rest.json 决定,与周末无关;同一天不会既放假又补班,两种状态互斥
|
|
154
|
+
const restState = restOf(config.restIndex, sy, sm, sd);
|
|
155
|
+
const isXiu = restState === "xiu";
|
|
156
|
+
const isBan = restState === "ban";
|
|
157
|
+
|
|
158
|
+
const dayCyclical = utc / DAY_MS - (sd - 1) + 25567 + 10;
|
|
159
|
+
const gzDayOffset = dayCyclical + sd - 1;
|
|
160
|
+
// 只有 2 月才需要立春当天的日期,其它月份不去查节气表
|
|
161
|
+
const ganzhiYear = sm > 2 || (sm === 2 && sd >= getTerm(sy, 3)) ? sy : sy - 1;
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
xiu: isXiu,
|
|
165
|
+
ban: isBan,
|
|
166
|
+
gljr: solarFestivals(sy, sm, sd, config.sFtv),
|
|
167
|
+
nljr: lunarFestivals(year, month, day, isLeap, config.lFtv),
|
|
168
|
+
lYear: year,
|
|
169
|
+
lMonth: month,
|
|
170
|
+
lDay: day,
|
|
171
|
+
animal: getAnimal(year),
|
|
172
|
+
lMonthCn: (isLeap ? "闰" : "") + toChinaMonth(month),
|
|
173
|
+
lDayCn: toChinaDay(day),
|
|
174
|
+
cYear: sy,
|
|
175
|
+
cMonth: sm,
|
|
176
|
+
cDay: sd,
|
|
177
|
+
gzYear: toGanZhiYear(ganzhiYear),
|
|
178
|
+
gzYearLunar: toGanZhiYear(year),
|
|
179
|
+
gzMonth: toGanZhi((sy - 1900) * 12 + sm + (sd >= firstTermDay ? 12 : 11)),
|
|
180
|
+
gzDay: toGanZhi(gzDayOffset),
|
|
181
|
+
gzHour: toHourGanZhi(gzDayOffset),
|
|
182
|
+
isToday: (function () {
|
|
183
|
+
const now = new Date();
|
|
184
|
+
return now.getFullYear() === sy && now.getMonth() + 1 === sm && now.getDate() === sd;
|
|
185
|
+
})(),
|
|
186
|
+
isLeap,
|
|
187
|
+
nWeek: dayOfWeek === 0 ? 7 : dayOfWeek,
|
|
188
|
+
dayOfWeek,
|
|
189
|
+
ncWeek: "星期" + toChinaNum(dayOfWeek),
|
|
190
|
+
term,
|
|
191
|
+
astro: toAstro(sm, sd),
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** 公历转农历的内部实现,节假日配置由调用方固定 */
|
|
196
|
+
const solarToLunarWith = function (
|
|
197
|
+
config: ResolvedConfig,
|
|
198
|
+
y?: number,
|
|
199
|
+
m?: number,
|
|
200
|
+
d?: number,
|
|
201
|
+
): LunarResult {
|
|
202
|
+
if (y === undefined && m === undefined && d === undefined) {
|
|
203
|
+
const now = new Date();
|
|
204
|
+
const today = validateSolar(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
|
205
|
+
return computeLunar(config, today.year, today.month, today.day);
|
|
206
|
+
}
|
|
207
|
+
if (y === undefined || m === undefined || d === undefined) {
|
|
208
|
+
failRange(
|
|
209
|
+
"solarToLunar",
|
|
210
|
+
`年、月、日必须同时传入,或全部不传以取今天(收到 year=${String(y)}, month=${String(m)}, day=${String(d)})`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const date = validateSolar(y, m, d);
|
|
214
|
+
return computeLunar(config, date.year, date.month, date.day);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** 农历转公历的内部实现,节假日配置由调用方固定 */
|
|
218
|
+
const lunarToSolarWith = function (
|
|
219
|
+
config: ResolvedConfig,
|
|
220
|
+
y: number,
|
|
221
|
+
m: number,
|
|
222
|
+
d: number,
|
|
223
|
+
isLeapMonth?: boolean,
|
|
224
|
+
): LunarResult {
|
|
225
|
+
const year = assertInteger("lunarToSolar(year)", y, MIN_YEAR, MAX_YEAR);
|
|
226
|
+
const month = assertInteger("lunarToSolar(month)", m, 1, 12);
|
|
227
|
+
if (isLeapMonth !== undefined && typeof isLeapMonth !== "boolean") {
|
|
228
|
+
throw failType("lunarToSolar(isLeapMonth)", `必须是布尔值,收到 ${describe(isLeapMonth)}`);
|
|
229
|
+
}
|
|
230
|
+
const leap = leapMonth(year);
|
|
231
|
+
const wantLeap = isLeapMonth === true;
|
|
232
|
+
if (wantLeap && leap !== month) {
|
|
233
|
+
failRange(
|
|
234
|
+
"lunarToSolar(isLeapMonth)",
|
|
235
|
+
`${year} 年${leap === 0 ? "没有闰月" : `的闰月是闰${leap}月`},不能按闰${month}月换算`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const maxDay = wantLeap ? leapDays(year) : lMonthDays(year, month);
|
|
239
|
+
const day = assertInteger("lunarToSolar(day)", d, 1, maxDay);
|
|
240
|
+
|
|
241
|
+
// 直接查表定位到该月第一天的绝对偏移,不再逐年逐月累加
|
|
242
|
+
const segments = monthSegmentsOf(year);
|
|
243
|
+
const segment = segments.find((item) => item.month === month && item.isLeap === wantLeap);
|
|
244
|
+
if (!segment) {
|
|
245
|
+
return failRange("lunarToSolar", `${year} 年${wantLeap ? "闰" : ""}${month}月不存在`);
|
|
246
|
+
}
|
|
247
|
+
const offset = yearStartOf(year) + segment.start;
|
|
248
|
+
|
|
249
|
+
const utc = MIN_SOLAR_UTC + (offset + day - 1) * DAY_MS;
|
|
250
|
+
if (utc > MAX_SOLAR_UTC) {
|
|
251
|
+
failRange("lunarToSolar", `农历 ${year}-${month}-${day} 换算后晚于 2100-12-31,超出支持范围`);
|
|
252
|
+
}
|
|
253
|
+
const date = new Date(utc);
|
|
254
|
+
return computeLunar(config, date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate());
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 创建固定节假日配置的日历实例
|
|
259
|
+
*
|
|
260
|
+
* 不读取也不修改任何全局状态;配置在创建时校验并冻结,
|
|
261
|
+
* 多个实例之间互不影响。不传配置时使用内置数据。
|
|
262
|
+
*
|
|
263
|
+
* @param config 节假日 / 调休配置
|
|
264
|
+
* @param options replace 为 true 时忽略内置数据
|
|
265
|
+
* @throws TypeError 配置格式不合法(含具体字段路径)
|
|
266
|
+
* @example
|
|
267
|
+
* const cc = createCalendar({ setRest: { 2027: { xiu: ["0101"] } } });
|
|
268
|
+
* cc.solarToLunar(2027, 1, 1).xiu; // true
|
|
269
|
+
*/
|
|
270
|
+
const createCalendar = function (
|
|
271
|
+
config: HolidayConfig = {},
|
|
272
|
+
options: SetHolidayOptions = {},
|
|
273
|
+
): Calendar {
|
|
274
|
+
// 用户配置只校验一次;只有一份可查询的配置,own 只在 getHolidays 需要时生成快照
|
|
275
|
+
const { merged, snapshotOwn } = createCalendarConfig(config, options);
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
/** 公历转农历,不传参返回今天 */
|
|
279
|
+
solarToLunar: (y, m, d) => solarToLunarWith(merged, y, m, d),
|
|
280
|
+
/** 农历转公历 */
|
|
281
|
+
lunarToSolar: (y, m, d, isLeapMonth) => lunarToSolarWith(merged, y, m, d, isLeapMonth),
|
|
282
|
+
/** 当前配置的深拷贝;merged 为 false 时只导出用户传入的部分 */
|
|
283
|
+
getHolidays: (getOptions) => {
|
|
284
|
+
if (getOptions !== undefined) assertPlainObject(getOptions, "getHolidays(options)");
|
|
285
|
+
if (getOptions?.merged !== undefined && typeof getOptions.merged !== "boolean") {
|
|
286
|
+
throw failType(
|
|
287
|
+
"getHolidays(options).merged",
|
|
288
|
+
`必须是布尔值,收到 ${describe(getOptions.merged)}`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return getOptions?.merged === false ? snapshotOwn() : snapshotOf(merged);
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const utils = {
|
|
297
|
+
lYearDays,
|
|
298
|
+
leapMonth,
|
|
299
|
+
leapDays,
|
|
300
|
+
lMonthDays,
|
|
301
|
+
toChinaMonth,
|
|
302
|
+
toChinaDay,
|
|
303
|
+
toChinaNum,
|
|
304
|
+
solarDays,
|
|
305
|
+
solarFirstWeek,
|
|
306
|
+
nthWeekdayOfMonth,
|
|
307
|
+
toAstro,
|
|
308
|
+
getAnimal,
|
|
309
|
+
getTerm,
|
|
310
|
+
getSolarTerm,
|
|
311
|
+
toGanZhi,
|
|
312
|
+
toGanZhiYear,
|
|
313
|
+
toHourGanZhi,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export { createCalendar, utils };
|
|
317
|
+
export default createCalendar;
|