feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// L3 契约:Solar() 保持库内默认选择,SolarCandidates() 给出全部合法候选(主答案在首),
|
||||
// 且每个候选都能正向回到该农历日。
|
||||
func TestSolarCandidatesForReformWindows(t *testing.T) {
|
||||
cases := []struct {
|
||||
year, month, day int
|
||||
leap bool
|
||||
want []string
|
||||
}{
|
||||
{762, 4, 1, false, []string{"0762-03-01", "0762-04-29"}}, // 唐肃宗改制
|
||||
{700, 11, 1, false, []string{"0700-12-15", "0700-10-17"}}, // 武则天改制
|
||||
{700, 12, 1, false, []string{"0700-11-15", "0701-01-14"}}, // 武则天改制
|
||||
{23, 12, 1, false, []string{"0023-12-02", "0023-12-31"}}, // 王莽改制
|
||||
{239, 12, 1, false, []string{"0239-12-13", "0240-01-12"}}, // 魏明帝改制
|
||||
{-104, 10, 1, false, []string{"-0104-11-26", "-0105-11-08"}}, // 太初改历交接
|
||||
}
|
||||
for _, c := range cases {
|
||||
res, err := LunarToSolarByYMD(c.year, c.month, c.day, c.leap)
|
||||
if err != nil {
|
||||
t.Fatalf("LunarToSolarByYMD(%d,%d,%d,%v) error: %v", c.year, c.month, c.day, c.leap, err)
|
||||
}
|
||||
candidates := res.SolarCandidates()
|
||||
if len(candidates) != len(c.want) {
|
||||
t.Errorf("%d/%d/%d: got %d candidates %v, want %d %v",
|
||||
c.year, c.month, c.day, len(candidates), formatCandidates(candidates), len(c.want), c.want)
|
||||
continue
|
||||
}
|
||||
if !candidates[0].Equal(res.Solar()) {
|
||||
t.Errorf("%d/%d/%d: first candidate %s is not Solar() %s",
|
||||
c.year, c.month, c.day, formatAncientSolar(candidates[0]), formatAncientSolar(res.Solar()))
|
||||
}
|
||||
for i, v := range candidates {
|
||||
if got := formatAncientSolar(v); got != c.want[i] {
|
||||
t.Errorf("%d/%d/%d: candidate %d = %s, want %s", c.year, c.month, c.day, i, got, c.want[i])
|
||||
}
|
||||
for j := 0; j < i; j++ {
|
||||
if candidates[j].Equal(v) {
|
||||
t.Errorf("%d/%d/%d: candidate %d duplicates candidate %d (%s)",
|
||||
c.year, c.month, c.day, i, j, formatAncientSolar(v))
|
||||
}
|
||||
}
|
||||
fwd, err := SolarToLunarByYMD(v.Year(), int(v.Month()), v.Day())
|
||||
if err != nil {
|
||||
t.Errorf("%d/%d/%d: candidate %s does not convert forward: %v",
|
||||
c.year, c.month, c.day, formatAncientSolar(v), err)
|
||||
continue
|
||||
}
|
||||
l := fwd.Lunar()
|
||||
if l.LunarYear() != c.year || l.LunarMonth() != c.month || l.LunarDay() != c.day || l.IsLeap() != c.leap {
|
||||
t.Errorf("%d/%d/%d: candidate %s converts back to lunar %d/%d/%d leap=%v",
|
||||
c.year, c.month, c.day, formatAncientSolar(v),
|
||||
l.LunarYear(), l.LunarMonth(), l.LunarDay(), l.IsLeap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 改历窗口外只有一个候选,且 SolarCandidates() 返回的是副本。
|
||||
func TestSolarCandidatesAreSingleOutsideReformWindows(t *testing.T) {
|
||||
cases := []struct{ year, month, day int }{
|
||||
{763, 4, 1}, {701, 11, 1}, {24, 12, 1}, {241, 12, 1}, {-103, 10, 1},
|
||||
{-105, 10, 1}, {2025, 1, 1}, {1912, 1, 1}, {1000, 7, 7}, {1, 1, 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
res, err := LunarToSolarByYMD(c.year, c.month, c.day, false)
|
||||
if err != nil {
|
||||
t.Fatalf("LunarToSolarByYMD(%d,%d,%d) error: %v", c.year, c.month, c.day, err)
|
||||
}
|
||||
candidates := res.SolarCandidates()
|
||||
if len(candidates) != 1 {
|
||||
t.Errorf("%d/%d/%d: got %d candidates %v, want a single one",
|
||||
c.year, c.month, c.day, len(candidates), formatCandidates(candidates))
|
||||
continue
|
||||
}
|
||||
if !candidates[0].Equal(res.Solar()) {
|
||||
t.Errorf("%d/%d/%d: single candidate %s is not Solar() %s",
|
||||
c.year, c.month, c.day, formatAncientSolar(candidates[0]), formatAncientSolar(res.Solar()))
|
||||
}
|
||||
// 返回值必须是副本。
|
||||
candidates[0] = candidates[0].AddDate(1, 1, 1)
|
||||
if again := res.SolarCandidates(); !again[0].Equal(res.Solar()) {
|
||||
t.Errorf("%d/%d/%d: SolarCandidates() is not a copy (%s)", c.year, c.month, c.day, formatAncientSolar(again[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1582 改历跳过的十天本身不存在,与 L2/L3 无关。
|
||||
func TestGregorianSkippedDaysHaveNoCandidates(t *testing.T) {
|
||||
for day := 5; day <= 14; day++ {
|
||||
if _, err := SolarToLunarByYMD(1582, 10, day); err == nil {
|
||||
t.Errorf("SolarToLunarByYMD(1582,10,%d) unexpectedly succeeded", day)
|
||||
}
|
||||
}
|
||||
res, err := LunarToSolarByYMD(1582, 9, 18, false)
|
||||
if err != nil {
|
||||
t.Fatalf("LunarToSolarByYMD(1582,9,18) error: %v", err)
|
||||
}
|
||||
if got := len(res.SolarCandidates()); got != 1 {
|
||||
t.Errorf("lunar 1582/9/18 has %d candidates, want 1", got)
|
||||
}
|
||||
if _, err := time.Parse("2006-01-02", formatAncientSolar(res.Solar())); err != nil {
|
||||
t.Errorf("solar label %q is not a valid date", formatAncientSolar(res.Solar()))
|
||||
}
|
||||
}
|
||||
|
||||
func formatCandidates(candidates []time.Time) []string {
|
||||
out := make([]string, 0, len(candidates))
|
||||
for _, v := range candidates {
|
||||
out = append(out, formatAncientSolar(v))
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user