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()) } }