From 46b555cd497c8a2954ca9ce2533b722350477e06 Mon Sep 17 00:00:00 2001 From: starainrt Date: Sat, 23 May 2026 23:08:05 +0800 Subject: [PATCH] =?UTF-8?q?=20fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=A9=E8=B1=A1?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=20API=20=E5=9C=A8=E4=BA=8B=E4=BB=B6=E8=BE=B9?= =?UTF-8?q?=E7=95=8C=E9=99=84=E8=BF=91=E7=9A=84=E9=87=8D=E5=A4=8D=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修正合月、合日、留等Last/Next/Closest接口在精确命中和事件后秒级查询时仍返回当前事件的问题,避免应用侧枚举卡死 - 收紧事件边界判定,并补强水星、金星、火星近事件路径的稳定化,保证公开API的单调前进语义 - 补充公开wrapper、跨世纪样本和外部基线回归,覆盖合月邻近事件与边界场景 --- basic/event_boundary.go | 7 ++- basic/inner_event_window.go | 8 ++- basic/inner_planet_event_boundary_test.go | 62 ++++++++++++++++++- basic/mars_events.go | 42 ++++++++++--- basic/mercury_events.go | 24 ++++++- basic/moon_planet_conjunction.go | 17 +++-- .../moon_planet_conjunction_external_test.go | 36 +++++++++-- basic/outer_planet_event_boundary_test.go | 31 +++++++++- basic/venus_events.go | 38 ++++-------- moon/conjunction_test.go | 21 +++++++ 10 files changed, 235 insertions(+), 51 deletions(-) diff --git a/basic/event_boundary.go b/basic/event_boundary.go index 2d06ab6..d41d8fb 100644 --- a/basic/event_boundary.go +++ b/basic/event_boundary.go @@ -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 { diff --git a/basic/inner_event_window.go b/basic/inner_event_window.go index 0122c03..8154d1f 100644 --- a/basic/inner_event_window.go +++ b/basic/inner_event_window.go @@ -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++ { diff --git a/basic/inner_planet_event_boundary_test.go b/basic/inner_planet_event_boundary_test.go index e3fb2b8..8eeb02a 100644 --- a/basic/inner_planet_event_boundary_test.go +++ b/basic/inner_planet_event_boundary_test.go @@ -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) + } + }) + } +} diff --git a/basic/mars_events.go b/basic/mars_events.go index 25afb22..8fffb54 100644 --- a/basic/mars_events.go +++ b/basic/mars_events.go @@ -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) diff --git a/basic/mercury_events.go b/basic/mercury_events.go index fa5837e..42e10c9 100644 --- a/basic/mercury_events.go +++ b/basic/mercury_events.go @@ -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) { diff --git a/basic/moon_planet_conjunction.go b/basic/moon_planet_conjunction.go index 21fd2a9..c70384e 100644 --- a/basic/moon_planet_conjunction.go +++ b/basic/moon_planet_conjunction.go @@ -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 } diff --git a/basic/moon_planet_conjunction_external_test.go b/basic/moon_planet_conjunction_external_test.go index 599e798..689a788 100644 --- a/basic/moon_planet_conjunction_external_test.go +++ b/basic/moon_planet_conjunction_external_test.go @@ -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), + ) + } +} diff --git a/basic/outer_planet_event_boundary_test.go b/basic/outer_planet_event_boundary_test.go index 3240276..5bcedb6 100644 --- a/basic/outer_planet_event_boundary_test.go +++ b/basic/outer_planet_event_boundary_test.go @@ -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) } diff --git a/basic/venus_events.go b/basic/venus_events.go index 5465f63..89deb60 100644 --- a/basic/venus_events.go +++ b/basic/venus_events.go @@ -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 { diff --git a/moon/conjunction_test.go b/moon/conjunction_test.go index d81d8f7..b501ba1 100644 --- a/moon/conjunction_test.go +++ b/moon/conjunction_test.go @@ -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), + ) + } +}