2026-09-17 12:27:40 +08:00
|
|
|
|
package basic
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"math"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
. "b612.me/astro/tools"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 本文件是「行星相位事件(留/合/冲/方照)」的顺序不变量回归。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 背景:类型化「留」的结果必须满足
|
|
|
|
|
|
// Next*(q) >= q - tol、Last*(q) <= q + tol、返回的是真值事件、且不得跳过更近的同类型事件。
|
|
|
|
|
|
// 2026-09 的 review 发现水星类型化「留」会跳过一个下合,导致 Last* 返回未来
|
|
|
|
|
|
// (例如查询 TT 2008-01-22 时 LastMercuryProgradeToRetrograde 返回 2008-01-28)。
|
|
|
|
|
|
// 真值来自对导出星历的独立二分扫描,不使用被测的 Next*/Last*,因此能独立复现该类缺陷。
|
|
|
|
|
|
|
|
|
|
|
|
const phaseInvariantToleranceDay = 0.6 / 86400.0 // 站事件同刻容差 0.5 s + 余量
|
|
|
|
|
|
|
|
|
|
|
|
type phaseTruthStation struct {
|
|
|
|
|
|
jd float64
|
|
|
|
|
|
p2r bool
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func phaseRate(ra func(float64) float64, jd float64) float64 {
|
|
|
|
|
|
sub := ra(jd+0.01) - ra(jd-0.01)
|
|
|
|
|
|
if sub > 180 {
|
|
|
|
|
|
sub -= 360
|
|
|
|
|
|
}
|
|
|
|
|
|
if sub < -180 {
|
|
|
|
|
|
sub += 360
|
|
|
|
|
|
}
|
|
|
|
|
|
return sub / 0.02
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func phaseTruthStations(ra func(float64) float64, jd0, jd1, step float64) []phaseTruthStation {
|
|
|
|
|
|
rate := func(jd float64) float64 { return phaseRate(ra, jd) }
|
|
|
|
|
|
var out []phaseTruthStation
|
|
|
|
|
|
prevJD, prev := jd0, rate(jd0)
|
|
|
|
|
|
for jd := jd0 + step; jd <= jd1; jd += step {
|
|
|
|
|
|
cur := rate(jd)
|
|
|
|
|
|
if prev*cur < 0 {
|
|
|
|
|
|
left, right := prevJD, jd
|
|
|
|
|
|
for i := 0; i < 100; i++ {
|
|
|
|
|
|
middle := (left + right) / 2
|
|
|
|
|
|
if middle == left || middle == right {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
if (prev < 0) == (rate(middle) < 0) {
|
|
|
|
|
|
left = middle
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
right = middle
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, phaseTruthStation{jd: (left + right) / 2, p2r: prev > 0})
|
|
|
|
|
|
}
|
|
|
|
|
|
prevJD, prev = jd, cur
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type phaseStationCase struct {
|
|
|
|
|
|
name string
|
|
|
|
|
|
ra func(float64) float64
|
|
|
|
|
|
nextP2R func(float64) float64
|
|
|
|
|
|
lastP2R func(float64) float64
|
|
|
|
|
|
nextR2P func(float64) float64
|
|
|
|
|
|
lastR2P func(float64) float64
|
|
|
|
|
|
step float64
|
|
|
|
|
|
// radius 真值扫描半径(天):必须覆盖「查询在站后几秒 ⇒ 答案是下一个同名站」的距离。
|
|
|
|
|
|
radius float64
|
|
|
|
|
|
// queryRadius 参与构造查询的站距中心的最大距离(天)。
|
|
|
|
|
|
queryRadius float64
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func phaseStationCases() []phaseStationCase {
|
|
|
|
|
|
return []phaseStationCase{
|
|
|
|
|
|
{"Mercury", MercuryApparentRa, NextMercuryProgradeToRetrograde, LastMercuryProgradeToRetrograde,
|
|
|
|
|
|
NextMercuryRetrogradeToPrograde, LastMercuryRetrogradeToPrograde, 0.25, 700, 450},
|
|
|
|
|
|
{"Mars", MarsApparentRa, NextMarsProgradeToRetrograde, LastMarsProgradeToRetrograde,
|
|
|
|
|
|
NextMarsRetrogradeToPrograde, LastMarsRetrogradeToPrograde, 0.5, 1100, 250},
|
|
|
|
|
|
{"Venus", VenusApparentRa, NextVenusProgradeToRetrograde, LastVenusProgradeToRetrograde,
|
|
|
|
|
|
NextVenusRetrogradeToPrograde, LastVenusRetrogradeToPrograde, 0.5, 900, 250},
|
|
|
|
|
|
{"Jupiter", JupiterApparentRa, NextJupiterProgradeToRetrograde, LastJupiterProgradeToRetrograde,
|
|
|
|
|
|
NextJupiterRetrogradeToPrograde, LastJupiterRetrogradeToPrograde, 1, 700, 250},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func phaseEpochTT(year int, month time.Month, day int) float64 {
|
2026-09-23 18:55:12 +08:00
|
|
|
|
return UTC2TT(Date2JD(time.Date(year, month, day, 0, 0, 0, 0, time.UTC)))
|
2026-09-17 12:27:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestPlanetStationOrderInvariant 在固定的历史失败时点附近逐点检查顺序不变量。
|
|
|
|
|
|
func TestPlanetStationOrderInvariant(t *testing.T) {
|
|
|
|
|
|
epochs := map[string][]time.Time{
|
|
|
|
|
|
"Mercury": {
|
|
|
|
|
|
time.Date(2008, 1, 22, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
time.Date(2006, 2, 23, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
time.Date(2007, 2, 6, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
time.Date(2007, 11, 8, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
time.Date(2003, 9, 27, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
time.Date(-209, 1, 14, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
time.Date(-208, 12, 11, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
},
|
|
|
|
|
|
"Mars": {time.Date(2003, 7, 30, 0, 0, 0, 0, time.UTC)},
|
|
|
|
|
|
"Venus": {time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC)},
|
|
|
|
|
|
"Jupiter": {time.Date(2003, 7, 30, 0, 0, 0, 0, time.UTC)},
|
|
|
|
|
|
}
|
|
|
|
|
|
offsets := []float64{0, 1, -1, 30, -30, 300, -300, 3600, -3600, 86400, -86400, 10 * 86400, -10 * 86400, 30 * 86400, -30 * 86400}
|
|
|
|
|
|
|
|
|
|
|
|
for _, tc := range phaseStationCases() {
|
|
|
|
|
|
for _, epoch := range epochs[tc.name] {
|
|
|
|
|
|
center := phaseEpochTT(epoch.Year(), epoch.Month(), epoch.Day())
|
|
|
|
|
|
stations := phaseTruthStations(tc.ra, center-tc.radius, center+tc.radius, tc.step)
|
|
|
|
|
|
if len(stations) == 0 {
|
|
|
|
|
|
t.Fatalf("%s: no truth stations near %s", tc.name, epoch.Format("2006-01-02"))
|
|
|
|
|
|
}
|
|
|
|
|
|
funcs := []struct {
|
|
|
|
|
|
label string
|
|
|
|
|
|
fn func(float64) float64
|
|
|
|
|
|
next bool
|
|
|
|
|
|
p2r bool
|
|
|
|
|
|
}{
|
|
|
|
|
|
{"NextP2R", tc.nextP2R, true, true},
|
|
|
|
|
|
{"LastP2R", tc.lastP2R, false, true},
|
|
|
|
|
|
{"NextR2P", tc.nextR2P, true, false},
|
|
|
|
|
|
{"LastR2P", tc.lastR2P, false, false},
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, f := range funcs {
|
|
|
|
|
|
var queries []float64
|
|
|
|
|
|
for _, st := range stations {
|
|
|
|
|
|
if st.p2r != f.p2r || math.Abs(st.jd-center) > tc.queryRadius {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, off := range offsets {
|
|
|
|
|
|
queries = append(queries, st.jd+off/86400.0)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, q := range queries {
|
|
|
|
|
|
gotUT := f.fn(q)
|
|
|
|
|
|
if math.IsNaN(gotUT) {
|
|
|
|
|
|
t.Fatalf("%s %s at %s returned NaN", tc.name, f.label,
|
2026-09-23 18:55:12 +08:00
|
|
|
|
JD2DateByZone(TT2UTC(q), time.UTC, false).Format("2006-01-02 15:04:05"))
|
2026-09-17 12:27:40 +08:00
|
|
|
|
}
|
2026-09-23 18:55:12 +08:00
|
|
|
|
got := UTC2TT(gotUT)
|
2026-09-17 12:27:40 +08:00
|
|
|
|
// 1) 顺序不变量
|
|
|
|
|
|
if f.next && got < q-phaseInvariantToleranceDay {
|
|
|
|
|
|
t.Fatalf("%s %s at %s returned past event %s", tc.name, f.label,
|
2026-09-23 18:55:12 +08:00
|
|
|
|
JD2DateByZone(TT2UTC(q), time.UTC, false).Format("2006-01-02 15:04:05"),
|
|
|
|
|
|
JD2DateByZone(gotUT, time.UTC, false).Format("2006-01-02 15:04:05"))
|
2026-09-17 12:27:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
if !f.next && got > q+phaseInvariantToleranceDay {
|
|
|
|
|
|
t.Fatalf("%s %s at %s returned future event %s", tc.name, f.label,
|
2026-09-23 18:55:12 +08:00
|
|
|
|
JD2DateByZone(TT2UTC(q), time.UTC, false).Format("2006-01-02 15:04:05"),
|
|
|
|
|
|
JD2DateByZone(gotUT, time.UTC, false).Format("2006-01-02 15:04:05"))
|
2026-09-17 12:27:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 2) 必须是真值事件
|
|
|
|
|
|
nearest, nearestDev := math.NaN(), math.Inf(1)
|
|
|
|
|
|
for _, st := range stations {
|
|
|
|
|
|
if st.p2r != f.p2r {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if dev := math.Abs(st.jd - got); dev < nearestDev {
|
|
|
|
|
|
nearest, nearestDev = st.jd, dev
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if nearestDev > 60.0/86400.0 {
|
|
|
|
|
|
t.Fatalf("%s %s at %s returned non-event %.6f (nearest truth %.3f d away)", tc.name, f.label,
|
2026-09-23 18:55:12 +08:00
|
|
|
|
JD2DateByZone(TT2UTC(q), time.UTC, false).Format("2006-01-02 15:04:05"), got, nearestDev)
|
2026-09-17 12:27:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
_ = nearest
|
|
|
|
|
|
// 3) 不得跳过更近的同类型事件
|
|
|
|
|
|
for _, st := range stations {
|
|
|
|
|
|
if st.p2r != f.p2r {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if f.next && st.jd > q+phaseInvariantToleranceDay && st.jd < got-60.0/86400.0 {
|
|
|
|
|
|
t.Fatalf("%s %s at %s skipped %s", tc.name, f.label,
|
2026-09-23 18:55:12 +08:00
|
|
|
|
JD2DateByZone(TT2UTC(q), time.UTC, false).Format("2006-01-02 15:04:05"),
|
|
|
|
|
|
JD2DateByZone(TT2UTC(st.jd), time.UTC, false).Format("2006-01-02 15:04:05"))
|
2026-09-17 12:27:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
if !f.next && st.jd < q-phaseInvariantToleranceDay && st.jd > got+60.0/86400.0 {
|
|
|
|
|
|
t.Fatalf("%s %s at %s skipped %s", tc.name, f.label,
|
2026-09-23 18:55:12 +08:00
|
|
|
|
JD2DateByZone(TT2UTC(q), time.UTC, false).Format("2006-01-02 15:04:05"),
|
|
|
|
|
|
JD2DateByZone(TT2UTC(st.jd), time.UTC, false).Format("2006-01-02 15:04:05"))
|
2026-09-17 12:27:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestMercuryConjunctionNeverSkips 检查水星「合」搜索不会跨过更近的合
|
|
|
|
|
|
// (历史缺陷:启发式跳 + 2 天走法会走满一个会合周期,跳过一次下合)。
|
|
|
|
|
|
func TestMercuryConjunctionNeverSkips(t *testing.T) {
|
|
|
|
|
|
delta := func(jd float64) float64 {
|
|
|
|
|
|
sub := Limit360(MercuryApparentLo(jd) - HSunApparentLo(jd))
|
|
|
|
|
|
if sub > 180 {
|
|
|
|
|
|
sub -= 360
|
|
|
|
|
|
}
|
|
|
|
|
|
if sub < -180 {
|
|
|
|
|
|
sub += 360
|
|
|
|
|
|
}
|
|
|
|
|
|
return sub
|
|
|
|
|
|
}
|
|
|
|
|
|
// 真值:局部细扫
|
|
|
|
|
|
truth := func(jd0, jd1, step float64) []float64 {
|
|
|
|
|
|
var out []float64
|
|
|
|
|
|
prevJD, prev := jd0, delta(jd0)
|
|
|
|
|
|
for jd := jd0 + step; jd <= jd1; jd += step {
|
|
|
|
|
|
cur := delta(jd)
|
|
|
|
|
|
if prev*cur < 0 {
|
|
|
|
|
|
left, right := prevJD, jd
|
|
|
|
|
|
for i := 0; i < 100; i++ {
|
|
|
|
|
|
middle := (left + right) / 2
|
|
|
|
|
|
if middle == left || middle == right {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
if (prev < 0) == (delta(middle) < 0) {
|
|
|
|
|
|
left = middle
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
right = middle
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, (left+right)/2)
|
|
|
|
|
|
}
|
|
|
|
|
|
prevJD, prev = jd, cur
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, epoch := range []time.Time{
|
|
|
|
|
|
time.Date(2008, 1, 22, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
time.Date(2006, 2, 23, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
time.Date(2007, 2, 6, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
time.Date(-209, 1, 14, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
|
} {
|
|
|
|
|
|
center := phaseEpochTT(epoch.Year(), epoch.Month(), epoch.Day())
|
|
|
|
|
|
truthEvents := truth(center-400, center+400, 0.25)
|
|
|
|
|
|
if len(truthEvents) < 5 {
|
|
|
|
|
|
t.Fatalf("truth scan too sparse near %s: %d", epoch.Format("2006-01-02"), len(truthEvents))
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, q := range []float64{center, center + 0.5, center + 12, center - 12, center + 60, center - 60} {
|
|
|
|
|
|
for _, next := range []uint8{0, 1} {
|
2026-09-23 18:55:12 +08:00
|
|
|
|
got := UTC2TT(mercuryConjunction(q, next))
|
2026-09-17 12:27:40 +08:00
|
|
|
|
if math.IsNaN(got) {
|
|
|
|
|
|
t.Fatalf("mercuryConjunction(%.6f, %d) = NaN", q, next)
|
|
|
|
|
|
}
|
|
|
|
|
|
want := math.NaN()
|
|
|
|
|
|
if next == 1 {
|
|
|
|
|
|
for _, e := range truthEvents {
|
|
|
|
|
|
if e >= q-0.1/86400.0 {
|
|
|
|
|
|
want = e
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for i := len(truthEvents) - 1; i >= 0; i-- {
|
|
|
|
|
|
if truthEvents[i] <= q+0.1/86400.0 {
|
|
|
|
|
|
want = truthEvents[i]
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if math.IsNaN(want) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if math.Abs(got-want) > 60.0/86400.0 {
|
|
|
|
|
|
t.Fatalf("mercuryConjunction(%.6f, %d) = %.6f want %.6f (%.3f d off) near %s",
|
|
|
|
|
|
q, next, got, want, got-want, epoch.Format("2006-01-02"))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestGreatestElongationNoSkip 检查大距事件不会跳过相邻的极大。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 口径说明:库内 Next*GreatestElongation 用的是「真距角」(mercuryTrueElongationN,
|
|
|
|
|
|
// 不含光行差/视位置),公开的 MercurySunElongation 是「视距角」,两者极大时刻相差约 10 分钟。
|
|
|
|
|
|
// 因此这里用视距角的极值作为参照,只做「不跳事件 / 顺序 / 是极值附近」的判定,
|
|
|
|
|
|
// 匹配容差取 30 分钟,足以覆盖口径差又远小于任何真实跳事件(数十天)。
|
|
|
|
|
|
func TestGreatestElongationNoSkip(t *testing.T) {
|
|
|
|
|
|
type elongCase struct {
|
|
|
|
|
|
name string
|
|
|
|
|
|
elongate func(float64) float64
|
|
|
|
|
|
next func(float64) float64
|
|
|
|
|
|
last func(float64) float64
|
|
|
|
|
|
}
|
|
|
|
|
|
cases := []elongCase{
|
|
|
|
|
|
{"Mercury", MercurySunElongation, NextMercuryGreatestElongation, LastMercuryGreatestElongation},
|
|
|
|
|
|
{"Venus", VenusSunElongation, NextVenusGreatestElongation, LastVenusGreatestElongation},
|
|
|
|
|
|
}
|
|
|
|
|
|
jd0 := phaseEpochTT(2024, 1, 1)
|
|
|
|
|
|
jd1 := phaseEpochTT(2029, 1, 1)
|
|
|
|
|
|
for _, tc := range cases {
|
|
|
|
|
|
// 真值:视距角的局部极大
|
|
|
|
|
|
var maxima []float64
|
|
|
|
|
|
prev, cur := tc.elongate(jd0), tc.elongate(jd0+0.5)
|
|
|
|
|
|
for jd := jd0 + 1.0; jd <= jd1; jd += 0.5 {
|
|
|
|
|
|
next := tc.elongate(jd)
|
|
|
|
|
|
if cur >= prev && cur >= next && (cur > prev || cur > next) {
|
|
|
|
|
|
left, right := jd-1.0, jd
|
|
|
|
|
|
for i := 0; i < 60; i++ {
|
|
|
|
|
|
third := (right - left) / 3
|
|
|
|
|
|
if tc.elongate(left+third) <= tc.elongate(right-third) {
|
|
|
|
|
|
left += third
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
right -= third
|
|
|
|
|
|
}
|
|
|
|
|
|
maxima = append(maxima, (left+right)/2)
|
|
|
|
|
|
}
|
|
|
|
|
|
prev, cur = cur, next
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(maxima) < 5 {
|
|
|
|
|
|
t.Fatalf("%s: too few elongation maxima (%d)", tc.name, len(maxima))
|
|
|
|
|
|
}
|
|
|
|
|
|
const matchTolerance = 30.0 / 1440.0
|
|
|
|
|
|
for i, maximum := range maxima {
|
|
|
|
|
|
// 查询落在极大前一天:Next 必须命中该极大(不得跳过)
|
2026-09-23 18:55:12 +08:00
|
|
|
|
next := UTC2TT(tc.next(maximum - 1))
|
2026-09-17 12:27:40 +08:00
|
|
|
|
if math.IsNaN(next) {
|
|
|
|
|
|
t.Fatalf("%s: Next at %s returned NaN", tc.name, tmpPhaseDate(maximum-1))
|
|
|
|
|
|
}
|
|
|
|
|
|
if dev := math.Abs(next - maximum); dev > matchTolerance {
|
|
|
|
|
|
t.Fatalf("%s: Next at %s = %s, expected the maximum %s (%.2f min off)",
|
|
|
|
|
|
tc.name, tmpPhaseDate(maximum-1), tmpPhaseDate(next), tmpPhaseDate(maximum), dev*1440)
|
|
|
|
|
|
}
|
|
|
|
|
|
if next < maximum-1-phaseInvariantToleranceDay {
|
|
|
|
|
|
t.Fatalf("%s: Next returned an event before the query", tc.name)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 查询落在极大后一天:Last 必须命中该极大,Next 必须命中下一个极大
|
2026-09-23 18:55:12 +08:00
|
|
|
|
last := UTC2TT(tc.last(maximum + 1))
|
2026-09-17 12:27:40 +08:00
|
|
|
|
if dev := math.Abs(last - maximum); dev > matchTolerance {
|
|
|
|
|
|
t.Fatalf("%s: Last at %s = %s, expected the previous maximum %s (%.2f min off)",
|
|
|
|
|
|
tc.name, tmpPhaseDate(maximum+1), tmpPhaseDate(last), tmpPhaseDate(maximum), dev*1440)
|
|
|
|
|
|
}
|
|
|
|
|
|
if last > maximum+1+phaseInvariantToleranceDay {
|
|
|
|
|
|
t.Fatalf("%s: Last returned an event after the query", tc.name)
|
|
|
|
|
|
}
|
|
|
|
|
|
if i+1 < len(maxima) {
|
2026-09-23 18:55:12 +08:00
|
|
|
|
following := UTC2TT(tc.next(maximum + 1))
|
2026-09-17 12:27:40 +08:00
|
|
|
|
if dev := math.Abs(following - maxima[i+1]); dev > matchTolerance {
|
|
|
|
|
|
t.Fatalf("%s: Next at %s = %s, expected the following maximum %s (%.2f min off)",
|
|
|
|
|
|
tc.name, tmpPhaseDate(maximum+1), tmpPhaseDate(following), tmpPhaseDate(maxima[i+1]), dev*1440)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func tmpPhaseDate(jd float64) string {
|
2026-09-23 18:55:12 +08:00
|
|
|
|
return JD2DateByZone(TT2UTC(jd), time.UTC, false).Format("2006-01-02 15:04")
|
2026-09-17 12:27:40 +08:00
|
|
|
|
}
|