fix: 修复天象事件 API 在事件边界附近的重复返回问题
- 修正合月、合日、留等Last/Next/Closest接口在精确命中和事件后秒级查询时仍返回当前事件的问题,避免应用侧枚举卡死 - 收紧事件边界判定,并补强水星、金星、火星近事件路径的稳定化,保证公开API的单调前进语义 - 补充公开wrapper、跨世纪样本和外部基线回归,覆盖合月邻近事件与边界场景
This commit is contained in:
parent
be3af3884c
commit
46b555cd49
@ -2,14 +2,17 @@ package basic
|
||||
|
||||
import "math"
|
||||
|
||||
const exactEventTolerance = 2.0 / 86400.0
|
||||
const (
|
||||
exactEventTolerance = 2.0 / 86400.0
|
||||
exactQueryTTToleranceUT = 0.1 / 86400.0
|
||||
)
|
||||
|
||||
func sameEventJD(a, b float64) bool {
|
||||
return math.Abs(a-b) <= exactEventTolerance
|
||||
}
|
||||
|
||||
func sameEventUTQueryTT(eventUT, queryTT float64) bool {
|
||||
return math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) <= exactEventTolerance
|
||||
return math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) <= exactQueryTTToleranceUT
|
||||
}
|
||||
|
||||
func closestEventUTToQueryTT(queryTT, best float64, candidates ...float64) float64 {
|
||||
|
||||
@ -2,7 +2,11 @@ package basic
|
||||
|
||||
import "math"
|
||||
|
||||
const innerEventEpsilon = 4.0 / 86400.0
|
||||
const (
|
||||
innerEventEpsilon = 0.1 / 86400.0
|
||||
innerEventWindowPadding = 4.0 / 86400.0
|
||||
innerEventMaximizeEpsilon = 4.0 / 86400.0
|
||||
)
|
||||
|
||||
func eventQueryTTAsUT(queryTT float64) float64 {
|
||||
return TD2UT(queryTT, false)
|
||||
@ -157,7 +161,7 @@ func maximizeInWindow(start, end, coarseStep float64, coarseFn, exactFn func(flo
|
||||
guess := scanWindowForMax(start, end, coarseStep, coarseFn)
|
||||
left := clampFloat64(guess-coarseStep, start, end)
|
||||
right := clampFloat64(guess+coarseStep, start, end)
|
||||
if right-left <= innerEventEpsilon {
|
||||
if right-left <= innerEventMaximizeEpsilon {
|
||||
return guess
|
||||
}
|
||||
for i := 0; i < 20; i++ {
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package basic
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestInnerPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
|
||||
cases := []struct {
|
||||
@ -43,3 +46,60 @@ func TestInnerPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInnerPlanetNextEventAdvancesPastReturnedEvent(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
seed float64
|
||||
next func(float64) float64
|
||||
}{
|
||||
{name: "MercuryConjunction", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryConjunction},
|
||||
{name: "MercuryInferior", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryInferiorConjunction},
|
||||
{name: "MercuryP2R", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryProgradeToRetrograde},
|
||||
{name: "MercuryEastElongation", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryGreatestElongationEast},
|
||||
{name: "VenusConjunction", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextVenusConjunction},
|
||||
{name: "VenusWestElongation", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextVenusGreatestElongationWest},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
first := tc.next(tc.seed)
|
||||
query := TD2UT(Date2JDE(JDE2DateByZone(first, time.UTC, false).Add(time.Second)), true)
|
||||
next := tc.next(query)
|
||||
if !eventUTQueryAfterOrEqual(next, query) {
|
||||
t.Fatalf("next should be after query: first=%.12f query=%.12f next=%.12f", first, query, next)
|
||||
}
|
||||
if sameEventJD(next, first) {
|
||||
t.Fatalf("next should advance past first event: first=%.12f next=%.12f", first, next)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInnerPlanetTypedConjunctionExactBoundaryIncludesCurrent(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
seed float64
|
||||
next func(float64) float64
|
||||
last func(float64) float64
|
||||
}{
|
||||
{name: "MercuryInferior", seed: NextMercuryInferiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextMercuryInferiorConjunction, last: LastMercuryInferiorConjunction},
|
||||
{name: "MercurySuperior", seed: NextMercurySuperiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextMercurySuperiorConjunction, last: LastMercurySuperiorConjunction},
|
||||
{name: "VenusInferior", seed: NextVenusInferiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextVenusInferiorConjunction, last: LastVenusInferiorConjunction},
|
||||
{name: "VenusSuperior", seed: NextVenusSuperiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextVenusSuperiorConjunction, last: LastVenusSuperiorConjunction},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
queryTT := TD2UT(tc.seed, true)
|
||||
last := tc.last(queryTT)
|
||||
next := tc.next(queryTT)
|
||||
if !sameEventJD(last, tc.seed) {
|
||||
t.Fatalf("last exact boundary mismatch: got %.12f want %.12f", last, tc.seed)
|
||||
}
|
||||
if !sameEventJD(next, tc.seed) {
|
||||
t.Fatalf("next exact boundary mismatch: got %.12f want %.12f", next, tc.seed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,12 +186,29 @@ func marsOppositionFromAfter(oppositionJD float64) float64 {
|
||||
return marsConjunctionFull(eventUTNextQueryTT(oppositionJD), 180, 0)
|
||||
}
|
||||
|
||||
func stabilizeMarsStationNearQuery(jde, date float64, searchBeforeOpposition bool) float64 {
|
||||
if math.Abs(eventUTQueryTTDelta(date, jde)) > exactEventTolerance {
|
||||
return date
|
||||
}
|
||||
if searchBeforeOpposition {
|
||||
stableOppositionJD := NextMarsOpposition(jde)
|
||||
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
|
||||
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(stableOppositionJD, true), marsRetrogradeAroundOpposition(sameOppositionJD, true))
|
||||
}
|
||||
stableOppositionJD := LastMarsOpposition(jde)
|
||||
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
|
||||
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(stableOppositionJD, false), marsRetrogradeAroundOpposition(sameOppositionJD, false))
|
||||
}
|
||||
|
||||
func NextMarsRetrogradeToPrograde(jde float64) float64 {
|
||||
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
|
||||
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
|
||||
date = stabilizeMarsStationNearQuery(jde, date, false)
|
||||
if sameEventUTQueryTT(date, jde) {
|
||||
sameOppositionJD := marsOppositionFromBefore(lastOppositionJD)
|
||||
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, false))
|
||||
stableOppositionJD := LastMarsOpposition(jde)
|
||||
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, false)
|
||||
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
|
||||
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, false))
|
||||
}
|
||||
if !eventUTQueryAfterOrEqual(date, jde) {
|
||||
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
|
||||
@ -203,9 +220,12 @@ func NextMarsRetrogradeToPrograde(jde float64) float64 {
|
||||
func LastMarsRetrogradeToPrograde(jde float64) float64 {
|
||||
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
|
||||
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
|
||||
date = stabilizeMarsStationNearQuery(jde, date, false)
|
||||
if sameEventUTQueryTT(date, jde) {
|
||||
sameOppositionJD := marsOppositionFromBefore(lastOppositionJD)
|
||||
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, false))
|
||||
stableOppositionJD := LastMarsOpposition(jde)
|
||||
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, false)
|
||||
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
|
||||
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, false))
|
||||
}
|
||||
if !eventUTQueryBeforeOrEqual(date, jde) {
|
||||
previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
|
||||
@ -217,9 +237,12 @@ func LastMarsRetrogradeToPrograde(jde float64) float64 {
|
||||
func NextMarsProgradeToRetrograde(jde float64) float64 {
|
||||
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
|
||||
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
|
||||
date = stabilizeMarsStationNearQuery(jde, date, true)
|
||||
if sameEventUTQueryTT(date, jde) {
|
||||
sameOppositionJD := marsOppositionFromAfter(nextOppositionJD)
|
||||
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, true))
|
||||
stableOppositionJD := NextMarsOpposition(jde)
|
||||
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, true)
|
||||
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
|
||||
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, true))
|
||||
}
|
||||
if !eventUTQueryAfterOrEqual(date, jde) {
|
||||
followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
|
||||
@ -231,9 +254,12 @@ func NextMarsProgradeToRetrograde(jde float64) float64 {
|
||||
func LastMarsProgradeToRetrograde(jde float64) float64 {
|
||||
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
|
||||
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
|
||||
date = stabilizeMarsStationNearQuery(jde, date, true)
|
||||
if sameEventUTQueryTT(date, jde) {
|
||||
sameOppositionJD := marsOppositionFromAfter(nextOppositionJD)
|
||||
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, true))
|
||||
stableOppositionJD := NextMarsOpposition(jde)
|
||||
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, true)
|
||||
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
|
||||
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, true))
|
||||
}
|
||||
if !eventUTQueryBeforeOrEqual(date, jde) {
|
||||
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
|
||||
|
||||
@ -168,6 +168,26 @@ func mercuryConjunctionLegacy(jde float64, next uint8) float64 {
|
||||
|
||||
func mercuryConjunction(jde float64, next uint8) float64 {
|
||||
//0=last 1=next
|
||||
if math.Abs(mercuryConjunctionExactDelta(jde)) <= 30.0/86400.0 {
|
||||
best := math.NaN()
|
||||
consider := func(inferior bool) {
|
||||
eventUT := TD2UT(mercuryConjunctionExactTT(jde, inferior), false)
|
||||
if next == 0 && !eventUTQueryBeforeOrEqual(eventUT, jde) {
|
||||
return
|
||||
}
|
||||
if next == 1 && !eventUTQueryAfterOrEqual(eventUT, jde) {
|
||||
return
|
||||
}
|
||||
if math.IsNaN(best) || math.Abs(eventUTQueryTTDelta(eventUT, jde)) < math.Abs(eventUTQueryTTDelta(best, jde)) {
|
||||
best = eventUT
|
||||
}
|
||||
}
|
||||
consider(true)
|
||||
consider(false)
|
||||
if !math.IsNaN(best) {
|
||||
return best
|
||||
}
|
||||
}
|
||||
currentDelta := mercuryConjunctionExactDelta(jde)
|
||||
// pos 大于0:远离太阳 小于0:靠近太阳
|
||||
distanceTrend := math.Abs(mercuryConjunctionExactDelta(jde+1/86400.0)) - math.Abs(currentDelta)
|
||||
@ -417,12 +437,12 @@ func mercuryGreatestElongationInWindow(start, end float64) float64 {
|
||||
|
||||
func mercuryEastElongationWindowEndingAt(inferior float64) (float64, float64) {
|
||||
lastSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(inferior))
|
||||
return lastSuperior + innerEventEpsilon, inferior - innerEventEpsilon
|
||||
return lastSuperior + innerEventWindowPadding, inferior - innerEventWindowPadding
|
||||
}
|
||||
|
||||
func mercuryWestElongationWindowEndingAt(superior float64) (float64, float64) {
|
||||
lastInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(superior))
|
||||
return lastInferior + innerEventEpsilon, superior - innerEventEpsilon
|
||||
return lastInferior + innerEventWindowPadding, superior - innerEventWindowPadding
|
||||
}
|
||||
|
||||
func mercuryEastElongationWindowContaining(jde float64) (float64, float64) {
|
||||
|
||||
@ -5,6 +5,7 @@ import "math"
|
||||
const (
|
||||
moonPlanetConjunctionEstimateN = 8
|
||||
moonPlanetConjunctionNearQueryDeltaDeg = 3.0
|
||||
moonPlanetConjunctionDirectionEpsilon = 0.1 / 86400.0
|
||||
moonPlanetConjunctionBracketStepDays = 0.5
|
||||
moonPlanetConjunctionNearQueryStepDays = 0.25
|
||||
moonPlanetConjunctionNearQueryHalfSpan = 1.5
|
||||
@ -94,12 +95,20 @@ func moonPlanetConjunctionPeriodDays(planet MoonPlanetConjunctionPlanet) float64
|
||||
}
|
||||
}
|
||||
|
||||
func moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT float64) bool {
|
||||
return eventUTQueryTTDelta(eventUT, queryTT) <= moonPlanetConjunctionDirectionEpsilon
|
||||
}
|
||||
|
||||
func moonPlanetConjunctionAfterOrEqual(eventUT, queryTT float64) bool {
|
||||
return eventUTQueryTTDelta(eventUT, queryTT) >= -moonPlanetConjunctionDirectionEpsilon
|
||||
}
|
||||
|
||||
func moonPlanetConjunctionInDirection(eventUT, queryTT float64, direction int) bool {
|
||||
switch direction {
|
||||
case -1:
|
||||
return eventUTQueryBeforeOrEqual(eventUT, queryTT)
|
||||
return moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT)
|
||||
case 1:
|
||||
return eventUTQueryAfterOrEqual(eventUT, queryTT)
|
||||
return moonPlanetConjunctionAfterOrEqual(eventUT, queryTT)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
@ -182,12 +191,12 @@ func moonPlanetConjunctionCollectLocalEvent(result *moonPlanetConjunctionLocalRe
|
||||
if math.IsNaN(eventUT) {
|
||||
return
|
||||
}
|
||||
if eventUTQueryBeforeOrEqual(eventUT, queryTT) {
|
||||
if moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT) {
|
||||
if math.IsNaN(result.lastUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.lastUT, queryTT)) {
|
||||
result.lastUT = eventUT
|
||||
}
|
||||
}
|
||||
if eventUTQueryAfterOrEqual(eventUT, queryTT) {
|
||||
if moonPlanetConjunctionAfterOrEqual(eventUT, queryTT) {
|
||||
if math.IsNaN(result.nextUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.nextUT, queryTT)) {
|
||||
result.nextUT = eventUT
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ func TestMoonPlanetConjunctionsMatchHorizonsBaseline(t *testing.T) {
|
||||
t.Logf("moon-planet conjunction max diff: time=%v", maxDiff)
|
||||
}
|
||||
|
||||
func TestMoonPlanetConjunctionDirectionalConsistencyAroundBaseline(t *testing.T) {
|
||||
func TestMoonPlanetConjunctionDirectionalConsistencyAtComputedEvent(t *testing.T) {
|
||||
baseline := loadMoonPlanetConjunctionBaseline(t)
|
||||
|
||||
planets := map[string]MoonPlanetConjunctionPlanet{
|
||||
@ -144,22 +144,24 @@ func TestMoonPlanetConjunctionDirectionalConsistencyAroundBaseline(t *testing.T)
|
||||
if err != nil {
|
||||
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
|
||||
}
|
||||
queryAtTT := TD2UT(Date2JDE(wantTime.UTC()), true)
|
||||
queryAfterTT := TD2UT(Date2JDE(wantTime.Add(time.Hour).UTC()), true)
|
||||
seedTT := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true)
|
||||
eventUT := NextMoonPlanetConjunction(seedTT, planet)
|
||||
eventTime := JDE2DateByZone(eventUT, time.UTC, false)
|
||||
queryAtTT := TD2UT(Date2JDE(eventTime.UTC()), true)
|
||||
queryAfterTT := TD2UT(Date2JDE(eventTime.Add(time.Hour).UTC()), true)
|
||||
|
||||
exactNext := NextMoonPlanetConjunction(queryAtTT, planet)
|
||||
exactClosest := ClosestMoonPlanetConjunction(queryAtTT, planet)
|
||||
exactLastAfter := LastMoonPlanetConjunction(queryAfterTT, planet)
|
||||
|
||||
wantUT := Date2JDE(wantTime.UTC())
|
||||
for name, gotUT := range map[string]float64{
|
||||
"exactNext": exactNext,
|
||||
"exactClosest": exactClosest,
|
||||
"lastAfterEvent": exactLastAfter,
|
||||
} {
|
||||
gotTime := JDE2DateByZone(gotUT, time.UTC, false)
|
||||
if diff := math.Abs(gotUT - wantUT); diff > 5.0/86400.0 {
|
||||
t.Fatalf("%s %s mismatch: got %s want %s diff=%v", sample.Planet, name, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, diff*86400)
|
||||
if diff := math.Abs(gotUT - eventUT); diff > 1e-9 {
|
||||
t.Fatalf("%s %s mismatch: got %s want %s diff=%v", sample.Planet, name, gotTime.Format(time.RFC3339Nano), eventTime.Format(time.RFC3339Nano), diff*86400)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -258,3 +260,25 @@ func TestMoonPlanetConjunctionKeepsImmediateNeighborEvents(t *testing.T) {
|
||||
t.Fatalf("closest should keep immediate previous event: closest=%s last=%s", JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoonPlanetConjunctionNextAdvancesPastReturnedEvent(t *testing.T) {
|
||||
seed := TD2UT(Date2JDE(time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)), true)
|
||||
eventUT := NextMoonPlanetConjunction(seed, MoonPlanetConjunctionMercury)
|
||||
query := JDE2DateByZone(eventUT, time.UTC, false).Add(time.Second)
|
||||
queryTT := TD2UT(Date2JDE(query.UTC()), true)
|
||||
|
||||
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionMercury)
|
||||
if eventUTQueryTTDelta(nextUT, queryTT) <= 0 {
|
||||
t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%.6fs",
|
||||
query.Format(time.RFC3339Nano),
|
||||
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
|
||||
eventUTQueryTTDelta(nextUT, queryTT)*86400,
|
||||
)
|
||||
}
|
||||
if sameEventJD(nextUT, eventUT) {
|
||||
t.Fatalf("next conjunction should advance to a later event: event=%s next=%s",
|
||||
JDE2DateByZone(eventUT, time.UTC, false).Format(time.RFC3339Nano),
|
||||
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ func TestOuterPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
|
||||
{name: "MarsEasternQuadrature", seed: NextMarsEasternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsEasternQuadrature, nextFn: NextMarsEasternQuadrature},
|
||||
{name: "MarsWesternQuadrature", seed: NextMarsWesternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsWesternQuadrature, nextFn: NextMarsWesternQuadrature},
|
||||
{name: "MarsP2R", seed: NextMarsProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsProgradeToRetrograde, nextFn: NextMarsProgradeToRetrograde},
|
||||
{name: "MarsR2P", seed: NextMarsRetrogradeToPrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsRetrogradeToPrograde, nextFn: NextMarsRetrogradeToPrograde},
|
||||
{name: "MarsR2P", seed: NextMarsRetrogradeToPrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastMarsRetrogradeToPrograde, nextFn: NextMarsRetrogradeToPrograde},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@ -50,6 +50,35 @@ func TestOuterPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOuterPlanetNextEventAdvancesPastReturnedEvent(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
seed float64
|
||||
next func(float64) float64
|
||||
}{
|
||||
{name: "MarsOpposition", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextMarsOpposition},
|
||||
{name: "MarsP2R", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextMarsProgradeToRetrograde},
|
||||
{name: "JupiterOpposition", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextJupiterOpposition},
|
||||
{name: "SaturnConjunction", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextSaturnConjunction},
|
||||
{name: "UranusP2R", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextUranusProgradeToRetrograde},
|
||||
{name: "NeptuneR2P", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextNeptuneRetrogradeToPrograde},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
first := tc.next(tc.seed)
|
||||
query := TD2UT(Date2JDE(JDE2DateByZone(first, time.UTC, false).Add(time.Second)), true)
|
||||
next := tc.next(query)
|
||||
if !eventUTQueryAfterOrEqual(next, query) {
|
||||
t.Fatalf("next should be after query: first=%.12f query=%.12f next=%.12f", first, query, next)
|
||||
}
|
||||
if sameEventJD(next, first) {
|
||||
t.Fatalf("next should advance past first event: first=%.12f next=%.12f", first, next)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ttjdUTC(year, month, day, hour, min, sec int) float64 {
|
||||
return TD2UT(Date2JDE(time.Date(year, time.Month(month), day, hour, min, sec, 0, time.UTC)), true)
|
||||
}
|
||||
|
||||
@ -202,10 +202,14 @@ func venusConjunction(jde float64, next uint8) float64 {
|
||||
}
|
||||
left := queryTT
|
||||
leftVal := venusSunLongitudeDeltaN(left, venusEventSearchN)
|
||||
if math.Abs(leftVal) <= 30.0/86400.0 {
|
||||
if math.Abs(venusSunLongitudeDelta(queryTT)) <= 30.0/86400.0 {
|
||||
exact := eventZeroRefine(left, 1.0, 0.000005, venusSunLongitudeDelta)
|
||||
if math.Abs(exact-queryTT) <= 1.0 {
|
||||
return TD2UT(exact, false)
|
||||
eventUT := TD2UT(exact, false)
|
||||
if next == 0 && eventUTQueryBeforeOrEqual(eventUT, queryTT) {
|
||||
return eventUT
|
||||
}
|
||||
if next == 1 && eventUTQueryAfterOrEqual(eventUT, queryTT) {
|
||||
return eventUT
|
||||
}
|
||||
}
|
||||
const step = 8.0
|
||||
@ -432,12 +436,12 @@ func venusGreatestElongationInWindow(start, end float64) float64 {
|
||||
|
||||
func venusEastElongationWindowEndingAt(inferior float64) (float64, float64) {
|
||||
lastSuperior := LastVenusSuperiorConjunction(eventUTLastQueryTT(inferior))
|
||||
return lastSuperior + innerEventEpsilon, inferior - innerEventEpsilon
|
||||
return lastSuperior + innerEventWindowPadding, inferior - innerEventWindowPadding
|
||||
}
|
||||
|
||||
func venusWestElongationWindowEndingAt(superior float64) (float64, float64) {
|
||||
lastInferior := LastVenusInferiorConjunction(eventUTLastQueryTT(superior))
|
||||
return lastInferior + innerEventEpsilon, superior - innerEventEpsilon
|
||||
return lastInferior + innerEventWindowPadding, superior - innerEventWindowPadding
|
||||
}
|
||||
|
||||
func venusEastElongationWindowContaining(jde float64) (float64, float64) {
|
||||
@ -539,35 +543,19 @@ func LastVenusGreatestElongation(jde float64) float64 {
|
||||
}
|
||||
|
||||
func LastVenusInferiorConjunctionInclusive(jde float64) float64 {
|
||||
date := LastVenusConjunction(jde)
|
||||
if venusConjunctionTypeAt(date) {
|
||||
return date
|
||||
}
|
||||
return LastVenusConjunction(eventUTLastQueryTT(date))
|
||||
return inclusiveLastSimpleEvent(jde, LastVenusInferiorConjunction, NextVenusInferiorConjunction)
|
||||
}
|
||||
|
||||
func NextVenusInferiorConjunctionInclusive(jde float64) float64 {
|
||||
date := NextVenusConjunction(jde)
|
||||
if venusConjunctionTypeAt(date) {
|
||||
return date
|
||||
}
|
||||
return NextVenusConjunction(eventUTNextQueryTT(date))
|
||||
return inclusiveNextSimpleEvent(jde, LastVenusInferiorConjunction, NextVenusInferiorConjunction)
|
||||
}
|
||||
|
||||
func LastVenusSuperiorConjunctionInclusive(jde float64) float64 {
|
||||
date := LastVenusConjunction(jde)
|
||||
if !venusConjunctionTypeAt(date) {
|
||||
return date
|
||||
}
|
||||
return LastVenusConjunction(eventUTLastQueryTT(date))
|
||||
return inclusiveLastSimpleEvent(jde, LastVenusSuperiorConjunction, NextVenusSuperiorConjunction)
|
||||
}
|
||||
|
||||
func NextVenusSuperiorConjunctionInclusive(jde float64) float64 {
|
||||
date := NextVenusConjunction(jde)
|
||||
if !venusConjunctionTypeAt(date) {
|
||||
return date
|
||||
}
|
||||
return NextVenusConjunction(eventUTNextQueryTT(date))
|
||||
return inclusiveNextSimpleEvent(jde, LastVenusSuperiorConjunction, NextVenusSuperiorConjunction)
|
||||
}
|
||||
|
||||
func LastVenusRetrogradeInclusive(jde float64) float64 {
|
||||
|
||||
@ -80,3 +80,24 @@ func TestInvalidConjunctionPlanetReturnsZeroTime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextConjunctionAdvancesPastReturnedEvent(t *testing.T) {
|
||||
cursor := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||
first := NextConjunctionWithPlanet(cursor, ConjunctionMercury)
|
||||
query := first.Add(time.Second)
|
||||
next := NextConjunctionWithPlanet(query, ConjunctionMercury)
|
||||
|
||||
if !next.After(query) {
|
||||
t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%v",
|
||||
query.Format(time.RFC3339Nano),
|
||||
next.Format(time.RFC3339Nano),
|
||||
next.Sub(query),
|
||||
)
|
||||
}
|
||||
if next.Equal(first) {
|
||||
t.Fatalf("expected next conjunction to advance: first=%s next=%s",
|
||||
first.Format(time.RFC3339Nano),
|
||||
next.Format(time.RFC3339Nano),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user