167 lines
7.2 KiB
Go
167 lines
7.2 KiB
Go
|
|
package calendar
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"testing"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 1582 改历:反向的 +10 天窗口必须与正向的 −10 天修正对称(1582-10-05..1583-01-13)。
|
|||
|
|
// formatAncientSolar 统一负年份的显示。
|
|||
|
|
func formatAncientSolar(t time.Time) string {
|
|||
|
|
year, sign := t.Year(), ""
|
|||
|
|
if year < 0 {
|
|||
|
|
year, sign = -year, "-"
|
|||
|
|
}
|
|||
|
|
return fmt.Sprintf("%s%04d-%02d-%02d", sign, year, int(t.Month()), t.Day())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestGregorianReformReverseMatchesForward(t *testing.T) {
|
|||
|
|
cases := []struct {
|
|||
|
|
ly, lm, ld int
|
|||
|
|
leap bool
|
|||
|
|
want string
|
|||
|
|
}{
|
|||
|
|
{1582, 12, 8, false, "1583-01-01"}, // 连续坐标 1582-12-22,原有 1582 规则覆盖
|
|||
|
|
{1582, 12, 18, false, "1583-01-11"}, // 连续坐标 1583-01-01,修复前返回 1583-01-01
|
|||
|
|
{1582, 12, 30, false, "1583-01-23"}, // 连续坐标 1583-01-13,修复前返回 1583-01-13
|
|||
|
|
{1583, 1, 1, false, "1583-01-24"}, // 连续坐标 1583-01-14,本就不该 +10
|
|||
|
|
}
|
|||
|
|
for _, tc := range cases {
|
|||
|
|
got, err := LunarToSolarByYMD(tc.ly, tc.lm, tc.ld, tc.leap)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) err=%v", tc.ly, tc.lm, tc.ld, err)
|
|||
|
|
}
|
|||
|
|
if s := got.Solar().Format("2006-01-02"); s != tc.want {
|
|||
|
|
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) = %s, want %s", tc.ly, tc.lm, tc.ld, s, tc.want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 改历前后整段往返一致(跳过被删除的 1582-10-05..14,它们在公历里不存在)。
|
|||
|
|
// The whole span round-trips apart from the ten days the reform deleted.
|
|||
|
|
for day := time.Date(1582, 10, 15, 0, 0, 0, 0, getCst()); day.Before(time.Date(1583, 3, 1, 0, 0, 0, 0, getCst())); day = day.AddDate(0, 0, 1) {
|
|||
|
|
y, m, d := day.Year(), int(day.Month()), day.Day()
|
|||
|
|
lunar, err := SolarToLunarByYMD(y, m, d)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("SolarToLunarByYMD(%d,%d,%d) err=%v", y, m, d, err)
|
|||
|
|
}
|
|||
|
|
l := lunar.Lunar()
|
|||
|
|
back, err := LunarToSolarByYMD(l.LunarYear(), l.LunarMonth(), l.LunarDay(), l.IsLeap())
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) err=%v", l.LunarYear(), l.LunarMonth(), l.LunarDay(), err)
|
|||
|
|
}
|
|||
|
|
if s := back.Solar(); s.Year() != y || int(s.Month()) != m || s.Day() != d {
|
|||
|
|
t.Fatalf("%d-%02d-%02d → 农历 %d/%d/%d → 反向 %s(改历往返不一致)",
|
|||
|
|
y, m, d, l.LunarYear(), l.LunarMonth(), l.LunarDay(), s.Format("2006-01-02"))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 先秦岁首跨界:正向按公历年选默认古历、反向按农历年选,交界处必须自洽。
|
|||
|
|
// 农历 -479 年正月落在公历 -480 年 12 月(春秋历),此前反向按农历年选了周历,整体差 1 天。
|
|||
|
|
// The pre-Qin year-start crossing: the forward picks the default ancient calendar by civil
|
|||
|
|
// year while the reverse picks by lunar year. Lunar year -479 month 1 falls in civil December
|
|||
|
|
// -480 (Chunqiu calendar); the reverse used to pick Zhou and shift those 20 days by one day.
|
|||
|
|
func TestAncientYearStartCrossingRoundTrips(t *testing.T) {
|
|||
|
|
cases := []struct {
|
|||
|
|
ly, lm, ld int
|
|||
|
|
want string
|
|||
|
|
}{
|
|||
|
|
{-479, 1, 1, "-0480-12-12"},
|
|||
|
|
{-479, 1, 20, "-0480-12-31"},
|
|||
|
|
{-479, 1, 22, "-0479-01-01"},
|
|||
|
|
{-479, 1, 29, "-0479-01-08"},
|
|||
|
|
}
|
|||
|
|
for _, tc := range cases {
|
|||
|
|
got, err := LunarToSolarByYMD(tc.ly, tc.lm, tc.ld, false)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) err=%v", tc.ly, tc.lm, tc.ld, err)
|
|||
|
|
}
|
|||
|
|
if s := formatAncientSolar(got.Solar()); s != tc.want {
|
|||
|
|
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) = %s, want %s", tc.ly, tc.lm, tc.ld, s, tc.want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 该农历日在正向可达的范围内没有对应公历日(春秋历说是 -479-01-01、周历说是 -480-12-31,
|
|||
|
|
// 而正向对这两个公历日分别给出周历 1/22 与春秋历 1/20),默认口径必须报错而不是给错日子。
|
|||
|
|
// That lunar day has no civil date in the default frame (the forward maps -479-01-01 to Zhou
|
|||
|
|
// 1/22 and -480-12-31 to Chunqiu 1/20), so the default entry point must report an error.
|
|||
|
|
if _, err := LunarToSolarByYMD(-479, 1, 21, false); err == nil {
|
|||
|
|
t.Fatal("LunarToSolarByYMD(-479,1,21) 应报错:该农历日在默认历法口径下没有公历日")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 交界窗口往返一致。
|
|||
|
|
// The junction window round-trips.
|
|||
|
|
for day := time.Date(-480, 12, 1, 0, 0, 0, 0, getCst()); day.Before(time.Date(-479, 2, 1, 0, 0, 0, 0, getCst())); day = day.AddDate(0, 0, 1) {
|
|||
|
|
y, m, d := day.Year(), int(day.Month()), day.Day()
|
|||
|
|
lunar, err := SolarToLunarByYMD(y, m, d)
|
|||
|
|
if err != nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
l := lunar.Lunar()
|
|||
|
|
back, err := LunarToSolarByYMD(l.LunarYear(), l.LunarMonth(), l.LunarDay(), l.IsLeap())
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("%d-%02d-%02d → 农历 %d/%d/%d → 反向 err=%v", y, m, d, l.LunarYear(), l.LunarMonth(), l.LunarDay(), err)
|
|||
|
|
}
|
|||
|
|
if s := back.Solar(); s.Year() != y || int(s.Month()) != m || s.Day() != d {
|
|||
|
|
t.Fatalf("%d-%02d-%02d → 农历 %d/%d/%d → 反向 %s(岁首跨界往返不一致)",
|
|||
|
|
y, m, d, l.LunarYear(), l.LunarMonth(), l.LunarDay(), formatAncientSolar(s))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 王莽改历的 23/24 年交界:同一套月序偏移会把"腊月三十"反解到腊月初一(标签碰撞),
|
|||
|
|
// 此时应改用另一套偏移下能正向回到该农历日的那一天,并把主答案换成它。
|
|||
|
|
// At the Wang Mang 23/24 boundary the primary month offset resolves "12th month, day 30" onto the
|
|||
|
|
// 1st of that month (a label collision); the reverse must switch to the offset whose civil day
|
|||
|
|
// converts forward to the requested lunar date and make that day the primary answer.
|
|||
|
|
func TestWangMangBoundaryReverseIsSelfConsistent(t *testing.T) {
|
|||
|
|
cases := []struct {
|
|||
|
|
ly, lm, ld int
|
|||
|
|
wantSolar string
|
|||
|
|
selfConsist bool
|
|||
|
|
}{
|
|||
|
|
{23, 12, 29, "0023-12-30", true},
|
|||
|
|
{23, 12, 30, "0024-01-29", true},
|
|||
|
|
{24, 1, 1, "0024-01-30", true},
|
|||
|
|
// 23/12/31 在两套月序下都没有对应的公历日(该年有两个腊月,第二个腊月只有 30 天),
|
|||
|
|
// 保持既有行为返回偏移 1 的结果,只断言主答案不变成空值。
|
|||
|
|
{23, 12, 31, "0024-01-01", false},
|
|||
|
|
}
|
|||
|
|
for _, c := range cases {
|
|||
|
|
res, err := LunarToSolarByYMD(c.ly, c.lm, c.ld, false)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) error: %v", c.ly, c.lm, c.ld, err)
|
|||
|
|
}
|
|||
|
|
if got := formatAncientSolar(res.Solar()); got != c.wantSolar {
|
|||
|
|
t.Errorf("lunar %d/%d/%d -> %s, want %s", c.ly, c.lm, c.ld, got, c.wantSolar)
|
|||
|
|
}
|
|||
|
|
if !c.selfConsist {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
fwd, err := SolarToLunarByYMD(res.Solar().Year(), int(res.Solar().Month()), res.Solar().Day())
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("SolarToLunarByYMD(%s) error: %v", c.wantSolar, err)
|
|||
|
|
}
|
|||
|
|
l := fwd.Lunar()
|
|||
|
|
if l.LunarYear() != c.ly || l.LunarMonth() != c.lm || l.LunarDay() != c.ld || l.IsLeap() {
|
|||
|
|
t.Errorf("lunar %d/%d/%d -> %s converts forward to %d/%d/%d",
|
|||
|
|
c.ly, c.lm, c.ld, c.wantSolar, l.LunarYear(), l.LunarMonth(), l.LunarDay())
|
|||
|
|
}
|
|||
|
|
if res.JDE() != fwd.JDE() {
|
|||
|
|
t.Errorf("lunar %d/%d/%d: JDE=%.1f, forward gives %.1f", c.ly, c.lm, c.ld, res.JDE(), fwd.JDE())
|
|||
|
|
}
|
|||
|
|
for _, cand := range res.SolarCandidates() {
|
|||
|
|
candFwd, err := SolarToLunarByYMD(cand.Year(), int(cand.Month()), cand.Day())
|
|||
|
|
if err != nil {
|
|||
|
|
t.Errorf("lunar %d/%d/%d candidate %s does not convert: %v", c.ly, c.lm, c.ld, formatAncientSolar(cand), err)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
cl := candFwd.Lunar()
|
|||
|
|
if cl.LunarYear() != c.ly || cl.LunarMonth() != c.lm || cl.LunarDay() != c.ld || cl.IsLeap() {
|
|||
|
|
t.Errorf("lunar %d/%d/%d candidate %s converts to %d/%d/%d",
|
|||
|
|
c.ly, c.lm, c.ld, formatAncientSolar(cand), cl.LunarYear(), cl.LunarMonth(), cl.LunarDay())
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|