package svg
import (
"regexp"
"strings"
"testing"
"time"
"b612.me/astro"
eclipsecore "b612.me/astro/eclipse"
)
// UT1 出图:换算不能碰零值(零值表示"该阶段不存在"),且非 UTC 时区应明确失败而不是画错图。
func TestLunarEclipseUT1LabelsPreserveZeroPhases(t *testing.T) {
info := eclipsecore.LunarEclipseInfo{
Maximum: time.Date(2025, 9, 7, 18, 11, 0, 0, time.UTC),
}
got := eclipsecore.LunarEclipseInfoInUT1(info)
if !got.TotalStart.IsZero() {
t.Fatalf("不存在的阶段应保持零值, got %v", got.TotalStart)
}
if delta := got.Maximum.Sub(info.Maximum).Seconds(); delta <= 0 {
t.Fatalf("UT1 时刻应领先民用时刻, got %g 秒", delta)
}
}
// assertUT1Declaration 钉住 UT1 声明必须落在 元素内,并写出 DUT1 差值。
func assertUT1Declaration(t *testing.T, name, svg string) {
t.Helper()
declaration := "DUT1 = UT1−UTC = "
idx := strings.Index(svg, declaration)
if idx < 0 {
t.Errorf("%s 应在图内声明 UT1 并写出 DUT1 差值", name)
return
}
rest := svg[idx:]
end := strings.Index(rest, "")
if end < 0 || strings.Contains(rest[:end], "<") {
t.Errorf("%s 的 UT1 声明必须落在 元素内", name)
}
}
func TestLunarEclipseMapSVGTimeScaleOption(t *testing.T) {
date := time.Date(2025, time.September, 7, 12, 0, 0, 0, time.UTC)
utcSVG, ok := LunarEclipseMapSVG(date, LunarEclipseMapSVGOptions{})
if !ok {
t.Fatal("UTC 渲染应成功")
}
ut1SVG, ok := LunarEclipseMapSVG(date, LunarEclipseMapSVGOptions{TimeScale: astro.TimeScaleUT1})
if !ok {
t.Fatal("UT1 渲染应成功")
}
assertUT1Declaration(t, "月食全球图", ut1SVG)
if strings.Contains(utcSVG, "世界时") {
t.Fatal("UTC 图不应带 UT1 图注")
}
cst := time.FixedZone("CST", 8*3600)
if _, ok := LunarEclipseMapSVG(date, LunarEclipseMapSVGOptions{TimeScale: astro.TimeScaleUT1, Location: cst}); ok {
t.Fatal("UT1 配非 UTC 时区应返回 false")
}
if _, ok := LunarEclipseMapSVG(date, LunarEclipseMapSVGOptions{TimeScale: astro.TimeScaleUT1, Location: time.UTC}); !ok {
t.Fatal("UT1 配 UTC 时区应成功")
}
}
// 三个月食入口共用同一个展示副本:不存在的阶段(零值)不得被换算成真实时刻,
// 且展示副本只影响文字,几何仍走原 UTC 数据。
func TestLunarEclipseUT1LabelHelperPreservesZero(t *testing.T) {
real := time.Date(2025, 9, 7, 18, 11, 0, 0, time.UTC)
info := LunarEclipseInfo{Maximum: real, PenumbralStart: real}
display := lunarEclipseDisplayInfo(info, astro.TimeScaleUT1)
if !display.TotalStart.IsZero() {
t.Fatalf("零值阶段应原样保留, got %v", display.TotalStart)
}
if !display.Maximum.After(real) {
t.Fatalf("UT1 时刻应领先民用时刻: %v vs %v", display.Maximum, real)
}
if utc := lunarEclipseDisplayInfo(info, astro.TimeScaleUTC); !utc.Maximum.Equal(real) {
t.Fatalf("UTC 口径不应换算: %v vs %v", utc.Maximum, real)
}
}
func TestLunarEclipseChartTimeScaleGuard(t *testing.T) {
date := time.Date(2025, time.September, 7, 12, 0, 0, 0, time.UTC)
cst := time.FixedZone("CST", 8*3600)
renderers := map[string]func(time.Time, *time.Location) (string, bool){
"diagram": func(d time.Time, loc *time.Location) (string, bool) {
return LunarEclipseSVG(d, LunarEclipseSVGOptions{TimeScale: astro.TimeScaleUT1, Location: loc})
},
"detailed": func(d time.Time, loc *time.Location) (string, bool) {
return LunarEclipseDetailedSVG(d, LunarEclipseDetailedSVGOptions{TimeScale: astro.TimeScaleUT1, Location: loc})
},
"map": func(d time.Time, loc *time.Location) (string, bool) {
return LunarEclipseMapSVG(d, LunarEclipseMapSVGOptions{TimeScale: astro.TimeScaleUT1, Location: loc})
},
}
for name, render := range renderers {
if _, ok := render(date, cst); ok {
t.Errorf("%s: UT1 配非 UTC 时区应返回 false", name)
}
if _, ok := render(date, time.UTC); !ok {
t.Errorf("%s: UT1 配 UTC 时区应成功", name)
}
if _, ok := render(date, nil); !ok {
t.Errorf("%s: UT1 下 nil Location 应被强制为 UTC 并成功", name)
}
}
}
// 日食两族:UT1 选项必须成功、在图注里声明尺度,并且拒绝非 UTC 时区。
func TestSolarEclipseChartTimeScaleOption(t *testing.T) {
date := time.Date(2024, time.April, 8, 12, 0, 0, 0, time.UTC)
cst := time.FixedZone("CST", 8*3600)
mapSVG, ok := SolarEclipseMapSVG(date, SolarEclipseMapSVGOptions{TimeScale: astro.TimeScaleUT1})
if !ok {
t.Fatal("日食地图 UT1 渲染应成功")
}
assertUT1Declaration(t, "日食地图", mapSVG)
if _, ok := SolarEclipseMapSVG(date, SolarEclipseMapSVGOptions{TimeScale: astro.TimeScaleUT1, Location: cst}); ok {
t.Error("日食地图 UT1 配非 UTC 时区应返回 false")
}
localSVG, ok := LocalSolarEclipseSVG(date, -96.8, 32.8, 0, LocalSolarEclipseSVGOptions{TimeScale: astro.TimeScaleUT1})
if !ok {
t.Fatal("站心日食 UT1 渲染应成功")
}
assertUT1Declaration(t, "站心日食图", localSVG)
if _, ok := LocalSolarEclipseSVG(date, -96.8, 32.8, 0, LocalSolarEclipseSVGOptions{TimeScale: astro.TimeScaleUT1, Location: cst}); ok {
t.Error("站心日食 UT1 配非 UTC 时区应返回 false")
}
}
// 月食组合图与详细版的图注也要声明尺度。
func TestLunarEclipseChartsDeclareUT1(t *testing.T) {
date := time.Date(2025, time.September, 7, 12, 0, 0, 0, time.UTC)
for name, svg := range map[string]string{
"diagram": func() string {
text, _ := LunarEclipseSVG(date, LunarEclipseSVGOptions{TimeScale: astro.TimeScaleUT1})
return text
}(),
"detailed": func() string {
text, _ := LunarEclipseDetailedSVG(date, LunarEclipseDetailedSVGOptions{TimeScale: astro.TimeScaleUT1})
return text
}(),
} {
if svg == "" {
t.Fatalf("%s: UT1 渲染应成功", name)
}
assertUT1Declaration(t, name, svg)
}
}
// UT1 输出只允许改文字:把 元素整体去掉后,UTC 与 UT1 的同名图必须逐字节相同。
// 几何一旦用了 UT1 读数(把 UT1 当民用时刻),地平线、月下点与地心坐标块都会平移。
func TestLunarEclipseUT1ChartsKeepGeometry(t *testing.T) {
date := time.Date(2011, 6, 15, 12, 0, 0, 0, time.UTC)
renderers := map[string]func(time.Time, astro.TimeScale) (string, bool){
"map": func(at time.Time, scale astro.TimeScale) (string, bool) {
return LunarEclipseMapSVG(at, LunarEclipseMapSVGOptions{
Width: 960, Height: 640, Location: time.UTC, TimeScale: scale})
},
"detailed": func(at time.Time, scale astro.TimeScale) (string, bool) {
return LunarEclipseDetailedSVG(at, LunarEclipseDetailedSVGOptions{
Width: 1000, Height: 1414, Location: time.UTC, TimeScale: scale})
},
"diagram": func(at time.Time, scale astro.TimeScale) (string, bool) {
return LunarEclipseSVG(at, LunarEclipseSVGOptions{
Width: 920, Height: 720, Location: time.UTC, TimeScale: scale})
},
}
textElements := regexp.MustCompile(`(?s)]*>.*?`)
for name, render := range renderers {
utc, ok := render(date, astro.TimeScaleUTC)
if !ok {
t.Fatalf("%s: UTC render failed", name)
}
ut1, ok := render(date, astro.TimeScaleUT1)
if !ok {
t.Fatalf("%s: UT1 render failed", name)
}
if strings.Contains(ut1, "UT1") == false {
t.Errorf("%s: UT1 chart does not declare the scale", name)
}
utcGeometry := textElements.ReplaceAllString(utc, "")
ut1Geometry := textElements.ReplaceAllString(ut1, "")
if utcGeometry != ut1Geometry {
t.Errorf("%s: switching to UT1 changed chart geometry", name)
}
}
}