chore: directory migration - gateway→server, web→client/electron

This commit is contained in:
2026-09-08 19:16:35 +08:00
parent fd9f99a3f9
commit f9d757b5e5
243 changed files with 5095 additions and 228 deletions

View File

@ -0,0 +1,216 @@
package lunar
import (
"testing"
"time"
)
// 已知对照点。农历换算错了不会报错,只会让提醒发在错误的日子,
// 因此必须钉住几个可人工核对的锚点。
func TestFromSolarKnownDates(t *testing.T) {
cases := []struct {
solar string
year int
month int
day int
}{
{"2026-09-03", 2026, 7, 22},
{"2026-01-01", 2025, 11, 13},
// 2025 有闰六月:闰月里的日子 Month 应为负
{"2025-07-25", 2025, -6, 1},
{"2025-06-25", 2025, 6, 1},
}
for _, c := range cases {
st, err := time.Parse("2006-01-02", c.solar)
if err != nil {
t.Fatalf("解析 %s: %v", c.solar, err)
}
got := FromSolar(st)
if got.Year != c.year || got.Month != c.month || got.Day != c.day {
t.Errorf("%s → 农历 %d-%d-%d,期望 %d-%d-%d",
c.solar, got.Year, got.Month, got.Day, c.year, c.month, c.day)
}
}
}
func TestToSolarRoundtrip(t *testing.T) {
loc := time.Local
for _, solar := range []string{"2026-09-03", "2027-02-14", "2028-06-30", "2025-07-25"} {
st, _ := time.Parse("2006-01-02", solar)
d := FromSolar(st)
back, clamped, err := d.ToSolar(loc, 9, 30, 0, 0)
if err != nil {
t.Errorf("%s 往返失败: %v", solar, err)
continue
}
if clamped {
t.Errorf("%s 往返不该发生夹取", solar)
}
if back.Format("2006-01-02") != solar {
t.Errorf("%s 往返得到 %s", solar, back.Format("2006-01-02"))
}
if back.Hour() != 9 || back.Minute() != 30 {
t.Errorf("%s 往返丢了时分:%v", solar, back)
}
}
}
// 农历月是 29 或 30 天不定,「每月农历三十」必然撞上 29 天的月份。
// lunar-go 在这种输入上**panic** 而不是返回 error —— 必须被夹住。
func TestToSolarClampsShortMonth(t *testing.T) {
// 2027 农历九月只有 29 天
days, err := DaysInMonth(2027, 9)
if err != nil {
t.Fatalf("查天数: %v", err)
}
if days != 29 {
t.Fatalf("前提变了:2027 农历九月现在是 %d 天", days)
}
got, clamped, err := Date{Year: 2027, Month: 9, Day: 30}.ToSolar(time.Local, 9, 0, 0, 0)
if err != nil {
t.Fatalf("三十日应被夹到廿九而不是报错,得到 %v", err)
}
if !clamped {
t.Error("应报告发生了夹取")
}
// 夹取后应等于该月廿九
want, _, _ := Date{Year: 2027, Month: 9, Day: 29}.ToSolar(time.Local, 9, 0, 0, 0)
if !got.Equal(want) {
t.Errorf("夹取后 %v,期望与廿九相同 %v", got, want)
}
// 且必须仍在同一个农历月内 —— 滚到下月初一是错的
if FromSolar(got).Month != 9 {
t.Errorf("夹取后跑出了农历九月:%s", FromSolar(got).String())
}
}
func TestToSolarRejectsNonexistentLeapMonth(t *testing.T) {
// 2026 无闰月
if LeapMonth(2026) != 0 {
t.Fatalf("前提变了:2026 闰月 = %d", LeapMonth(2026))
}
_, _, err := Date{Year: 2026, Month: -6, Day: 1}.ToSolar(time.Local, 9, 0, 0, 0)
if err == nil {
t.Error("不存在的闰月应返回错误而不是猜一个日子")
}
}
func TestLeapMonth(t *testing.T) {
cases := map[int]int{2025: 6, 2026: 0, 2027: 0, 2028: 5}
for y, want := range cases {
if got := LeapMonth(y); got != want {
t.Errorf("LeapMonth(%d) = %d,期望 %d", y, got, want)
}
}
}
func TestDaysInMonth(t *testing.T) {
if d, err := DaysInMonth(2026, 1); err != nil || d != 30 {
t.Errorf("2026 正月应 30 天,得到 %d err=%v", d, err)
}
if d, err := DaysInMonth(2027, 9); err != nil || d != 29 {
t.Errorf("2027 九月应 29 天,得到 %d err=%v", d, err)
}
if _, err := DaysInMonth(2026, -6); err == nil {
t.Error("2026 没有闰六月,应返回错误")
}
if _, err := DaysInMonth(2026, 13); err == nil {
t.Error("13 月应返回错误")
}
}
// 按农历推进不能靠加固定天数:农历月 29~30 天,闰年 13 个月。
func TestAddMonths(t *testing.T) {
d := Date{Year: 2026, Month: 7, Day: 22}
if got := d.AddMonths(1); got.Month != 8 || got.Year != 2026 {
t.Errorf("+1 月 = %d-%d,期望 2026-8", got.Year, got.Month)
}
// 跨年
if got := (Date{Year: 2026, Month: 12, Day: 5}).AddMonths(1); got.Year != 2027 || got.Month != 1 {
t.Errorf("腊月 +1 = %d-%d,期望 2027-1", got.Year, got.Month)
}
// 推 12 次回到同月次年
if got := d.AddMonths(12); got.Year != 2027 || got.Month != 7 {
t.Errorf("+12 月 = %d-%d,期望 2027-7", got.Year, got.Month)
}
// 从闰月出发先归正:闰六月 +1 → 七月(一年 12 次,不因闰年多一次)
if got := (Date{Year: 2025, Month: -6, Day: 15}).AddMonths(1); got.Month != 7 {
t.Errorf("闰六月 +1 = %d 月,期望 7 月", got.Month)
}
}
func TestAddYears(t *testing.T) {
if got := (Date{Year: 2026, Month: 7, Day: 22}).AddYears(1); got.Year != 2027 || got.Month != 7 {
t.Errorf("+1 年 = %d-%d", got.Year, got.Month)
}
// 从闰月出发、目标年没有同一闰月 → 退回正月份而不是静默消失
got := (Date{Year: 2025, Month: -6, Day: 15}).AddYears(1)
if got.Year != 2026 || got.Month != 6 {
t.Errorf("闰六月 +1 年 = %d-%d,期望 2026-6(退回正六月)", got.Year, got.Month)
}
// 目标年恰好也有同一闰月 → 保持闰月
// 2025 闰六月 → 2028 闰五月,所以这里构造 2028 的闰五月 +0 年
if LeapMonth(2028) == 5 {
keep := (Date{Year: 2028, Month: -5, Day: 1}).AddYears(0)
if keep.Month != -5 {
t.Errorf("目标年有同一闰月时应保持,得到 %d", keep.Month)
}
}
}
// 「每年农历某月某日」的公历日期每年都在漂移 —— 这正是需要农历重复的理由。
// 如果用公历 yearly,日子会固定,与用户的期望(过农历生日/祭日)不符。
func TestYearlyLunarDriftsInSolar(t *testing.T) {
seen := map[string]bool{}
d := Date{Year: 2026, Month: 7, Day: 22}
for i := 0; i < 6; i++ {
st, _, err := d.AddYears(i).ToSolar(time.Local, 9, 0, 0, 0)
if err != nil {
t.Fatalf("第 %d 年转换失败: %v", i, err)
}
seen[st.Format("01-02")] = true
}
if len(seen) < 4 {
t.Errorf("六年里公历月日只有 %d 种,农历重复应当漂移", len(seen))
}
}
func TestStringChinese(t *testing.T) {
got := (Date{Year: 2026, Month: 7, Day: 22}).String()
if got != "二〇二六年七月廿二" {
t.Errorf("String() = %q,期望 二〇二六年七月廿二", got)
}
// 闰月要带「闰」字,否则人分不清是哪个月
leap := (Date{Year: 2025, Month: -6, Day: 1}).String()
if leap != "二〇二五年闰六月初一" {
t.Errorf("闰月 String() = %q", leap)
}
// 非法日期不 panic
bad := (Date{Year: 2026, Month: 13, Day: 1}).String()
if bad == "" {
t.Error("非法日期应返回可读文本而不是空串")
}
}
func TestFormatSolar(t *testing.T) {
st, _ := time.Parse("2006-01-02", "2026-09-03")
got := FormatSolar(st)
if got != "2026-09-03(农历七月廿二)" {
t.Errorf("FormatSolar = %q,期望 2026-09-03(农历七月廿二)", got)
}
}
// 时区:农历只定义到「日」,时分秒是公历时钟的概念,必须原样带过去。
func TestToSolarKeepsClockTime(t *testing.T) {
got, _, err := (Date{Year: 2026, Month: 7, Day: 22}).ToSolar(time.Local, 14, 45, 30, 0)
if err != nil {
t.Fatalf("转换: %v", err)
}
if got.Hour() != 14 || got.Minute() != 45 || got.Second() != 30 {
t.Errorf("时钟被改动:%v", got)
}
if got.Location() != time.Local {
t.Errorf("时区被改动:%v", got.Location())
}
}