feat(lunar): 双端农历换算层(Go + TS,同作者同算法)
日历要支持「每农历月十五」「农历生日」这类规则。公历与农历的换算不能
自己算,两端各引一个库:Go 用 6tail/lunar-go v1.4.6,前端用同作者的
lunar-javascript 1.7.7 —— 同算法保证两端结果一致(前端要在格子上显示
农历日、在编辑器里预览接下来几次触发)。
为什么要包一层而不直接用库:
**1. 库在非法日期上 panic 而不是返回 error。**
`NewLunarFromYmd(2027, 9, 30)` 直接 panic("only 29 days in lunar year
2027 month 9")。农历月是 29 或 30 天不定,「每月农历三十」这条规则必然
撞上短月份。调度器里一次 panic 就让那条提醒永久卡住。
修法是夹到该月实际天数并返回 clamped 标记 —— 夹而不滚:「每月三十」的
语义是「月末那天」,滚到下月初一会让提醒与前一次只隔一天。
**2. 闰月用负数月份表示**(-6 = 闰六月),这个约定藏在库内部。
2025 有闰六月、2028 有闰五月,2026/2027 没有。AddYears 从闰月出发而
目标年没有同一闰月时退回正月份 —— 静默让重复事件消失更糟。
**3. 按农历推进不能加固定天数。**
农历月 29~30 天、农历年 353~385 天(闰年多一整月),AddDate 近似一年
能偏半个月。
前端另有一个 TS 陷阱:日名有五种前缀形态(初一/十一/二十/廿一/三十),
原来用正则从 toString() 截取时漏了「二十」,20 号会显示整串「七月二十」。
改成查表。
测试:Go 12 例 / TS 37 例。含「同一农历日在六年公历里落到至少 4 个不同
月日上」—— 那正是农历重复存在的理由(公历 yearly 会固定在同一天)。
This commit is contained in:
57
web/src/types/lunar-javascript.d.ts
vendored
Normal file
57
web/src/types/lunar-javascript.d.ts
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* lunar-javascript 的类型声明。
|
||||
*
|
||||
* 上游没有发布 .d.ts(也没有 @types/lunar-javascript),不声明的话
|
||||
* `import` 直接 TS7016 编译失败。
|
||||
*
|
||||
* 只声明我们真正用到的成员而不是 `declare module 'lunar-javascript'`
|
||||
* (那等于放弃整个模块的类型)—— 写错方法名时仍要能在编译期发现,
|
||||
* 否则会变成运行时的「undefined is not a function」,而日历页面
|
||||
* 一旦抛错整片区域白屏。
|
||||
*/
|
||||
declare module 'lunar-javascript' {
|
||||
export interface LunarMonthLike {
|
||||
getYear(): number;
|
||||
/** 负数表示闰月 */
|
||||
getMonth(): number;
|
||||
/** 29 或 30 */
|
||||
getDayCount(): number;
|
||||
}
|
||||
|
||||
export interface SolarLike {
|
||||
getYear(): number;
|
||||
/** 1-12 */
|
||||
getMonth(): number;
|
||||
getDay(): number;
|
||||
toYmd(): string;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export interface LunarLike {
|
||||
getYear(): number;
|
||||
/** 负数表示闰月 */
|
||||
getMonth(): number;
|
||||
getDay(): number;
|
||||
getSolar(): SolarLike;
|
||||
/** 「二〇二六年七月廿二」 */
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export const Solar: {
|
||||
fromYmd(year: number, month: number, day: number): SolarLike & { getLunar(): LunarLike };
|
||||
};
|
||||
|
||||
export const Lunar: {
|
||||
/** month 传负数表示闰月。**非法日期会 throw** —— 农历月是 29 或 30 天不定。 */
|
||||
fromYmd(year: number, month: number, day: number): LunarLike;
|
||||
};
|
||||
|
||||
export const LunarYear: {
|
||||
fromYear(year: number): {
|
||||
/** 0 = 无闰月 */
|
||||
getLeapMonth(): number;
|
||||
/** 含跨年边界的月份,因此使用时必须同时比对 getYear() */
|
||||
getMonths(): LunarMonthLike[];
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user