- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息 - 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算 - 新增木星伽利略卫星位置、现象与接触事件计算 - 新增恒星星表、星座判定、自行修正与观测辅助能力 - 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包 - 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算 - 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试 - 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试 - 更新中文和英文 README,补充示例、精度说明、SVG 配图
137 lines
4.7 KiB
Go
137 lines
4.7 KiB
Go
package jupiter
|
|
|
|
import (
|
|
"encoding/json"
|
|
"math"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"b612.me/astro/basic"
|
|
)
|
|
|
|
type galileanPublicEventBaselineRecord struct {
|
|
Label string `json:"label"`
|
|
Satellite int `json:"satellite"`
|
|
Type string `json:"type"`
|
|
StartUTC string `json:"start_utc"`
|
|
StartDurationMinutes float64 `json:"start_duration_minutes"`
|
|
EndUTC string `json:"end_utc"`
|
|
EndDurationMinutes float64 `json:"end_duration_minutes"`
|
|
}
|
|
|
|
func TestGalileanPhenomenonEventWrappersMatchBasic(t *testing.T) {
|
|
records := loadGalileanPublicEventBaseline(t)
|
|
loc := time.FixedZone("UTC+8", 8*3600)
|
|
for _, record := range records {
|
|
startUTC := mustParseGalileanEventTime(t, record.StartUTC)
|
|
endUTC := mustParseGalileanEventTime(t, record.EndUTC)
|
|
queryBefore := startUTC.Add(-12 * time.Hour).In(loc)
|
|
queryAfter := endUTC.Add(12 * time.Hour).In(loc)
|
|
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2).In(loc)
|
|
phenomenonType := GalileanPhenomenonType(record.Type)
|
|
|
|
assertGalileanWrapperMatchesBasic(
|
|
t,
|
|
record.Label+" next",
|
|
NextGalileanPhenomenonEvent(queryBefore, record.Satellite, phenomenonType),
|
|
basic.NextJupiterGalileanPhenomenonEvent(basic.Date2JDE(queryBefore.UTC()), record.Satellite, basic.JupiterGalileanPhenomenonType(phenomenonType)),
|
|
loc,
|
|
)
|
|
assertGalileanWrapperMatchesBasic(
|
|
t,
|
|
record.Label+" last",
|
|
LastGalileanPhenomenonEvent(queryAfter, record.Satellite, phenomenonType),
|
|
basic.LastJupiterGalileanPhenomenonEvent(basic.Date2JDE(queryAfter.UTC()), record.Satellite, basic.JupiterGalileanPhenomenonType(phenomenonType)),
|
|
loc,
|
|
)
|
|
assertGalileanWrapperMatchesBasic(
|
|
t,
|
|
record.Label+" closest",
|
|
ClosestGalileanPhenomenonEvent(queryMid, record.Satellite, phenomenonType),
|
|
basic.ClosestJupiterGalileanPhenomenonEvent(basic.Date2JDE(queryMid.UTC()), record.Satellite, basic.JupiterGalileanPhenomenonType(phenomenonType)),
|
|
loc,
|
|
)
|
|
}
|
|
}
|
|
|
|
func assertGalileanWrapperMatchesBasic(
|
|
t *testing.T,
|
|
name string,
|
|
got GalileanPhenomenonEvent,
|
|
want basic.JupiterGalileanPhenomenonEvent,
|
|
loc *time.Location,
|
|
) {
|
|
t.Helper()
|
|
if got.Valid != want.Valid {
|
|
t.Fatalf("%s valid mismatch: got %v want %v", name, got.Valid, want.Valid)
|
|
}
|
|
if !got.Valid {
|
|
return
|
|
}
|
|
if got.Start.Location() != loc || got.Greatest.Location() != loc || got.End.Location() != loc {
|
|
t.Fatalf("%s timezone mismatch", name)
|
|
}
|
|
wantStart := basic.JDE2DateByZone(want.Start, loc, false)
|
|
wantGreatest := basic.JDE2DateByZone(want.Greatest, loc, false)
|
|
wantEnd := basic.JDE2DateByZone(want.End, loc, false)
|
|
if !got.Start.Equal(wantStart) || !got.Greatest.Equal(wantGreatest) || !got.End.Equal(wantEnd) {
|
|
t.Fatalf(
|
|
"%s time mismatch: got [%s %s %s] want [%s %s %s]",
|
|
name,
|
|
got.Start.Format(time.RFC3339Nano),
|
|
got.Greatest.Format(time.RFC3339Nano),
|
|
got.End.Format(time.RFC3339Nano),
|
|
wantStart.Format(time.RFC3339Nano),
|
|
wantGreatest.Format(time.RFC3339Nano),
|
|
wantEnd.Format(time.RFC3339Nano),
|
|
)
|
|
}
|
|
if got.Duration != got.End.Sub(got.Start) {
|
|
t.Fatalf("%s duration mismatch: got %s want %s", name, got.Duration, got.End.Sub(got.Start))
|
|
}
|
|
if got.Satellite != want.Satellite || string(got.Type) != string(want.Type) {
|
|
t.Fatalf("%s id/type mismatch", name)
|
|
}
|
|
assertSameGalileanPhenomenon(t, name+" greatest", got.GreatestState, want.GreatestPhenomenon)
|
|
}
|
|
|
|
func assertSameGalileanPhenomenon(t *testing.T, name string, got GalileanSatellitePhenomenon, want basic.JupiterGalileanPhenomenon) {
|
|
t.Helper()
|
|
if got.Transit != want.Transit || got.Occultation != want.Occultation || got.Eclipse != want.Eclipse || got.ShadowTransit != want.ShadowTransit {
|
|
t.Fatalf("%s flag mismatch", name)
|
|
}
|
|
gotFloats := []float64{got.ShadowOffsetXArcsec, got.ShadowOffsetYArcsec, got.ShadowOffsetXJupiterR, got.ShadowOffsetYJupiterR}
|
|
wantFloats := []float64{want.ShadowOffsetXArcsec, want.ShadowOffsetYArcsec, want.ShadowOffsetXJupiterRadii, want.ShadowOffsetYJupiterRadii}
|
|
for i := range gotFloats {
|
|
if math.Float64bits(gotFloats[i]) != math.Float64bits(wantFloats[i]) {
|
|
t.Fatalf("%s shadow field %d mismatch: got %.18f want %.18f", name, i, gotFloats[i], wantFloats[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadGalileanPublicEventBaseline(t *testing.T) []galileanPublicEventBaselineRecord {
|
|
t.Helper()
|
|
data, err := os.ReadFile("testdata/galilean_events_imcce_2026.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var records []galileanPublicEventBaselineRecord
|
|
if err := json.Unmarshal(data, &records); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(records) == 0 {
|
|
t.Fatal("empty galilean public event baseline")
|
|
}
|
|
return records
|
|
}
|
|
|
|
func mustParseGalileanEventTime(t *testing.T, value string) time.Time {
|
|
t.Helper()
|
|
date, err := time.Parse(time.RFC3339Nano, value)
|
|
if err != nil {
|
|
t.Fatalf("parse %q: %v", value, err)
|
|
}
|
|
return date
|
|
}
|