16c62a97d5
- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
199 lines
14 KiB
Go
199 lines
14 KiB
Go
package astro_test
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"b612.me/astro/jupiter"
|
|
litemoon "b612.me/astro/lite/moon"
|
|
litesun "b612.me/astro/lite/sun"
|
|
"b612.me/astro/mars"
|
|
"b612.me/astro/mercury"
|
|
"b612.me/astro/moon"
|
|
"b612.me/astro/neptune"
|
|
"b612.me/astro/orbit"
|
|
"b612.me/astro/saturn"
|
|
"b612.me/astro/star"
|
|
"b612.me/astro/sun"
|
|
"b612.me/astro/uranus"
|
|
"b612.me/astro/venus"
|
|
)
|
|
|
|
type civilEventCase struct {
|
|
name string
|
|
event func(time.Time) (time.Time, error)
|
|
altitude func(time.Time) float64
|
|
}
|
|
|
|
func civilEventCases(lon, lat float64) []civilEventCase {
|
|
elements := orbit.Elements{EpochJD: 2461000.5, A: 2.765615651508659, E: 0.07957631994408416,
|
|
I: 10.58788658206854, Omega: 80.24963090816965, W: 73.29975464616518, M0: 231.5397330043706}
|
|
return []civilEventCase{
|
|
{"sun/RiseTime", func(d time.Time) (time.Time, error) { return sun.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return sun.Altitude(d, lon, lat) }},
|
|
{"sun/RiseTimeN", func(d time.Time) (time.Time, error) { return sun.RiseTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return sun.AltitudeN(d, lon, lat, -1) }},
|
|
{"sun/SetTime", func(d time.Time) (time.Time, error) { return sun.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return sun.Altitude(d, lon, lat) }},
|
|
{"sun/SetTimeN", func(d time.Time) (time.Time, error) { return sun.SetTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return sun.AltitudeN(d, lon, lat, -1) }},
|
|
{"sun/CulminationTime", func(d time.Time) (time.Time, error) { return sun.CulminationTime(d, lon), nil }, nil},
|
|
{"sun/CulminationTimeN", func(d time.Time) (time.Time, error) { return sun.CulminationTimeN(d, lon, -1), nil }, nil},
|
|
{"moon/RiseTime", func(d time.Time) (time.Time, error) { return moon.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return moon.Altitude(d, lon, lat) }},
|
|
{"moon/SetTime", func(d time.Time) (time.Time, error) { return moon.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return moon.Altitude(d, lon, lat) }},
|
|
{"moon/CulminationTime", func(d time.Time) (time.Time, error) { return moon.CulminationTime(d, lon, lat), nil }, nil},
|
|
{"mercury/RiseTime", func(d time.Time) (time.Time, error) { return mercury.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return mercury.Altitude(d, lon, lat) }},
|
|
{"mercury/RiseTimeN", func(d time.Time) (time.Time, error) { return mercury.RiseTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return mercury.AltitudeN(d, lon, lat, -1) }},
|
|
{"mercury/SetTime", func(d time.Time) (time.Time, error) { return mercury.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return mercury.Altitude(d, lon, lat) }},
|
|
{"mercury/SetTimeN", func(d time.Time) (time.Time, error) { return mercury.SetTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return mercury.AltitudeN(d, lon, lat, -1) }},
|
|
{"mercury/CulminationTime", func(d time.Time) (time.Time, error) { return mercury.CulminationTime(d, lon), nil }, nil},
|
|
{"mercury/CulminationTimeN", func(d time.Time) (time.Time, error) { return mercury.CulminationTimeN(d, lon, -1), nil }, nil},
|
|
{"venus/RiseTime", func(d time.Time) (time.Time, error) { return venus.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return venus.Altitude(d, lon, lat) }},
|
|
{"venus/RiseTimeN", func(d time.Time) (time.Time, error) { return venus.RiseTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return venus.AltitudeN(d, lon, lat, -1) }},
|
|
{"venus/SetTime", func(d time.Time) (time.Time, error) { return venus.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return venus.Altitude(d, lon, lat) }},
|
|
{"venus/SetTimeN", func(d time.Time) (time.Time, error) { return venus.SetTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return venus.AltitudeN(d, lon, lat, -1) }},
|
|
{"venus/CulminationTime", func(d time.Time) (time.Time, error) { return venus.CulminationTime(d, lon), nil }, nil},
|
|
{"venus/CulminationTimeN", func(d time.Time) (time.Time, error) { return venus.CulminationTimeN(d, lon, -1), nil }, nil},
|
|
{"mars/RiseTime", func(d time.Time) (time.Time, error) { return mars.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return mars.Altitude(d, lon, lat) }},
|
|
{"mars/RiseTimeN", func(d time.Time) (time.Time, error) { return mars.RiseTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return mars.AltitudeN(d, lon, lat, -1) }},
|
|
{"mars/SetTime", func(d time.Time) (time.Time, error) { return mars.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return mars.Altitude(d, lon, lat) }},
|
|
{"mars/SetTimeN", func(d time.Time) (time.Time, error) { return mars.SetTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return mars.AltitudeN(d, lon, lat, -1) }},
|
|
{"mars/CulminationTime", func(d time.Time) (time.Time, error) { return mars.CulminationTime(d, lon), nil }, nil},
|
|
{"mars/CulminationTimeN", func(d time.Time) (time.Time, error) { return mars.CulminationTimeN(d, lon, -1), nil }, nil},
|
|
{"jupiter/RiseTime", func(d time.Time) (time.Time, error) { return jupiter.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return jupiter.Altitude(d, lon, lat) }},
|
|
{"jupiter/RiseTimeN", func(d time.Time) (time.Time, error) { return jupiter.RiseTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return jupiter.AltitudeN(d, lon, lat, -1) }},
|
|
{"jupiter/SetTime", func(d time.Time) (time.Time, error) { return jupiter.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return jupiter.Altitude(d, lon, lat) }},
|
|
{"jupiter/SetTimeN", func(d time.Time) (time.Time, error) { return jupiter.SetTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return jupiter.AltitudeN(d, lon, lat, -1) }},
|
|
{"jupiter/CulminationTime", func(d time.Time) (time.Time, error) { return jupiter.CulminationTime(d, lon), nil }, nil},
|
|
{"jupiter/CulminationTimeN", func(d time.Time) (time.Time, error) { return jupiter.CulminationTimeN(d, lon, -1), nil }, nil},
|
|
{"saturn/RiseTime", func(d time.Time) (time.Time, error) { return saturn.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return saturn.Altitude(d, lon, lat) }},
|
|
{"saturn/RiseTimeN", func(d time.Time) (time.Time, error) { return saturn.RiseTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return saturn.AltitudeN(d, lon, lat, -1) }},
|
|
{"saturn/SetTime", func(d time.Time) (time.Time, error) { return saturn.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return saturn.Altitude(d, lon, lat) }},
|
|
{"saturn/SetTimeN", func(d time.Time) (time.Time, error) { return saturn.SetTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return saturn.AltitudeN(d, lon, lat, -1) }},
|
|
{"saturn/CulminationTime", func(d time.Time) (time.Time, error) { return saturn.CulminationTime(d, lon), nil }, nil},
|
|
{"saturn/CulminationTimeN", func(d time.Time) (time.Time, error) { return saturn.CulminationTimeN(d, lon, -1), nil }, nil},
|
|
{"uranus/RiseTime", func(d time.Time) (time.Time, error) { return uranus.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return uranus.Altitude(d, lon, lat) }},
|
|
{"uranus/RiseTimeN", func(d time.Time) (time.Time, error) { return uranus.RiseTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return uranus.AltitudeN(d, lon, lat, -1) }},
|
|
{"uranus/SetTime", func(d time.Time) (time.Time, error) { return uranus.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return uranus.Altitude(d, lon, lat) }},
|
|
{"uranus/SetTimeN", func(d time.Time) (time.Time, error) { return uranus.SetTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return uranus.AltitudeN(d, lon, lat, -1) }},
|
|
{"uranus/CulminationTime", func(d time.Time) (time.Time, error) { return uranus.CulminationTime(d, lon), nil }, nil},
|
|
{"uranus/CulminationTimeN", func(d time.Time) (time.Time, error) { return uranus.CulminationTimeN(d, lon, -1), nil }, nil},
|
|
{"neptune/RiseTime", func(d time.Time) (time.Time, error) { return neptune.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return neptune.Altitude(d, lon, lat) }},
|
|
{"neptune/RiseTimeN", func(d time.Time) (time.Time, error) { return neptune.RiseTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return neptune.AltitudeN(d, lon, lat, -1) }},
|
|
{"neptune/SetTime", func(d time.Time) (time.Time, error) { return neptune.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return neptune.Altitude(d, lon, lat) }},
|
|
{"neptune/SetTimeN", func(d time.Time) (time.Time, error) { return neptune.SetTimeN(d, lon, lat, 0, false, -1) }, func(d time.Time) float64 { return neptune.AltitudeN(d, lon, lat, -1) }},
|
|
{"neptune/CulminationTime", func(d time.Time) (time.Time, error) { return neptune.CulminationTime(d, lon), nil }, nil},
|
|
{"neptune/CulminationTimeN", func(d time.Time) (time.Time, error) { return neptune.CulminationTimeN(d, lon, -1), nil }, nil},
|
|
{"litesun/RiseTime", func(d time.Time) (time.Time, error) { return litesun.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return litesun.Altitude(d, lon, lat) }},
|
|
{"litesun/SetTime", func(d time.Time) (time.Time, error) { return litesun.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return litesun.Altitude(d, lon, lat) }},
|
|
{"litemoon/RiseTime", func(d time.Time) (time.Time, error) { return litemoon.RiseTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return litemoon.Altitude(d, lon, lat) }},
|
|
{"litemoon/SetTime", func(d time.Time) (time.Time, error) { return litemoon.SetTime(d, lon, lat, 0, false) }, func(d time.Time) float64 { return litemoon.Altitude(d, lon, lat) }},
|
|
{"star/RiseTime", func(d time.Time) (time.Time, error) { return star.RiseTime(d, 100, -10, lon, lat, 0, false) }, func(d time.Time) float64 { return star.Altitude(d, 100, -10, lon, lat) }},
|
|
{"orbit/RiseTime", func(d time.Time) (time.Time, error) { return orbit.RiseTime(d, elements, lon, lat, 0, false) }, func(d time.Time) float64 { return orbit.Altitude(d, elements, lon, lat, 0) }},
|
|
{"star/SetTime", func(d time.Time) (time.Time, error) { return star.SetTime(d, 100, -10, lon, lat, 0, false) }, func(d time.Time) float64 { return star.Altitude(d, 100, -10, lon, lat) }},
|
|
{"orbit/SetTime", func(d time.Time) (time.Time, error) { return orbit.SetTime(d, elements, lon, lat, 0, false) }, func(d time.Time) float64 { return orbit.Altitude(d, elements, lon, lat, 0) }},
|
|
{"star/CulminationTime", func(d time.Time) (time.Time, error) { return star.CulminationTime(d, 100, lon), nil }, nil},
|
|
{"orbit/CulminationTime", func(d time.Time) (time.Time, error) { return orbit.CulminationTime(d, elements, lon, lat, 0), nil }, nil},
|
|
{"sun/MorningTwilight", func(d time.Time) (time.Time, error) { return sun.MorningTwilight(d, lon, lat, -6) }, func(d time.Time) float64 { return sun.Altitude(d, lon, lat) + 6 }},
|
|
{"sun/MorningTwilightN", func(d time.Time) (time.Time, error) { return sun.MorningTwilightN(d, lon, lat, -6, -1) }, func(d time.Time) float64 { return sun.AltitudeN(d, lon, lat, -1) + 6 }},
|
|
{"sun/EveningTwilight", func(d time.Time) (time.Time, error) { return sun.EveningTwilight(d, lon, lat, -6) }, func(d time.Time) float64 { return sun.Altitude(d, lon, lat) + 6 }},
|
|
{"sun/EveningTwilightN", func(d time.Time) (time.Time, error) { return sun.EveningTwilightN(d, lon, lat, -6, -1) }, func(d time.Time) float64 { return sun.AltitudeN(d, lon, lat, -1) + 6 }},
|
|
}
|
|
}
|
|
|
|
func TestCivilEventsIgnoreTimeOfDay(t *testing.T) {
|
|
loc := time.FixedZone("CST", 8*3600)
|
|
base := time.Date(2026, 2, 17, 0, 0, 0, 0, loc)
|
|
for _, tc := range civilEventCases(108.93, 34.27) {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
want, err := tc.event(base)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, clock := range [][3]int{{11, 59, 59}, {12, 0, 0}, {12, 30, 0}, {12, 59, 59}, {13, 0, 0}, {23, 59, 59}} {
|
|
input := time.Date(2026, 2, 17, clock[0], clock[1], clock[2], 0, loc)
|
|
got, err := tc.event(input)
|
|
if err != nil || !got.Equal(want) || got.Location() != loc {
|
|
t.Errorf("input %s: got %s, %v; want %s", input, got, err, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCivilEventsAcrossDST(t *testing.T) {
|
|
loc, err := time.LoadLocation("America/New_York")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, md := range [][2]int{{3, 8}, {11, 1}} {
|
|
base := time.Date(2026, time.Month(md[0]), md[1], 0, 0, 0, 0, loc)
|
|
for _, tc := range civilEventCases(-74.006, 40.7128) {
|
|
t.Run(base.Format("2006-01-02")+"/"+tc.name, func(t *testing.T) {
|
|
want, wantErr := tc.event(base)
|
|
if wantErr == nil && want.Location() != loc {
|
|
t.Fatalf("lost location: %s", want)
|
|
}
|
|
if wantErr == nil && tc.altitude != nil {
|
|
if residual := tc.altitude(want); math.IsNaN(residual) || math.Abs(residual) > 0.03 {
|
|
t.Errorf("event %s residual altitude %.6f degrees", want, residual)
|
|
}
|
|
}
|
|
for _, hour := range []int{1, 3, 10, 12, 13, 23} {
|
|
input := time.Date(base.Year(), base.Month(), base.Day(), hour, 30, 0, 0, loc)
|
|
got, err := tc.event(input)
|
|
if err != wantErr || !got.Equal(want) {
|
|
t.Errorf("input %s: got %s, %v; want %s, %v", input, got, err, want, wantErr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSunriseDSTMatchesFixedOffsetInstant(t *testing.T) {
|
|
loc, err := time.LoadLocation("America/New_York")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, md := range [][2]int{{3, 8}, {11, 1}} {
|
|
date := time.Date(2026, time.Month(md[0]), md[1], 10, 0, 0, 0, loc)
|
|
got, err := sun.RiseTime(date, -74.006, 40.7128, 0, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, offset := got.Zone()
|
|
fixed := time.FixedZone("fixed", offset)
|
|
want, err := sun.RiseTime(time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, fixed), -74.006, 40.7128, 0, true)
|
|
if err != nil || math.Abs(got.Sub(want).Seconds()) > 0.001 {
|
|
t.Errorf("%s: got %s, want %s, err=%v", date, got, want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMoonriseInLastHourOfLongCivilDay(t *testing.T) {
|
|
loc, err := time.LoadLocation("America/New_York")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
date := time.Date(2026, 11, 1, 12, 30, 0, 0, loc)
|
|
for _, tc := range []struct {
|
|
name string
|
|
rise func(time.Time, float64, float64, float64, bool) (time.Time, error)
|
|
}{
|
|
{"moon", moon.RiseTime}, {"lite/moon", litemoon.RiseTime},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := tc.rise(date, -74.006, 40.7128, 0, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Day() != 1 || got.Hour() != 23 || got.Location() != loc {
|
|
t.Fatalf("expected late local moonrise, got %s", got)
|
|
}
|
|
fixed := time.FixedZone("EST", -5*3600)
|
|
want, err := tc.rise(time.Date(2026, 11, 1, 0, 0, 0, 0, fixed), -74.006, 40.7128, 0, false)
|
|
if err != nil || !got.Equal(want) {
|
|
t.Fatalf("got %s, want %s, err=%v", got, want, err)
|
|
}
|
|
})
|
|
}
|
|
}
|