Files
MailUI4Agents/server/internal/lunar/lunar_test.go

217 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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