217 lines
7.2 KiB
Go
217 lines
7.2 KiB
Go
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())
|
||
}
|
||
}
|