diff --git a/basic/moon_geocentric_apparent_external_test.go b/basic/moon_geocentric_apparent_external_test.go new file mode 100644 index 0000000..49b55b1 --- /dev/null +++ b/basic/moon_geocentric_apparent_external_test.go @@ -0,0 +1,64 @@ +package basic + +import ( + "encoding/json" + "os" + "testing" + "time" +) + +type moonGeocentricApparentSample struct { + InputUTC string `json:"input_utc"` + RightAscension float64 `json:"right_ascension"` + Declination float64 `json:"declination"` + EclipticLongitude float64 `json:"ecliptic_longitude"` + EclipticLatitude float64 `json:"ecliptic_latitude"` +} + +func TestMoonGeocentricApparentCoordinatesMatchHorizonsBaseline(t *testing.T) { + data, err := os.ReadFile("testdata/moon_geocentric_apparent_baseline.json") + if err != nil { + t.Fatalf("read baseline: %v", err) + } + + var samples []moonGeocentricApparentSample + if err := json.Unmarshal(data, &samples); err != nil { + t.Fatalf("decode baseline: %v", err) + } + if len(samples) == 0 { + t.Fatal("empty moon apparent baseline") + } + + for _, sample := range samples { + date, err := time.Parse(time.RFC3339, sample.InputUTC) + if err != nil { + t.Fatalf("parse sample time %q: %v", sample.InputUTC, err) + } + jd := TD2UT(Date2JDE(date.UTC()), true) + prefix := "moon." + sample.InputUTC + + assertPlanetApparentAngleClose(t, prefix+".RightAscension", HMoonGeocentricApparentRa(jd), sample.RightAscension, 0.001) + assertPlanetPhaseClose(t, prefix+".Declination", HMoonGeocentricApparentDec(jd), sample.Declination, 0.001) + assertPlanetApparentAngleClose(t, prefix+".EclipticLongitude", HMoonApparentLo(jd), sample.EclipticLongitude, 0.001) + assertPlanetPhaseClose(t, prefix+".EclipticLatitude", HMoonTrueBo(jd), sample.EclipticLatitude, 0.001) + } +} + +func TestMoonGeocentricTrueCoordinatesFollowDefinition(t *testing.T) { + samples := []time.Time{ + time.Date(1900, 1, 14, 12, 0, 0, 0, time.UTC), + time.Date(1950, 6, 3, 0, 0, 0, 0, time.UTC), + time.Date(2000, 2, 29, 18, 0, 0, 0, time.UTC), + time.Date(2026, 1, 1, 6, 0, 0, 0, time.UTC), + time.Date(2100, 8, 17, 9, 0, 0, 0, time.UTC), + } + + for _, sample := range samples { + jd := TD2UT(Date2JDE(sample.UTC()), true) + wantRA, wantDec := LoBoToRaDec(jd, HMoonTrueLo(jd), HMoonTrueBo(jd)) + gotRA, gotDec := HMoonGeocentricTrueRaDec(jd) + + assertPlanetApparentAngleClose(t, sample.Format(time.RFC3339)+".TrueRightAscension", gotRA, wantRA, 1e-12) + assertPlanetPhaseClose(t, sample.Format(time.RFC3339)+".TrueDeclination", gotDec, wantDec, 1e-12) + } +} diff --git a/basic/moon_geocentric_apparent_test.go b/basic/moon_geocentric_apparent_test.go new file mode 100644 index 0000000..4765f88 --- /dev/null +++ b/basic/moon_geocentric_apparent_test.go @@ -0,0 +1,30 @@ +package basic + +import ( + "math" + "testing" +) + +func TestHMoonGeocentricApparentRaDecComponentsMatch(t *testing.T) { + jd := TD2UT(JDECalc(2026, 1, 1.25), true) + + ra, dec := HMoonGeocentricApparentRaDec(jd) + if diff := math.Abs(ra - HMoonGeocentricApparentRa(jd)); diff > 1e-12 { + t.Fatalf("RA pair mismatch: got %.15f want %.15f", ra, HMoonGeocentricApparentRa(jd)) + } + if diff := math.Abs(dec - HMoonGeocentricApparentDec(jd)); diff > 1e-12 { + t.Fatalf("Dec pair mismatch: got %.15f want %.15f", dec, HMoonGeocentricApparentDec(jd)) + } +} + +func TestHMoonGeocentricTrueRaDecComponentsMatch(t *testing.T) { + jd := TD2UT(JDECalc(2026, 1, 1.25), true) + + ra, dec := HMoonGeocentricTrueRaDec(jd) + if diff := math.Abs(ra - HMoonGeocentricTrueRa(jd)); diff > 1e-12 { + t.Fatalf("RA pair mismatch: got %.15f want %.15f", ra, HMoonGeocentricTrueRa(jd)) + } + if diff := math.Abs(dec - HMoonGeocentricTrueDec(jd)); diff > 1e-12 { + t.Fatalf("Dec pair mismatch: got %.15f want %.15f", dec, HMoonGeocentricTrueDec(jd)) + } +} diff --git a/basic/moon_planet_conjunction.go b/basic/moon_planet_conjunction.go new file mode 100644 index 0000000..21fd2a9 --- /dev/null +++ b/basic/moon_planet_conjunction.go @@ -0,0 +1,388 @@ +package basic + +import "math" + +const ( + moonPlanetConjunctionEstimateN = 8 + moonPlanetConjunctionNearQueryDeltaDeg = 3.0 + moonPlanetConjunctionBracketStepDays = 0.5 + moonPlanetConjunctionNearQueryStepDays = 0.25 + moonPlanetConjunctionNearQueryHalfSpan = 1.5 + moonPlanetConjunctionBracketHalfSpan = 2.0 + moonPlanetConjunctionBracketGrowth = 2.0 + moonPlanetConjunctionBracketAttempts = 3 + moonPlanetConjunctionRefineStepDays = 0.5 / 86400.0 + moonPlanetConjunctionEventTolerance = 0.01 + moonPlanetConjunctionFallbackSpanScale = 1.5 +) + +type moonPlanetConjunctionLocalResult struct { + lastUT float64 + nextUT float64 +} + +func emptyMoonPlanetConjunctionLocalResult() moonPlanetConjunctionLocalResult { + return moonPlanetConjunctionLocalResult{ + lastUT: math.NaN(), + nextUT: math.NaN(), + } +} + +// MoonPlanetConjunctionPlanet 月球合月目标行星 / target planet for Moon-planet conjunction events. +type MoonPlanetConjunctionPlanet int + +const ( + MoonPlanetConjunctionMercury MoonPlanetConjunctionPlanet = iota + 1 + MoonPlanetConjunctionVenus + MoonPlanetConjunctionMars + MoonPlanetConjunctionJupiter + MoonPlanetConjunctionSaturn + MoonPlanetConjunctionUranus + MoonPlanetConjunctionNeptune +) + +func moonPlanetConjunctionWrappedDelta(diff float64) float64 { + diff = math.Mod(diff+180, 360) + if diff < 0 { + diff += 360 + } + return diff - 180 +} + +func moonPlanetConjunctionDeltaAt(jdTT float64, planet MoonPlanetConjunctionPlanet, n int) float64 { + moonRA := HMoonGeocentricApparentRaN(jdTT, n) + var planetRA float64 + switch planet { + case MoonPlanetConjunctionMercury: + planetRA = MercuryApparentRaN(jdTT, n) + case MoonPlanetConjunctionVenus: + planetRA = VenusApparentRaN(jdTT, n) + case MoonPlanetConjunctionMars: + planetRA = MarsApparentRaN(jdTT, n) + case MoonPlanetConjunctionJupiter: + planetRA = JupiterApparentRaN(jdTT, n) + case MoonPlanetConjunctionSaturn: + planetRA = SaturnApparentRaN(jdTT, n) + case MoonPlanetConjunctionUranus: + planetRA = UranusApparentRaN(jdTT, n) + case MoonPlanetConjunctionNeptune: + planetRA = NeptuneApparentRaN(jdTT, n) + default: + return math.NaN() + } + return moonPlanetConjunctionWrappedDelta(moonRA - planetRA) +} + +func moonPlanetConjunctionPeriodDays(planet MoonPlanetConjunctionPlanet) float64 { + switch planet { + case MoonPlanetConjunctionMercury: + return 28.1 + case MoonPlanetConjunctionVenus: + return 28.4 + case MoonPlanetConjunctionMars: + return 29.2 + case MoonPlanetConjunctionJupiter: + return 28.0 + case MoonPlanetConjunctionSaturn: + return 27.4 + case MoonPlanetConjunctionUranus: + return 27.3 + case MoonPlanetConjunctionNeptune: + return 27.3 + default: + return math.NaN() + } +} + +func moonPlanetConjunctionInDirection(eventUT, queryTT float64, direction int) bool { + switch direction { + case -1: + return eventUTQueryBeforeOrEqual(eventUT, queryTT) + case 1: + return eventUTQueryAfterOrEqual(eventUT, queryTT) + default: + return true + } +} + +func moonPlanetConjunctionFindBracket(centerTT, halfSpan, step float64, planet MoonPlanetConjunctionPlanet) (float64, float64, bool) { + if math.IsNaN(centerTT) || math.IsNaN(halfSpan) || math.IsNaN(step) || halfSpan <= 0 || step <= 0 { + return 0, 0, false + } + start := centerTT - halfSpan + end := centerTT + halfSpan + samples := int(math.Ceil((end-start)/step)) + 1 + prevTT := start + prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1) + if math.IsNaN(prevVal) { + return 0, 0, false + } + if prevVal == 0 { + return prevTT, prevTT, true + } + bestLeft := 0.0 + bestRight := 0.0 + bestDistance := math.Inf(1) + for i := 1; i <= samples; i++ { + tt := start + float64(i)*step + if tt > end { + tt = end + } + val := moonPlanetConjunctionDeltaAt(tt, planet, -1) + if math.IsNaN(val) { + return 0, 0, false + } + if val == 0 { + return tt, tt, true + } + if prevVal*val < 0 { + mid := (prevTT + tt) / 2.0 + distance := math.Abs(mid - centerTT) + if distance < bestDistance { + bestLeft = prevTT + bestRight = tt + bestDistance = distance + } + } + if tt == end { + break + } + prevTT = tt + prevVal = val + } + if math.IsInf(bestDistance, 1) { + return 0, 0, false + } + return bestLeft, bestRight, true +} + +func moonPlanetConjunctionRefineBracket(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet) float64 { + if leftTT > rightTT { + leftTT, rightTT = rightTT, leftTT + } + if leftTT == rightTT { + return leftTT + } + center := (leftTT + rightTT) / 2.0 + halfWindow := (rightTT - leftTT) / 2.0 + return eventZeroRefine(center, halfWindow, moonPlanetConjunctionRefineStepDays, func(sampleTT float64) float64 { + return moonPlanetConjunctionDeltaAt(sampleTT, planet, -1) + }) +} + +func moonPlanetConjunctionEventUT(leftTT, rightTT float64, planet MoonPlanetConjunctionPlanet) float64 { + eventTT := moonPlanetConjunctionRefineBracket(leftTT, rightTT, planet) + if math.Abs(moonPlanetConjunctionDeltaAt(eventTT, planet, -1)) > moonPlanetConjunctionEventTolerance { + return math.NaN() + } + return TD2UT(eventTT, false) +} + +func moonPlanetConjunctionCollectLocalEvent(result *moonPlanetConjunctionLocalResult, queryTT, eventUT float64) { + if math.IsNaN(eventUT) { + return + } + if eventUTQueryBeforeOrEqual(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 math.IsNaN(result.nextUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.nextUT, queryTT)) { + result.nextUT = eventUT + } + } +} + +func moonPlanetConjunctionShouldCheckLocal(queryTT float64, planet MoonPlanetConjunctionPlanet) bool { + delta := moonPlanetConjunctionDeltaAt(queryTT, planet, moonPlanetConjunctionEstimateN) + if math.IsNaN(delta) { + return false + } + return math.Abs(delta) <= moonPlanetConjunctionNearQueryDeltaDeg +} + +func moonPlanetConjunctionLocalEvents(queryTT float64, planet MoonPlanetConjunctionPlanet) moonPlanetConjunctionLocalResult { + result := emptyMoonPlanetConjunctionLocalResult() + start := queryTT - moonPlanetConjunctionNearQueryHalfSpan + end := queryTT + moonPlanetConjunctionNearQueryHalfSpan + step := moonPlanetConjunctionNearQueryStepDays + prevTT := start + prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1) + if math.IsNaN(prevVal) { + return result + } + samples := int(math.Ceil((end-start)/step)) + 1 + for i := 1; i <= samples; i++ { + tt := start + float64(i)*step + if tt > end { + tt = end + } + val := moonPlanetConjunctionDeltaAt(tt, planet, -1) + if math.IsNaN(val) { + return emptyMoonPlanetConjunctionLocalResult() + } + if prevVal == 0 || val == 0 || prevVal*val < 0 { + moonPlanetConjunctionCollectLocalEvent(&result, queryTT, moonPlanetConjunctionEventUT(prevTT, tt, planet)) + } + if tt == end { + break + } + prevTT = tt + prevVal = val + } + return result +} + +func moonPlanetConjunctionMaybeLocalEvents(queryTT float64, planet MoonPlanetConjunctionPlanet) moonPlanetConjunctionLocalResult { + if !moonPlanetConjunctionShouldCheckLocal(queryTT, planet) { + return emptyMoonPlanetConjunctionLocalResult() + } + return moonPlanetConjunctionLocalEvents(queryTT, planet) +} + +func moonPlanetConjunctionGuessTT(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 { + delta := moonPlanetConjunctionDeltaAt(queryTT, planet, moonPlanetConjunctionEstimateN) + if math.IsNaN(delta) { + return math.NaN() + } + period := moonPlanetConjunctionPeriodDays(planet) + if math.IsNaN(period) { + return math.NaN() + } + switch direction { + case -1: + return queryTT - innerLastCycleOffset(delta, period) + case 1: + return queryTT + innerNextCycleOffset(delta, period) + default: + return math.NaN() + } +} + +func moonPlanetConjunctionDirectionalFallback(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 { + period := moonPlanetConjunctionPeriodDays(planet) + if math.IsNaN(period) { + return math.NaN() + } + span := period * moonPlanetConjunctionFallbackSpanScale + if span <= 0 { + return math.NaN() + } + step := moonPlanetConjunctionNearQueryStepDays + start := queryTT + end := queryTT + switch direction { + case -1: + start -= span + case 1: + end += span + default: + return math.NaN() + } + + prevTT := start + prevVal := moonPlanetConjunctionDeltaAt(prevTT, planet, -1) + if math.IsNaN(prevVal) { + return math.NaN() + } + + bestEventUT := math.NaN() + for tt := start + step; ; tt += step { + if tt > end { + tt = end + } + val := moonPlanetConjunctionDeltaAt(tt, planet, -1) + if math.IsNaN(val) { + return math.NaN() + } + if prevVal == 0 || val == 0 || prevVal*val < 0 { + eventUT := moonPlanetConjunctionEventUT(prevTT, tt, planet) + if !math.IsNaN(eventUT) && moonPlanetConjunctionInDirection(eventUT, queryTT, direction) { + if direction == 1 { + return eventUT + } + bestEventUT = eventUT + } + } + if tt == end { + break + } + prevTT = tt + prevVal = val + } + return bestEventUT +} + +func moonPlanetConjunctionDirectionalEventWithLocal(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int, local moonPlanetConjunctionLocalResult) float64 { + switch direction { + case -1: + if !math.IsNaN(local.lastUT) { + return local.lastUT + } + case 1: + if !math.IsNaN(local.nextUT) { + return local.nextUT + } + } + guessTT := moonPlanetConjunctionGuessTT(queryTT, planet, direction) + if math.IsNaN(guessTT) { + return math.NaN() + } + halfSpan := moonPlanetConjunctionBracketHalfSpan + for attempt := 0; attempt < moonPlanetConjunctionBracketAttempts; attempt++ { + left, right, ok := moonPlanetConjunctionFindBracket(guessTT, halfSpan, moonPlanetConjunctionBracketStepDays, planet) + if ok { + eventUT := moonPlanetConjunctionEventUT(left, right, planet) + if math.IsNaN(eventUT) { + halfSpan *= moonPlanetConjunctionBracketGrowth + continue + } + if moonPlanetConjunctionInDirection(eventUT, queryTT, direction) { + return eventUT + } + } + halfSpan *= moonPlanetConjunctionBracketGrowth + } + return moonPlanetConjunctionDirectionalFallback(queryTT, planet, direction) +} + +func moonPlanetConjunctionDirectionalEvent(queryTT float64, planet MoonPlanetConjunctionPlanet, direction int) float64 { + return moonPlanetConjunctionDirectionalEventWithLocal(queryTT, planet, direction, moonPlanetConjunctionMaybeLocalEvents(queryTT, planet)) +} + +// LastMoonPlanetConjunction 指定时刻之前最近一次行星合月(赤经合) / previous Moon-planet conjunction at or before jde. +func LastMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 { + return moonPlanetConjunctionDirectionalEvent(jde, planet, -1) +} + +// NextMoonPlanetConjunction 指定时刻之后最近一次行星合月(赤经合) / next Moon-planet conjunction at or after jde. +func NextMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 { + return moonPlanetConjunctionDirectionalEvent(jde, planet, 1) +} + +// ClosestMoonPlanetConjunction 离指定时刻最近一次行星合月(赤经合) / closest Moon-planet conjunction to jde. +func ClosestMoonPlanetConjunction(jde float64, planet MoonPlanetConjunctionPlanet) float64 { + local := moonPlanetConjunctionMaybeLocalEvents(jde, planet) + if !math.IsNaN(local.lastUT) && !math.IsNaN(local.nextUT) { + if sameEventJD(local.lastUT, local.nextUT) { + return local.lastUT + } + return closestEventUTToQueryTT(jde, local.lastUT, local.nextUT) + } + if !math.IsNaN(local.lastUT) { + return local.lastUT + } + if !math.IsNaN(local.nextUT) { + return local.nextUT + } + last := moonPlanetConjunctionDirectionalEventWithLocal(jde, planet, -1, local) + next := moonPlanetConjunctionDirectionalEventWithLocal(jde, planet, 1, local) + if math.IsNaN(last) { + return next + } + if math.IsNaN(next) { + return last + } + return closestEventUTToQueryTT(jde, last, next) +} diff --git a/basic/moon_planet_conjunction_external_test.go b/basic/moon_planet_conjunction_external_test.go new file mode 100644 index 0000000..599e798 --- /dev/null +++ b/basic/moon_planet_conjunction_external_test.go @@ -0,0 +1,260 @@ +package basic + +import ( + "encoding/json" + "math" + "os" + "testing" + "time" +) + +type moonPlanetConjunctionBaselineSample struct { + Planet string `json:"planet"` + Year int `json:"year"` + Month int `json:"month"` + TimeUTC string `json:"time_utc"` +} + +type moonPlanetConjunctionBaseline struct { + Samples []moonPlanetConjunctionBaselineSample `json:"samples"` +} + +func loadMoonPlanetConjunctionBaseline(t *testing.T) moonPlanetConjunctionBaseline { + t.Helper() + + paths := [][]string{ + { + "testdata/moon_planet_conjunction_baseline.json", + "basic/testdata/moon_planet_conjunction_baseline.json", + }, + { + "testdata/moon_planet_conjunction_baseline_samples.json", + "basic/testdata/moon_planet_conjunction_baseline_samples.json", + }, + } + + var merged moonPlanetConjunctionBaseline + for index, candidates := range paths { + var ( + data []byte + err error + ) + for _, path := range candidates { + data, err = os.ReadFile(path) + if err == nil { + var baseline moonPlanetConjunctionBaseline + if err := json.Unmarshal(data, &baseline); err != nil { + t.Fatalf("decode baseline %s: %v", path, err) + } + merged.Samples = append(merged.Samples, baseline.Samples...) + break + } + } + if err != nil && index == 0 { + t.Fatalf("read baseline: %v", err) + } + } + if len(merged.Samples) == 0 { + t.Fatal("empty moon-planet conjunction baseline") + } + return merged +} + +func TestMoonPlanetConjunctionsMatchHorizonsBaseline(t *testing.T) { + baseline := loadMoonPlanetConjunctionBaseline(t) + + type conjunctionCase struct { + planet MoonPlanetConjunctionPlanet + next func(float64, MoonPlanetConjunctionPlanet) float64 + } + + cases := map[string]conjunctionCase{ + "mercury": {planet: MoonPlanetConjunctionMercury, next: NextMoonPlanetConjunction}, + "venus": {planet: MoonPlanetConjunctionVenus, next: NextMoonPlanetConjunction}, + "mars": {planet: MoonPlanetConjunctionMars, next: NextMoonPlanetConjunction}, + "jupiter": {planet: MoonPlanetConjunctionJupiter, next: NextMoonPlanetConjunction}, + "saturn": {planet: MoonPlanetConjunctionSaturn, next: NextMoonPlanetConjunction}, + "uranus": {planet: MoonPlanetConjunctionUranus, next: NextMoonPlanetConjunction}, + "neptune": {planet: MoonPlanetConjunctionNeptune, next: NextMoonPlanetConjunction}, + } + + const tolerance = 20 * time.Second + var maxDiff time.Duration + + seen := make(map[string]int, len(cases)) + for _, sample := range baseline.Samples { + tc, ok := cases[sample.Planet] + if !ok { + t.Fatalf("unknown planet %q", sample.Planet) + } + + wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC) + if err != nil { + t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err) + } + queryTT := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true) + gotUT := tc.next(queryTT, tc.planet) + gotTime := JDE2DateByZone(gotUT, time.UTC, false) + diff := gotTime.Sub(wantTime) + if diff < 0 { + diff = -diff + } + if diff > maxDiff { + maxDiff = diff + } + if diff > tolerance { + t.Fatalf("%s %04d-%02d time mismatch: got %s want %s tolerance %v", sample.Planet, sample.Year, sample.Month, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, tolerance) + } + + delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), tc.planet, -1)) + if delta > 0.01 { + t.Fatalf("%s %04d-%02d event not near conjunction: delta=%.8f deg", sample.Planet, sample.Year, sample.Month, delta) + } + seen[sample.Planet]++ + } + + for planet := range cases { + if seen[planet] == 0 { + t.Fatalf("missing baseline samples for %s", planet) + } + } + + t.Logf("moon-planet conjunction max diff: time=%v", maxDiff) +} + +func TestMoonPlanetConjunctionDirectionalConsistencyAroundBaseline(t *testing.T) { + baseline := loadMoonPlanetConjunctionBaseline(t) + + planets := map[string]MoonPlanetConjunctionPlanet{ + "mercury": MoonPlanetConjunctionMercury, + "venus": MoonPlanetConjunctionVenus, + "mars": MoonPlanetConjunctionMars, + "jupiter": MoonPlanetConjunctionJupiter, + "saturn": MoonPlanetConjunctionSaturn, + "uranus": MoonPlanetConjunctionUranus, + "neptune": MoonPlanetConjunctionNeptune, + } + + for _, sample := range baseline.Samples { + planet, ok := planets[sample.Planet] + if !ok { + t.Fatalf("unknown planet %q", sample.Planet) + } + wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC) + 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) + + 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) + } + } + } +} + +func TestMoonPlanetConjunctionRejectsOppositionBranchJump(t *testing.T) { + query := time.Date(1900, 11, 10, 12, 0, 0, 0, time.UTC) + queryTT := TD2UT(Date2JDE(query), true) + + lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn) + nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn) + + if math.Abs(lastUT-Date2JDE(query)) <= 5.0/86400.0 { + t.Fatalf("last returned query time on branch jump: got %s", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano)) + } + if math.Abs(nextUT-Date2JDE(query)) <= 5.0/86400.0 { + t.Fatalf("next returned query time on branch jump: got %s", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano)) + } + + for name, gotUT := range map[string]float64{ + "last": lastUT, + "next": nextUT, + } { + delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), MoonPlanetConjunctionSaturn, -1)) + if delta > moonPlanetConjunctionEventTolerance { + t.Fatalf("%s returned non-event candidate: delta=%.8f event=%s", name, delta, JDE2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano)) + } + } +} + +func TestMoonPlanetConjunctionDirectionalOrderingOnSampleQueries(t *testing.T) { + samples := []struct { + planet MoonPlanetConjunctionPlanet + query time.Time + }{ + {planet: MoonPlanetConjunctionSaturn, query: time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC)}, + {planet: MoonPlanetConjunctionMercury, query: time.Date(1900, 1, 14, 12, 0, 0, 0, time.UTC)}, + {planet: MoonPlanetConjunctionVenus, query: time.Date(1950, 6, 3, 12, 0, 0, 0, time.UTC)}, + {planet: MoonPlanetConjunctionMars, query: time.Date(2000, 2, 29, 18, 0, 0, 0, time.UTC)}, + {planet: MoonPlanetConjunctionJupiter, query: time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC)}, + {planet: MoonPlanetConjunctionSaturn, query: time.Date(2100, 8, 17, 6, 0, 0, 0, time.UTC)}, + {planet: MoonPlanetConjunctionUranus, query: time.Date(2200, 11, 2, 9, 0, 0, 0, time.UTC)}, + {planet: MoonPlanetConjunctionNeptune, query: time.Date(2300, 4, 24, 3, 0, 0, 0, time.UTC)}, + } + + for _, sample := range samples { + queryTT := TD2UT(Date2JDE(sample.query.UTC()), true) + lastUT := LastMoonPlanetConjunction(queryTT, sample.planet) + nextUT := NextMoonPlanetConjunction(queryTT, sample.planet) + closestUT := ClosestMoonPlanetConjunction(queryTT, sample.planet) + + if math.IsNaN(lastUT) || math.IsNaN(nextUT) || math.IsNaN(closestUT) { + t.Fatalf("planet=%v query=%s returned NaN event(s): last=%v next=%v closest=%v", sample.planet, sample.query.Format(time.RFC3339), lastUT, nextUT, closestUT) + } + if !eventUTQueryBeforeOrEqual(lastUT, queryTT) { + t.Fatalf("planet=%v last after query: last=%s query=%s", sample.planet, JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano)) + } + if !eventUTQueryAfterOrEqual(nextUT, queryTT) { + t.Fatalf("planet=%v next before query: next=%s query=%s", sample.planet, JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano)) + } + if closestUT != closestEventUTToQueryTT(queryTT, lastUT, nextUT) { + t.Fatalf("planet=%v closest mismatch: got=%s want=%s", sample.planet, JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(closestEventUTToQueryTT(queryTT, lastUT, nextUT), time.UTC, false).Format(time.RFC3339Nano)) + } + for name, gotUT := range map[string]float64{ + "last": lastUT, + "next": nextUT, + "closest": closestUT, + } { + delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), sample.planet, -1)) + if delta > moonPlanetConjunctionEventTolerance { + t.Fatalf("planet=%v %s returned non-event candidate: delta=%.8f event=%s", sample.planet, name, delta, JDE2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano)) + } + } + } +} + +func TestMoonPlanetConjunctionKeepsImmediateNeighborEvents(t *testing.T) { + query := time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC) + queryTT := TD2UT(Date2JDE(query.UTC()), true) + + lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn) + nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn) + closestUT := ClosestMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn) + + wantLast := time.Date(1700, 4, 15, 11, 55, 59, 115569293, time.UTC) + wantNext := time.Date(1700, 5, 13, 0, 35, 5, 981616675, time.UTC) + const tolerance = 5.0 / 86400.0 + + if diff := math.Abs(lastUT - Date2JDE(wantLast)); diff > tolerance { + t.Fatalf("last mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), wantLast.Format(time.RFC3339Nano), diff*86400) + } + if diff := math.Abs(nextUT - Date2JDE(wantNext)); diff > tolerance { + t.Fatalf("next mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), wantNext.Format(time.RFC3339Nano), diff*86400) + } + if !sameEventJD(closestUT, lastUT) { + 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)) + } +} diff --git a/basic/moon_precision.go b/basic/moon_precision.go index 5fa0c39..f98d971 100644 --- a/basic/moon_precision.go +++ b/basic/moon_precision.go @@ -126,6 +126,68 @@ func HMoonApparentLoN(jd float64, n int) float64 { return HMoonTrueLoN(jd, n) + Nutation2000Bi(jd) } +// HMoonGeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension of the Moon. +func HMoonGeocentricApparentRa(jd float64) float64 { + return HMoonGeocentricApparentRaN(jd, -1) +} + +// HMoonGeocentricApparentRaN 月亮地心视赤经(截断版) / truncated apparent geocentric right ascension of the Moon. +func HMoonGeocentricApparentRaN(jd float64, n int) float64 { + return LoToRa(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n)) +} + +// HMoonGeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination of the Moon. +func HMoonGeocentricApparentDec(jd float64) float64 { + return HMoonGeocentricApparentDecN(jd, -1) +} + +// HMoonGeocentricApparentDecN 月亮地心视赤纬(截断版) / truncated apparent geocentric declination of the Moon. +func HMoonGeocentricApparentDecN(jd float64, n int) float64 { + return ArcSin(Sin(HMoonTrueBoN(jd, n))*Cos(TrueObliquity(jd)) + + Cos(HMoonTrueBoN(jd, n))*Sin(TrueObliquity(jd))*Sin(HMoonApparentLoN(jd, n))) +} + +// HMoonGeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination of the Moon. +func HMoonGeocentricApparentRaDec(jd float64) (float64, float64) { + return HMoonGeocentricApparentRaDecN(jd, -1) +} + +// HMoonGeocentricApparentRaDecN 月亮地心视赤经、视赤纬(截断版) / truncated apparent geocentric right ascension and declination of the Moon. +func HMoonGeocentricApparentRaDecN(jd float64, n int) (float64, float64) { + return LoBoToRaDec(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n)) +} + +// HMoonGeocentricTrueRa 月亮地心真赤经 / true geocentric right ascension of the Moon. +func HMoonGeocentricTrueRa(jd float64) float64 { + return HMoonGeocentricTrueRaN(jd, -1) +} + +// HMoonGeocentricTrueRaN 月亮地心真赤经(截断版) / truncated true geocentric right ascension of the Moon. +func HMoonGeocentricTrueRaN(jd float64, n int) float64 { + return LoToRa(jd, HMoonTrueLoN(jd, n), HMoonTrueBoN(jd, n)) +} + +// HMoonGeocentricTrueDec 月亮地心真赤纬 / true geocentric declination of the Moon. +func HMoonGeocentricTrueDec(jd float64) float64 { + return HMoonGeocentricTrueDecN(jd, -1) +} + +// HMoonGeocentricTrueDecN 月亮地心真赤纬(截断版) / truncated true geocentric declination of the Moon. +func HMoonGeocentricTrueDecN(jd float64, n int) float64 { + return ArcSin(Sin(HMoonTrueBoN(jd, n))*Cos(TrueObliquity(jd)) + + Cos(HMoonTrueBoN(jd, n))*Sin(TrueObliquity(jd))*Sin(HMoonTrueLoN(jd, n))) +} + +// HMoonGeocentricTrueRaDec 月亮地心真赤经、真赤纬 / true geocentric right ascension and declination of the Moon. +func HMoonGeocentricTrueRaDec(jd float64) (float64, float64) { + return HMoonGeocentricTrueRaDecN(jd, -1) +} + +// HMoonGeocentricTrueRaDecN 月亮地心真赤经、真赤纬(截断版) / truncated true geocentric right ascension and declination of the Moon. +func HMoonGeocentricTrueRaDecN(jd float64, n int) (float64, float64) { + return LoBoToRaDec(jd, HMoonTrueLoN(jd, n), HMoonTrueBoN(jd, n)) +} + func HMoonTrueRaDec(jd float64) (float64, float64) { return HMoonTrueRaDecN(jd, -1) } diff --git a/basic/testdata/moon_geocentric_apparent_baseline.json b/basic/testdata/moon_geocentric_apparent_baseline.json new file mode 100644 index 0000000..7491894 --- /dev/null +++ b/basic/testdata/moon_geocentric_apparent_baseline.json @@ -0,0 +1,34 @@ +[ + { + "input_utc": "2026-01-01T00:00:00Z", + "right_ascension": 63.920306258, + "declination": 26.403701421, + "ecliptic_longitude": 66.7156363, + "ecliptic_latitude": 5.0490966 + } +, + { + "input_utc": "2026-05-01T00:00:00Z", + "right_ascension": 208.849626532, + "declination": -16.256039871, + "ecliptic_longitude": 212.5315932, + "ecliptic_latitude": -4.1622995 + } +, + { + "input_utc": "2026-08-29T00:00:00Z", + "right_ascension": 346.062573698, + "declination": -4.416718092, + "ecliptic_longitude": 345.4608613, + "ecliptic_latitude": 1.4247855 + } +, + { + "input_utc": "2026-12-27T00:00:00Z", + "right_ascension": 139.195692903, + "declination": 16.272137989, + "ecliptic_longitude": 136.6058459, + "ecliptic_latitude": 0.4338603 + } + +] diff --git a/basic/testdata/moon_planet_conjunction_baseline.json b/basic/testdata/moon_planet_conjunction_baseline.json new file mode 100644 index 0000000..530a930 --- /dev/null +++ b/basic/testdata/moon_planet_conjunction_baseline.json @@ -0,0 +1,94 @@ +{ + "samples": [ + {"planet":"mercury","year":2026,"month":1,"time_utc":"2026-01-18T15:06:38Z"}, + {"planet":"mercury","year":2026,"month":2,"time_utc":"2026-02-18T23:02:34Z"}, + {"planet":"mercury","year":2026,"month":3,"time_utc":"2026-03-17T14:07:16Z"}, + {"planet":"mercury","year":2026,"month":4,"time_utc":"2026-04-15T19:11:09Z"}, + {"planet":"mercury","year":2026,"month":5,"time_utc":"2026-05-17T02:50:17Z"}, + {"planet":"mercury","year":2026,"month":6,"time_utc":"2026-06-16T19:32:07Z"}, + {"planet":"mercury","year":2026,"month":7,"time_utc":"2026-07-14T04:37:03Z"}, + {"planet":"mercury","year":2026,"month":8,"time_utc":"2026-08-11T12:47:22Z"}, + {"planet":"mercury","year":2026,"month":9,"time_utc":"2026-09-12T07:28:45Z"}, + {"planet":"mercury","year":2026,"month":10,"time_utc":"2026-10-12T20:08:25Z"}, + {"planet":"mercury","year":2026,"month":11,"time_utc":"2026-11-08T16:33:09Z"}, + {"planet":"mercury","year":2026,"month":12,"time_utc":"2026-12-07T22:03:14Z"}, + {"planet":"venus","year":2026,"month":1,"time_utc":"2026-01-19T01:01:00Z"}, + {"planet":"venus","year":2026,"month":2,"time_utc":"2026-02-18T09:20:04Z"}, + {"planet":"venus","year":2026,"month":3,"time_utc":"2026-03-20T12:37:37Z"}, + {"planet":"venus","year":2026,"month":4,"time_utc":"2026-04-19T08:47:17Z"}, + {"planet":"venus","year":2026,"month":5,"time_utc":"2026-05-19T01:49:30Z"}, + {"planet":"venus","year":2026,"month":6,"time_utc":"2026-06-17T20:20:35Z"}, + {"planet":"venus","year":2026,"month":7,"time_utc":"2026-07-17T16:31:26Z"}, + {"planet":"venus","year":2026,"month":8,"time_utc":"2026-08-16T08:47:10Z"}, + {"planet":"venus","year":2026,"month":9,"time_utc":"2026-09-14T11:10:57Z"}, + {"planet":"venus","year":2026,"month":10,"time_utc":"2026-10-12T02:31:37Z"}, + {"planet":"venus","year":2026,"month":11,"time_utc":"2026-11-07T11:33:28Z"}, + {"planet":"venus","year":2026,"month":12,"time_utc":"2026-12-05T10:44:42Z"}, + {"planet":"mars","year":2026,"month":1,"time_utc":"2026-01-18T14:09:59Z"}, + {"planet":"mars","year":2026,"month":2,"time_utc":"2026-02-16T17:40:45Z"}, + {"planet":"mars","year":2026,"month":3,"time_utc":"2026-03-17T21:52:35Z"}, + {"planet":"mars","year":2026,"month":4,"time_utc":"2026-04-16T00:46:20Z"}, + {"planet":"mars","year":2026,"month":5,"time_utc":"2026-05-15T00:44:20Z"}, + {"planet":"mars","year":2026,"month":6,"time_utc":"2026-06-12T21:15:20Z"}, + {"planet":"mars","year":2026,"month":7,"time_utc":"2026-07-11T14:38:57Z"}, + {"planet":"mars","year":2026,"month":8,"time_utc":"2026-08-09T05:31:55Z"}, + {"planet":"mars","year":2026,"month":9,"time_utc":"2026-09-06T18:25:43Z"}, + {"planet":"mars","year":2026,"month":10,"time_utc":"2026-10-05T05:31:58Z"}, + {"planet":"mars","year":2026,"month":11,"time_utc":"2026-11-02T14:25:02Z"}, + {"planet":"mars","year":2026,"month":11,"time_utc":"2026-11-30T19:34:48Z"}, + {"planet":"mars","year":2026,"month":12,"time_utc":"2026-12-28T17:44:28Z"}, + {"planet":"jupiter","year":2026,"month":1,"time_utc":"2026-01-03T21:59:20Z"}, + {"planet":"jupiter","year":2026,"month":1,"time_utc":"2026-01-31T02:29:07Z"}, + {"planet":"jupiter","year":2026,"month":2,"time_utc":"2026-02-27T06:24:21Z"}, + {"planet":"jupiter","year":2026,"month":3,"time_utc":"2026-03-26T12:11:13Z"}, + {"planet":"jupiter","year":2026,"month":4,"time_utc":"2026-04-22T22:03:39Z"}, + {"planet":"jupiter","year":2026,"month":5,"time_utc":"2026-05-20T12:36:55Z"}, + {"planet":"jupiter","year":2026,"month":6,"time_utc":"2026-06-17T06:51:28Z"}, + {"planet":"jupiter","year":2026,"month":7,"time_utc":"2026-07-15T03:03:33Z"}, + {"planet":"jupiter","year":2026,"month":8,"time_utc":"2026-08-11T23:22:55Z"}, + {"planet":"jupiter","year":2026,"month":9,"time_utc":"2026-09-08T18:11:18Z"}, + {"planet":"jupiter","year":2026,"month":10,"time_utc":"2026-10-06T10:16:20Z"}, + {"planet":"jupiter","year":2026,"month":11,"time_utc":"2026-11-02T23:09:31Z"}, + {"planet":"jupiter","year":2026,"month":11,"time_utc":"2026-11-30T09:16:19Z"}, + {"planet":"jupiter","year":2026,"month":12,"time_utc":"2026-12-27T17:30:35Z"}, + {"planet":"saturn","year":2026,"month":1,"time_utc":"2026-01-23T12:40:10Z"}, + {"planet":"saturn","year":2026,"month":2,"time_utc":"2026-02-20T00:03:21Z"}, + {"planet":"saturn","year":2026,"month":3,"time_utc":"2026-03-19T14:12:20Z"}, + {"planet":"saturn","year":2026,"month":4,"time_utc":"2026-04-16T06:08:13Z"}, + {"planet":"saturn","year":2026,"month":5,"time_utc":"2026-05-13T21:58:07Z"}, + {"planet":"saturn","year":2026,"month":6,"time_utc":"2026-06-10T11:41:01Z"}, + {"planet":"saturn","year":2026,"month":7,"time_utc":"2026-07-07T21:49:36Z"}, + {"planet":"saturn","year":2026,"month":8,"time_utc":"2026-08-04T04:10:47Z"}, + {"planet":"saturn","year":2026,"month":8,"time_utc":"2026-08-31T08:07:26Z"}, + {"planet":"saturn","year":2026,"month":9,"time_utc":"2026-09-27T12:00:54Z"}, + {"planet":"saturn","year":2026,"month":10,"time_utc":"2026-10-24T17:41:49Z"}, + {"planet":"saturn","year":2026,"month":11,"time_utc":"2026-11-21T01:26:46Z"}, + {"planet":"saturn","year":2026,"month":12,"time_utc":"2026-12-18T10:18:35Z"}, + {"planet":"uranus","year":2026,"month":1,"time_utc":"2026-01-27T18:46:51Z"}, + {"planet":"uranus","year":2026,"month":2,"time_utc":"2026-02-24T00:35:51Z"}, + {"planet":"uranus","year":2026,"month":3,"time_utc":"2026-03-23T07:40:49Z"}, + {"planet":"uranus","year":2026,"month":4,"time_utc":"2026-04-19T17:35:45Z"}, + {"planet":"uranus","year":2026,"month":5,"time_utc":"2026-05-17T05:58:41Z"}, + {"planet":"uranus","year":2026,"month":6,"time_utc":"2026-06-13T19:08:48Z"}, + {"planet":"uranus","year":2026,"month":7,"time_utc":"2026-07-11T07:07:53Z"}, + {"planet":"uranus","year":2026,"month":8,"time_utc":"2026-08-07T16:30:03Z"}, + {"planet":"uranus","year":2026,"month":9,"time_utc":"2026-09-03T23:05:03Z"}, + {"planet":"uranus","year":2026,"month":10,"time_utc":"2026-10-01T04:18:41Z"}, + {"planet":"uranus","year":2026,"month":10,"time_utc":"2026-10-28T10:24:46Z"}, + {"planet":"uranus","year":2026,"month":11,"time_utc":"2026-11-24T18:39:41Z"}, + {"planet":"uranus","year":2026,"month":12,"time_utc":"2026-12-22T04:19:54Z"}, + {"planet":"neptune","year":2026,"month":1,"time_utc":"2026-01-23T15:49:28Z"}, + {"planet":"neptune","year":2026,"month":2,"time_utc":"2026-02-19T23:30:09Z"}, + {"planet":"neptune","year":2026,"month":3,"time_utc":"2026-03-19T09:34:27Z"}, + {"planet":"neptune","year":2026,"month":4,"time_utc":"2026-04-15T21:23:14Z"}, + {"planet":"neptune","year":2026,"month":5,"time_utc":"2026-05-13T09:11:35Z"}, + {"planet":"neptune","year":2026,"month":6,"time_utc":"2026-06-09T19:13:52Z"}, + {"planet":"neptune","year":2026,"month":7,"time_utc":"2026-07-07T02:37:58Z"}, + {"planet":"neptune","year":2026,"month":8,"time_utc":"2026-08-03T07:56:14Z"}, + {"planet":"neptune","year":2026,"month":8,"time_utc":"2026-08-30T12:50:11Z"}, + {"planet":"neptune","year":2026,"month":9,"time_utc":"2026-09-26T19:04:28Z"}, + {"planet":"neptune","year":2026,"month":10,"time_utc":"2026-10-24T03:16:37Z"}, + {"planet":"neptune","year":2026,"month":11,"time_utc":"2026-11-20T12:35:40Z"}, + {"planet":"neptune","year":2026,"month":12,"time_utc":"2026-12-17T21:26:40Z"} + ] +} diff --git a/basic/testdata/moon_planet_conjunction_baseline_samples.json b/basic/testdata/moon_planet_conjunction_baseline_samples.json new file mode 100644 index 0000000..2a10c2a --- /dev/null +++ b/basic/testdata/moon_planet_conjunction_baseline_samples.json @@ -0,0 +1,92 @@ +{ + "samples": [ + {"planet":"mercury","year":1996,"month":1,"time_utc":"1996-01-20T07:48:44Z"}, + {"planet":"mercury","year":1996,"month":2,"time_utc":"1996-02-17T05:58:27Z"}, + {"planet":"mercury","year":1996,"month":3,"time_utc":"1996-03-18T21:54:14Z"}, + {"planet":"mercury","year":1996,"month":4,"time_utc":"1996-04-19T10:29:52Z"}, + {"planet":"mercury","year":1996,"month":5,"time_utc":"1996-05-17T04:02:30Z"}, + {"planet":"mercury","year":1996,"month":6,"time_utc":"1996-06-14T00:19:49Z"}, + {"planet":"mercury","year":1996,"month":7,"time_utc":"1996-07-16T08:03:39Z"}, + {"planet":"mercury","year":1996,"month":8,"time_utc":"1996-08-16T18:24:11Z"}, + {"planet":"mercury","year":1996,"month":9,"time_utc":"1996-09-13T13:17:46Z"}, + {"planet":"mercury","year":1996,"month":10,"time_utc":"1996-10-11T09:44:59Z"}, + {"planet":"mercury","year":1996,"month":11,"time_utc":"1996-11-11T12:53:54Z"}, + {"planet":"mercury","year":1996,"month":12,"time_utc":"1996-12-12T05:10:41Z"}, + {"planet":"venus","year":1996,"month":1,"time_utc":"1996-01-23T08:25:31Z"}, + {"planet":"venus","year":1996,"month":2,"time_utc":"1996-02-22T04:36:35Z"}, + {"planet":"venus","year":1996,"month":3,"time_utc":"1996-03-22T23:53:11Z"}, + {"planet":"venus","year":1996,"month":4,"time_utc":"1996-04-21T14:13:39Z"}, + {"planet":"venus","year":1996,"month":5,"time_utc":"1996-05-20T00:40:14Z"}, + {"planet":"venus","year":1996,"month":6,"time_utc":"1996-06-15T09:13:17Z"}, + {"planet":"venus","year":1996,"month":7,"time_utc":"1996-07-12T08:38:52Z"}, + {"planet":"venus","year":1996,"month":8,"time_utc":"1996-08-10T03:59:30Z"}, + {"planet":"venus","year":1996,"month":9,"time_utc":"1996-09-08T23:10:01Z"}, + {"planet":"venus","year":1996,"month":10,"time_utc":"1996-10-09T04:07:49Z"}, + {"planet":"venus","year":1996,"month":11,"time_utc":"1996-11-08T09:31:53Z"}, + {"planet":"venus","year":1996,"month":12,"time_utc":"1996-12-08T13:14:51Z"}, + {"planet":"mars","year":1996,"month":1,"time_utc":"1996-01-21T07:43:41Z"}, + {"planet":"mars","year":1996,"month":2,"time_utc":"1996-02-19T07:57:56Z"}, + {"planet":"mars","year":1996,"month":3,"time_utc":"1996-03-19T07:08:23Z"}, + {"planet":"mars","year":1996,"month":4,"time_utc":"1996-04-17T05:16:03Z"}, + {"planet":"mars","year":1996,"month":5,"time_utc":"1996-05-16T02:56:01Z"}, + {"planet":"mars","year":1996,"month":6,"time_utc":"1996-06-14T00:47:52Z"}, + {"planet":"mars","year":1996,"month":7,"time_utc":"1996-07-12T23:06:35Z"}, + {"planet":"mars","year":1996,"month":8,"time_utc":"1996-08-10T21:29:02Z"}, + {"planet":"mars","year":1996,"month":9,"time_utc":"1996-09-08T19:05:05Z"}, + {"planet":"mars","year":1996,"month":10,"time_utc":"1996-10-07T14:58:13Z"}, + {"planet":"mars","year":1996,"month":11,"time_utc":"1996-11-05T08:08:58Z"}, + {"planet":"mars","year":1996,"month":12,"time_utc":"1996-12-03T21:11:34Z"}, + {"planet":"jupiter","year":1996,"month":1,"time_utc":"1996-01-18T19:46:08Z"}, + {"planet":"jupiter","year":1996,"month":2,"time_utc":"1996-02-15T15:00:36Z"}, + {"planet":"jupiter","year":1996,"month":3,"time_utc":"1996-03-14T06:11:13Z"}, + {"planet":"jupiter","year":1996,"month":4,"time_utc":"1996-04-10T16:55:53Z"}, + {"planet":"jupiter","year":1996,"month":5,"time_utc":"1996-05-08T00:11:28Z"}, + {"planet":"jupiter","year":1996,"month":6,"time_utc":"1996-06-04T05:31:30Z"}, + {"planet":"jupiter","year":1996,"month":7,"time_utc":"1996-07-01T10:21:43Z"}, + {"planet":"jupiter","year":1996,"month":7,"time_utc":"1996-07-28T15:41:14Z"}, + {"planet":"jupiter","year":1996,"month":8,"time_utc":"1996-08-24T22:04:24Z"}, + {"planet":"jupiter","year":1996,"month":9,"time_utc":"1996-09-21T05:58:04Z"}, + {"planet":"jupiter","year":1996,"month":10,"time_utc":"1996-10-18T16:05:44Z"}, + {"planet":"jupiter","year":1996,"month":11,"time_utc":"1996-11-15T05:27:06Z"}, + {"planet":"jupiter","year":1996,"month":12,"time_utc":"1996-12-12T22:38:11Z"}, + {"planet":"saturn","year":1996,"month":1,"time_utc":"1996-01-24T03:47:15Z"}, + {"planet":"saturn","year":1996,"month":2,"time_utc":"1996-02-20T19:10:42Z"}, + {"planet":"saturn","year":1996,"month":3,"time_utc":"1996-03-19T10:59:03Z"}, + {"planet":"saturn","year":1996,"month":4,"time_utc":"1996-04-16T01:04:52Z"}, + {"planet":"saturn","year":1996,"month":5,"time_utc":"1996-05-13T12:31:53Z"}, + {"planet":"saturn","year":1996,"month":6,"time_utc":"1996-06-09T21:39:17Z"}, + {"planet":"saturn","year":1996,"month":7,"time_utc":"1996-07-07T05:32:21Z"}, + {"planet":"saturn","year":1996,"month":8,"time_utc":"1996-08-03T13:13:36Z"}, + {"planet":"saturn","year":1996,"month":8,"time_utc":"1996-08-30T21:01:02Z"}, + {"planet":"saturn","year":1996,"month":9,"time_utc":"1996-09-27T04:20:31Z"}, + {"planet":"saturn","year":1996,"month":10,"time_utc":"1996-10-24T10:21:42Z"}, + {"planet":"saturn","year":1996,"month":11,"time_utc":"1996-11-20T15:05:45Z"}, + {"planet":"saturn","year":1996,"month":12,"time_utc":"1996-12-17T20:17:06Z"}, + {"planet":"uranus","year":1996,"month":1,"time_utc":"1996-01-20T15:54:24Z"}, + {"planet":"uranus","year":1996,"month":2,"time_utc":"1996-02-17T05:20:18Z"}, + {"planet":"uranus","year":1996,"month":3,"time_utc":"1996-03-15T16:05:29Z"}, + {"planet":"uranus","year":1996,"month":4,"time_utc":"1996-04-11T23:42:31Z"}, + {"planet":"uranus","year":1996,"month":5,"time_utc":"1996-05-09T05:36:22Z"}, + {"planet":"uranus","year":1996,"month":6,"time_utc":"1996-06-05T11:50:19Z"}, + {"planet":"uranus","year":1996,"month":7,"time_utc":"1996-07-02T19:33:47Z"}, + {"planet":"uranus","year":1996,"month":7,"time_utc":"1996-07-30T04:29:08Z"}, + {"planet":"uranus","year":1996,"month":8,"time_utc":"1996-08-26T13:21:06Z"}, + {"planet":"uranus","year":1996,"month":9,"time_utc":"1996-09-22T20:54:56Z"}, + {"planet":"uranus","year":1996,"month":10,"time_utc":"1996-10-20T03:02:32Z"}, + {"planet":"uranus","year":1996,"month":11,"time_utc":"1996-11-16T09:17:15Z"}, + {"planet":"uranus","year":1996,"month":12,"time_utc":"1996-12-13T17:54:51Z"}, + {"planet":"neptune","year":1996,"month":1,"time_utc":"1996-01-20T07:21:06Z"}, + {"planet":"neptune","year":1996,"month":2,"time_utc":"1996-02-16T19:38:46Z"}, + {"planet":"neptune","year":1996,"month":3,"time_utc":"1996-03-15T05:08:50Z"}, + {"planet":"neptune","year":1996,"month":4,"time_utc":"1996-04-11T11:48:33Z"}, + {"planet":"neptune","year":1996,"month":5,"time_utc":"1996-05-08T17:24:20Z"}, + {"planet":"neptune","year":1996,"month":6,"time_utc":"1996-06-04T23:59:10Z"}, + {"planet":"neptune","year":1996,"month":7,"time_utc":"1996-07-02T08:22:46Z"}, + {"planet":"neptune","year":1996,"month":7,"time_utc":"1996-07-29T17:55:53Z"}, + {"planet":"neptune","year":1996,"month":8,"time_utc":"1996-08-26T03:10:20Z"}, + {"planet":"neptune","year":1996,"month":9,"time_utc":"1996-09-22T10:48:58Z"}, + {"planet":"neptune","year":1996,"month":10,"time_utc":"1996-10-19T16:49:59Z"}, + {"planet":"neptune","year":1996,"month":11,"time_utc":"1996-11-15T22:54:41Z"}, + {"planet":"neptune","year":1996,"month":12,"time_utc":"1996-12-13T07:18:13Z"} + ] +} diff --git a/event_boundary_public_test.go b/event_boundary_public_test.go index 0a4f450..539ae89 100644 --- a/event_boundary_public_test.go +++ b/event_boundary_public_test.go @@ -9,6 +9,7 @@ import ( "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/uranus" @@ -88,6 +89,56 @@ func TestPublicPlanetEventBoundaryIncludesCurrent(t *testing.T) { } } +func TestPublicMoonPlanetConjunctionBoundaryIncludesCurrent(t *testing.T) { + type eventFuncs struct { + last func(time.Time) time.Time + next func(time.Time) time.Time + } + + cases := []struct { + name string + eventUT float64 + funcs eventFuncs + }{ + {name: "MoonMercuryConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionMercury), funcs: eventFuncs{ + last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionMercury) }, + next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionMercury) }, + }}, + {name: "MoonVenusConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionVenus), funcs: eventFuncs{ + last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionVenus) }, + next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionVenus) }, + }}, + {name: "MoonMarsConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionMars), funcs: eventFuncs{ + last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionMars) }, + next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionMars) }, + }}, + {name: "MoonJupiterConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionJupiter), funcs: eventFuncs{ + last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionJupiter) }, + next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionJupiter) }, + }}, + {name: "MoonSaturnConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionSaturn), funcs: eventFuncs{ + last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionSaturn) }, + next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionSaturn) }, + }}, + {name: "MoonUranusConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionUranus), funcs: eventFuncs{ + last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionUranus) }, + next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionUranus) }, + }}, + {name: "MoonNeptuneConjunction", eventUT: basic.NextMoonPlanetConjunction(eventBoundaryTT(2026), basic.MoonPlanetConjunctionNeptune), funcs: eventFuncs{ + last: func(date time.Time) time.Time { return moon.LastConjunctionWithPlanet(date, moon.ConjunctionNeptune) }, + next: func(date time.Time) time.Time { return moon.NextConjunctionWithPlanet(date, moon.ConjunctionNeptune) }, + }}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + eventTime := basic.JDE2DateByZone(tc.eventUT, time.UTC, false) + assertSameEventTime(t, "last", tc.funcs.last(eventTime), eventTime) + assertSameEventTime(t, "next", tc.funcs.next(eventTime), eventTime) + }) + } +} + func eventBoundaryTT(year int) float64 { return basic.TD2UT(basic.Date2JDE(time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC)), true) } diff --git a/moon/conjunction.go b/moon/conjunction.go new file mode 100644 index 0000000..02cd2a9 --- /dev/null +++ b/moon/conjunction.go @@ -0,0 +1,72 @@ +package moon + +import ( + "time" + + "b612.me/astro/basic" +) + +// ConjunctionPlanet 月球合月目标行星 / target planet for Moon-planet conjunction. +type ConjunctionPlanet string + +const ( + ConjunctionMercury ConjunctionPlanet = "mercury" + ConjunctionVenus ConjunctionPlanet = "venus" + ConjunctionMars ConjunctionPlanet = "mars" + ConjunctionJupiter ConjunctionPlanet = "jupiter" + ConjunctionSaturn ConjunctionPlanet = "saturn" + ConjunctionUranus ConjunctionPlanet = "uranus" + ConjunctionNeptune ConjunctionPlanet = "neptune" +) + +func conjunctionPlanetToBasic(planet ConjunctionPlanet) basic.MoonPlanetConjunctionPlanet { + switch planet { + case ConjunctionMercury: + return basic.MoonPlanetConjunctionMercury + case ConjunctionVenus: + return basic.MoonPlanetConjunctionVenus + case ConjunctionMars: + return basic.MoonPlanetConjunctionMars + case ConjunctionJupiter: + return basic.MoonPlanetConjunctionJupiter + case ConjunctionSaturn: + return basic.MoonPlanetConjunctionSaturn + case ConjunctionUranus: + return basic.MoonPlanetConjunctionUranus + case ConjunctionNeptune: + return basic.MoonPlanetConjunctionNeptune + default: + return 0 + } +} + +func validConjunctionPlanet(planet ConjunctionPlanet) bool { + return conjunctionPlanetToBasic(planet) != 0 +} + +// LastConjunctionWithPlanet 上一次行星合月(赤经合) / previous Moon-planet conjunction. +func LastConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time { + if !validConjunctionPlanet(planet) { + return time.Time{} + } + jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true) + return basic.JDE2DateByZone(basic.LastMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false) +} + +// NextConjunctionWithPlanet 下一次行星合月(赤经合) / next Moon-planet conjunction. +func NextConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time { + if !validConjunctionPlanet(planet) { + return time.Time{} + } + jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true) + return basic.JDE2DateByZone(basic.NextMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false) +} + +// ClosestConjunctionWithPlanet 最近一次行星合月(赤经合) / closest Moon-planet conjunction. +func ClosestConjunctionWithPlanet(date time.Time, planet ConjunctionPlanet) time.Time { + if !validConjunctionPlanet(planet) { + return time.Time{} + } + jde := basic.TD2UT(basic.Date2JDE(date.UTC()), true) + return basic.JDE2DateByZone(basic.ClosestMoonPlanetConjunction(jde, conjunctionPlanetToBasic(planet)), date.Location(), false) +} diff --git a/moon/conjunction_test.go b/moon/conjunction_test.go new file mode 100644 index 0000000..d81d8f7 --- /dev/null +++ b/moon/conjunction_test.go @@ -0,0 +1,82 @@ +package moon + +import ( + "math" + "testing" + "time" + + "b612.me/astro/basic" +) + +func TestConjunctionPlanetWrappersMatchBasic(t *testing.T) { + loc := time.FixedZone("CST", 8*3600) + query := time.Date(2026, 1, 15, 20, 0, 0, 0, loc) + queryTT := basic.TD2UT(basic.Date2JDE(query.UTC()), true) + + cases := []struct { + name string + planet ConjunctionPlanet + basic basic.MoonPlanetConjunctionPlanet + }{ + {name: "Mercury", planet: ConjunctionMercury, basic: basic.MoonPlanetConjunctionMercury}, + {name: "Venus", planet: ConjunctionVenus, basic: basic.MoonPlanetConjunctionVenus}, + {name: "Mars", planet: ConjunctionMars, basic: basic.MoonPlanetConjunctionMars}, + {name: "Jupiter", planet: ConjunctionJupiter, basic: basic.MoonPlanetConjunctionJupiter}, + {name: "Saturn", planet: ConjunctionSaturn, basic: basic.MoonPlanetConjunctionSaturn}, + {name: "Uranus", planet: ConjunctionUranus, basic: basic.MoonPlanetConjunctionUranus}, + {name: "Neptune", planet: ConjunctionNeptune, basic: basic.MoonPlanetConjunctionNeptune}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assertSameConjunctionTime(t, "last", LastConjunctionWithPlanet(query, tc.planet), basic.LastMoonPlanetConjunction(queryTT, tc.basic), loc) + assertSameConjunctionTime(t, "next", NextConjunctionWithPlanet(query, tc.planet), basic.NextMoonPlanetConjunction(queryTT, tc.basic), loc) + assertSameConjunctionTime(t, "closest", ClosestConjunctionWithPlanet(query, tc.planet), basic.ClosestMoonPlanetConjunction(queryTT, tc.basic), loc) + }) + } +} + +func assertSameConjunctionTime(t *testing.T, name string, got time.Time, wantJDE float64, loc *time.Location) { + t.Helper() + want := basic.JDE2DateByZone(wantJDE, loc, false) + if got.Location() != loc { + t.Fatalf("%s location mismatch: got %q want %q", name, got.Location().String(), loc.String()) + } + if !got.Equal(want) { + t.Fatalf("%s time mismatch: got %s want %s", name, got.Format(time.RFC3339Nano), want.Format(time.RFC3339Nano)) + } +} + +func TestClosestConjunctionReturnsNearestCandidate(t *testing.T) { + query := time.Date(2026, 1, 15, 12, 0, 0, 0, time.UTC) + last := LastConjunctionWithPlanet(query, ConjunctionMercury) + next := NextConjunctionWithPlanet(query, ConjunctionMercury) + got := ClosestConjunctionWithPlanet(query, ConjunctionMercury) + + lastDiff := math.Abs(query.Sub(last).Seconds()) + nextDiff := math.Abs(next.Sub(query).Seconds()) + if lastDiff <= nextDiff { + if !got.Equal(last) { + t.Fatalf("closest should match last: got %s want %s", got.Format(time.RFC3339Nano), last.Format(time.RFC3339Nano)) + } + return + } + if !got.Equal(next) { + t.Fatalf("closest should match next: got %s want %s", got.Format(time.RFC3339Nano), next.Format(time.RFC3339Nano)) + } +} + +func TestInvalidConjunctionPlanetReturnsZeroTime(t *testing.T) { + query := time.Date(2026, 1, 15, 12, 0, 0, 0, time.FixedZone("CST", 8*3600)) + invalid := ConjunctionPlanet("pluto") + + for name, fn := range map[string]func(time.Time, ConjunctionPlanet) time.Time{ + "last": LastConjunctionWithPlanet, + "next": NextConjunctionWithPlanet, + "closest": ClosestConjunctionWithPlanet, + } { + if got := fn(query, invalid); !got.IsZero() { + t.Fatalf("%s should return zero time for invalid planet, got %s", name, got.Format(time.RFC3339Nano)) + } + } +} diff --git a/moon/geocentric_apparent_test.go b/moon/geocentric_apparent_test.go new file mode 100644 index 0000000..8ad1708 --- /dev/null +++ b/moon/geocentric_apparent_test.go @@ -0,0 +1,43 @@ +package moon + +import ( + "math" + "testing" + "time" + + "b612.me/astro/basic" +) + +func TestGeocentricApparentRaDecComponentsMatch(t *testing.T) { + date := time.Date(2026, 1, 1, 6, 0, 0, 0, time.UTC) + + ra, dec := GeocentricApparentRaDec(date) + if diff := math.Abs(ra - GeocentricApparentRa(date)); diff > 1e-12 { + t.Fatalf("RA pair mismatch: got %.15f want %.15f", ra, GeocentricApparentRa(date)) + } + if diff := math.Abs(dec - GeocentricApparentDec(date)); diff > 1e-12 { + t.Fatalf("Dec pair mismatch: got %.15f want %.15f", dec, GeocentricApparentDec(date)) + } +} + +func TestGeocentricApparentRaDecDiffersFromTopocentricAtSite(t *testing.T) { + date := time.Date(2026, 1, 1, 6, 0, 0, 0, time.FixedZone("CST", 8*3600)) + + geoRA, geoDec := GeocentricApparentRaDec(date) + topoRA, topoDec := ApparentRaDec(date, 121.4737, 31.2304) + + if math.Abs(geoRA-topoRA) < 1e-6 && math.Abs(geoDec-topoDec) < 1e-6 { + t.Fatalf("geocentric apparent RA/Dec unexpectedly matches topocentric values: geo=(%.12f, %.12f) topo=(%.12f, %.12f)", + geoRA, geoDec, topoRA, topoDec) + } +} + +func TestTrueRaDecUsesBasicGeocentricTrue(t *testing.T) { + date := time.Date(2026, 1, 1, 6, 0, 0, 0, time.UTC) + + wantRA, wantDec := basic.HMoonGeocentricTrueRaDec(basic.TD2UT(basic.Date2JDE(date.UTC()), true)) + gotRA, gotDec := TrueRaDec(date) + if math.Abs(gotRA-wantRA) > 1e-12 || math.Abs(gotDec-wantDec) > 1e-12 { + t.Fatalf("TrueRaDec mismatch: got (%.15f, %.15f) want (%.15f, %.15f)", gotRA, gotDec, wantRA, wantDec) + } +} diff --git a/moon/moon.go b/moon/moon.go index 83df684..71a003e 100644 --- a/moon/moon.go +++ b/moon/moon.go @@ -84,7 +84,7 @@ func ApparentLo(date time.Time) float64 { // Returns the Moon's geocentric true right ascension at the instant represented by date, in degrees. func TrueRa(date time.Time) float64 { jde := basic.Date2JDE(date.UTC()) - return basic.HMoonTrueRa(basic.TD2UT(jde, true)) + return basic.HMoonGeocentricTrueRa(basic.TD2UT(jde, true)) } // TrueDec 月亮地心真赤纬 / true geocentric declination. @@ -93,7 +93,7 @@ func TrueRa(date time.Time) float64 { // Returns the Moon's geocentric true declination at the instant represented by date, in degrees. func TrueDec(date time.Time) float64 { jde := basic.Date2JDE(date.UTC()) - return basic.HMoonTrueDec(basic.TD2UT(jde, true)) + return basic.HMoonGeocentricTrueDec(basic.TD2UT(jde, true)) } // TrueRaDec 月亮地心真赤经、真赤纬 / true geocentric right ascension and declination. @@ -102,7 +102,34 @@ func TrueDec(date time.Time) float64 { // Returns the Moon's geocentric true right ascension and declination at the instant represented by date, in degrees. func TrueRaDec(date time.Time) (float64, float64) { jde := basic.Date2JDE(date.UTC()) - return basic.HMoonTrueRaDec(basic.TD2UT(jde, true)) + return basic.HMoonGeocentricTrueRaDec(basic.TD2UT(jde, true)) +} + +// GeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension. +// +// 返回月亮在 date 对应绝对时刻的地心视赤经,单位度。 +// Returns the Moon's apparent geocentric right ascension at the instant represented by date, in degrees. +func GeocentricApparentRa(date time.Time) float64 { + jde := basic.Date2JDE(date.UTC()) + return basic.HMoonGeocentricApparentRa(basic.TD2UT(jde, true)) +} + +// GeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination. +// +// 返回月亮在 date 对应绝对时刻的地心视赤纬,单位度。 +// Returns the Moon's apparent geocentric declination at the instant represented by date, in degrees. +func GeocentricApparentDec(date time.Time) float64 { + jde := basic.Date2JDE(date.UTC()) + return basic.HMoonGeocentricApparentDec(basic.TD2UT(jde, true)) +} + +// GeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination. +// +// 返回月亮在 date 对应绝对时刻的地心视赤经与视赤纬,单位度。 +// Returns the Moon's apparent geocentric right ascension and declination at the instant represented by date, in degrees. +func GeocentricApparentRaDec(date time.Time) (float64, float64) { + jde := basic.Date2JDE(date.UTC()) + return basic.HMoonGeocentricApparentRaDec(basic.TD2UT(jde, true)) } // ApparentRa 月亮站心视赤经 / apparent topocentric right ascension.