package astro_test import ( "math" "testing" "time" "b612.me/astro/basic" "b612.me/astro/calendar" "b612.me/astro/jupiter" "b612.me/astro/mars" "b612.me/astro/mercury" "b612.me/astro/moon" "b612.me/astro/neptune" "b612.me/astro/saturn" "b612.me/astro/star" "b612.me/astro/sun" "b612.me/astro/uranus" "b612.me/astro/venus" ) func nearlyEqual(a, b float64) bool { return math.Abs(a-b) <= 1e-12 } func TestPlanetAbsoluteQuantitiesIgnoreInputTimezone(t *testing.T) { utc := time.Date(2026, 1, 2, 3, 4, 5, 123456789, time.UTC) cst := time.FixedZone("CST", 8*3600) local := utc.In(cst) scalars := []struct { name string fn func(time.Time) float64 }{ {"mercury.ApparentLo", mercury.ApparentLo}, {"mercury.ApparentBo", mercury.ApparentBo}, {"mercury.ApparentRa", mercury.ApparentRa}, {"mercury.ApparentDec", mercury.ApparentDec}, {"mercury.ApparentMagnitude", mercury.ApparentMagnitude}, {"mercury.EarthDistance", mercury.EarthDistance}, {"mercury.SunDistance", mercury.SunDistance}, {"venus.ApparentLo", venus.ApparentLo}, {"venus.ApparentBo", venus.ApparentBo}, {"venus.ApparentRa", venus.ApparentRa}, {"venus.ApparentDec", venus.ApparentDec}, {"venus.ApparentMagnitude", venus.ApparentMagnitude}, {"venus.EarthDistance", venus.EarthDistance}, {"venus.SunDistance", venus.SunDistance}, {"mars.ApparentLo", mars.ApparentLo}, {"mars.ApparentBo", mars.ApparentBo}, {"mars.ApparentRa", mars.ApparentRa}, {"mars.ApparentDec", mars.ApparentDec}, {"mars.ApparentMagnitude", mars.ApparentMagnitude}, {"mars.EarthDistance", mars.EarthDistance}, {"mars.SunDistance", mars.SunDistance}, {"jupiter.ApparentLo", jupiter.ApparentLo}, {"jupiter.ApparentBo", jupiter.ApparentBo}, {"jupiter.ApparentRa", jupiter.ApparentRa}, {"jupiter.ApparentDec", jupiter.ApparentDec}, {"jupiter.ApparentMagnitude", jupiter.ApparentMagnitude}, {"jupiter.EarthDistance", jupiter.EarthDistance}, {"jupiter.SunDistance", jupiter.SunDistance}, {"saturn.ApparentLo", saturn.ApparentLo}, {"saturn.ApparentBo", saturn.ApparentBo}, {"saturn.ApparentRa", saturn.ApparentRa}, {"saturn.ApparentDec", saturn.ApparentDec}, {"saturn.ApparentMagnitude", saturn.ApparentMagnitude}, {"saturn.EarthDistance", saturn.EarthDistance}, {"saturn.SunDistance", saturn.SunDistance}, {"uranus.ApparentLo", uranus.ApparentLo}, {"uranus.ApparentBo", uranus.ApparentBo}, {"uranus.ApparentRa", uranus.ApparentRa}, {"uranus.ApparentDec", uranus.ApparentDec}, {"uranus.ApparentMagnitude", uranus.ApparentMagnitude}, {"uranus.EarthDistance", uranus.EarthDistance}, {"uranus.SunDistance", uranus.SunDistance}, {"neptune.ApparentLo", neptune.ApparentLo}, {"neptune.ApparentBo", neptune.ApparentBo}, {"neptune.ApparentRa", neptune.ApparentRa}, {"neptune.ApparentDec", neptune.ApparentDec}, {"neptune.ApparentMagnitude", neptune.ApparentMagnitude}, {"neptune.EarthDistance", neptune.EarthDistance}, {"neptune.SunDistance", neptune.SunDistance}, } for _, tc := range scalars { if !nearlyEqual(tc.fn(utc), tc.fn(local)) { t.Fatalf("%s should depend on absolute time only", tc.name) } } pairs := []struct { name string fn func(time.Time) (float64, float64) }{ {"mercury.ApparentRaDec", mercury.ApparentRaDec}, {"venus.ApparentRaDec", venus.ApparentRaDec}, {"mars.ApparentRaDec", mars.ApparentRaDec}, {"jupiter.ApparentRaDec", jupiter.ApparentRaDec}, {"saturn.ApparentRaDec", saturn.ApparentRaDec}, {"uranus.ApparentRaDec", uranus.ApparentRaDec}, {"neptune.ApparentRaDec", neptune.ApparentRaDec}, } for _, tc := range pairs { leftA, leftB := tc.fn(utc) rightA, rightB := tc.fn(local) if !nearlyEqual(leftA, rightA) || !nearlyEqual(leftB, rightB) { t.Fatalf("%s should depend on absolute time only", tc.name) } } } func TestJDECalcRejectsGregorianGap(t *testing.T) { cases := []float64{5, 6.5, 10, 14.25} for _, day := range cases { got := basic.JDECalc(1582, 10, day) if !math.IsNaN(got) { t.Fatalf("1582-10-%v should be rejected, got %.15f", day, got) } } before := basic.JDECalc(1582, 10, 4) after := basic.JDECalc(1582, 10, 15) if math.IsNaN(before) || math.IsNaN(after) { t.Fatal("boundary dates around Gregorian reform should remain valid") } if !nearlyEqual(after-before, 1) { t.Fatalf("1582-10-15 should remain the civil day after 1582-10-04") } } func TestCalendarAddPreservesOriginalTimezone(t *testing.T) { oldLocal := time.Local time.Local = time.UTC defer func() { time.Local = oldLocal }() tz := time.FixedZone("CST", 8*3600) start := time.Date(1985, 1, 21, 9, 30, 0, 0, tz) lunar, err := calendar.SolarToLunar(start) if err != nil { t.Fatal(err) } expected, err := calendar.SolarToLunar(lunar.Time().Add(36 * time.Hour)) if err != nil { t.Fatal(err) } shifted := lunar.Add(36 * time.Hour).Time() if delta := shifted.Sub(expected.Time()); delta < -time.Millisecond || delta > time.Millisecond { t.Fatalf("calendar.Time.Add should not depend on time.Local: got %v want %v", shifted, expected.Time()) } } func TestObservationZenithSemantics(t *testing.T) { date := time.Date(2026, 4, 26, 9, 30, 45, 123456789, time.FixedZone("CST", 8*3600)) lon := 116.391 lat := 39.907 ra := 6.752477 dec := -16.716116 checks := []struct { name string altitude func() float64 zenith func() float64 }{ {"sun", func() float64 { return sun.Altitude(date, lon, lat) }, func() float64 { return sun.Zenith(date, lon, lat) }}, {"moon", func() float64 { return moon.Altitude(date, lon, lat) }, func() float64 { return moon.Zenith(date, lon, lat) }}, {"star", func() float64 { return star.Altitude(date, ra, dec, lon, lat) }, func() float64 { return star.Zenith(date, ra, dec, lon, lat) }}, {"mercury", func() float64 { return mercury.Altitude(date, lon, lat) }, func() float64 { return mercury.Zenith(date, lon, lat) }}, {"venus", func() float64 { return venus.Altitude(date, lon, lat) }, func() float64 { return venus.Zenith(date, lon, lat) }}, {"mars", func() float64 { return mars.Altitude(date, lon, lat) }, func() float64 { return mars.Zenith(date, lon, lat) }}, {"jupiter", func() float64 { return jupiter.Altitude(date, lon, lat) }, func() float64 { return jupiter.Zenith(date, lon, lat) }}, {"saturn", func() float64 { return saturn.Altitude(date, lon, lat) }, func() float64 { return saturn.Zenith(date, lon, lat) }}, {"uranus", func() float64 { return uranus.Altitude(date, lon, lat) }, func() float64 { return uranus.Zenith(date, lon, lat) }}, {"neptune", func() float64 { return neptune.Altitude(date, lon, lat) }, func() float64 { return neptune.Zenith(date, lon, lat) }}, } for _, tc := range checks { if !nearlyEqual(tc.zenith(), 90-tc.altitude()) { t.Fatalf("%s zenith should equal 90-altitude", tc.name) } } }