fix: 修复天象事件 API 在事件边界附近的重复返回问题

- 修正合月、合日、留等Last/Next/Closest接口在精确命中和事件后秒级查询时仍返回当前事件的问题,避免应用侧枚举卡死
- 收紧事件边界判定,并补强水星、金星、火星近事件路径的稳定化,保证公开API的单调前进语义
- 补充公开wrapper、跨世纪样本和外部基线回归,覆盖合月邻近事件与边界场景
This commit is contained in:
兔子 2026-05-23 23:08:05 +08:00
parent be3af3884c
commit 46b555cd49
Signed by: b612
GPG Key ID: 99DD2222B612B612
10 changed files with 235 additions and 51 deletions

View File

@ -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 {

View File

@ -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++ {

View File

@ -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)
}
})
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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
}

View File

@ -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),
)
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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),
)
}
}