Files
astro/basic/timescale_test.go
T
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00

715 lines
26 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package basic
import (
"math"
"testing"
)
// 尺度换算的锚定契约:闰秒表、月度实测 ΔT、恒等式 TT−UTC = ΔT + DUT1、边界与政策、钩子与世代号。
func resetTimeScaleState(t *testing.T) {
t.Helper()
SetTTMinusUTCFn(nil)
SetTimeScaleFuturePolicy(TimeScaleLeapSecond)
SetDeltaTFn(nil)
t.Cleanup(func() {
SetTTMinusUTCFn(nil)
SetTimeScaleFuturePolicy(TimeScaleLeapSecond)
SetDeltaTFn(nil)
})
}
func TestTimeScaleCalendarAnchors(t *testing.T) {
if got := JDCalc(1972, 1, 1); got != 2441317.5 {
t.Fatalf("JDCalc(1972,1,1)=%v, want 2441317.5", got)
}
if got := JDCalc(2026, 9, 1); got != timeScaleExactEndJDE {
t.Fatalf("JDCalc(2026,9,1)=%v, want %v", got, timeScaleExactEndJDE)
}
if got := deltaTMonthlyJDE[len(deltaTMonthlyJDE)-1]; got != timeScaleExactEndJDE {
t.Fatalf("月度表末项=%v, want 精确窗口末端 %v", got, timeScaleExactEndJDE)
}
if len(deltaTMonthlyJDE) != len(deltaTMonthlyObserved) {
t.Fatalf("月度表长度不一致: %d vs %d", len(deltaTMonthlyJDE), len(deltaTMonthlyObserved))
}
}
// 闰时进位量由 ΔT 决定,步长为一小时,校正后 DUT1 必须落回容限内。
func TestTimeScaleLeapHourPolicyCarriesFromDeltaT(t *testing.T) {
resetTimeScaleState(t)
SetTimeScaleFuturePolicy(TimeScaleLeapHour)
base := TTMinusUTCSeconds(timeScaleExactEndJDE)
if got := TTMinusUTCSeconds(timeScaleExactEndJDE); got != base {
t.Fatalf("交接处应连续:%v vs %v", got, base)
}
prev := utcToTTOffsetSeconds(timeScaleExactEndJDE)
carries := 0
firstCarryYear := 0.0
for year := 2027; year <= 3500; year++ {
jd := JDCalc(year, 1, 1)
if dut1 := DUT1Seconds(jd); math.Abs(dut1) >= timeScaleLeapHourSeconds {
t.Fatalf("%d 年 |DUT1|=%v 越过容限 %v", year, math.Abs(dut1), timeScaleLeapHourSeconds)
}
offset := utcToTTOffsetSeconds(jd)
if offset == prev {
continue
}
step := offset - prev
if step <= 0 || math.Abs(step-timeScaleLeapHourSeconds) > 1e-9 {
t.Fatalf("%d 年进位量 %v, want +%v", year, step, timeScaleLeapHourSeconds)
}
if carries == 0 {
firstCarryYear = float64(year)
}
carries++
prev = offset
}
if carries < 1 || carries > 3 {
t.Fatalf("2027..3500 进位次数 %d 不合理", carries)
}
if math.Abs(firstCarryYear-2909) > 3 {
t.Errorf("首次进位应在 2909 年前后, got %.0f", firstCarryYear)
}
for _, jd := range []float64{JDCalc(2400, 1, 1), JDCalc(2909, 1, 1), JDCalc(3200, 1, 1)} {
if got := TT2UTC(UTC2TT(jd)); math.Abs(got-jd) > 1e-9 {
t.Errorf("jd=%v 往返差 %g 秒", jd, (got-jd)*86400)
}
}
}
// 默认与零值均为整数秒外推,首次越限前保持末端偏移。
func TestTimeScaleDefaultFuturePolicyIsLeapSecondRegime(t *testing.T) {
resetTimeScaleState(t)
if got := GetTimeScaleFuturePolicy(); got != TimeScaleLeapSecond {
t.Fatalf("默认未来政策=%v, want TimeScaleLeapSecond", got)
}
var zero TimeScaleFuturePolicy
if zero != TimeScaleLeapSecond {
t.Fatalf("零值政策=%v, 应即默认", zero)
}
base := TTMinusUTCSeconds(timeScaleExactEndJDE)
for _, jd := range []float64{JDCalc(2027, 1, 1), JDCalc(2029, 7, 1), JDCalc(2035, 7, 1)} {
if got := utcToTTOffsetSeconds(jd); math.Abs(got-base) > 1e-9 {
t.Errorf("%v 年默认政策 TT−UTC=%v, want %v(首次触限前应与已宣告偏移相同)",
jd, got, base)
}
if got := DUT1Seconds(jd); math.Abs(got) > utcDUT1ToleranceSeconds+1e-9 {
t.Errorf("%v 年默认政策 |DUT1|=%v 越过容限", jd, math.Abs(got))
}
}
// 平滑跟随 UT1 的政策必须给出不同的值,否则这条守卫会与它混同。
SetTimeScaleFuturePolicy(TimeScaleAssumeUT1Tracking)
if got := utcToTTOffsetSeconds(JDCalc(2035, 7, 1)); math.Abs(got-base) < 1e-3 {
t.Fatalf("跟随 UT1 政策在 2035 年给出 %v,与已宣告偏移 %v 混同,测试失去区分度", got, base)
}
SetTimeScaleFuturePolicy(TimeScaleLeapSecond)
}
func TestTimeScaleLeapTableMagnitudes(t *testing.T) {
resetTimeScaleState(t)
if len(utcLeapJDEs) != len(utcLeapOffsets) {
t.Fatalf("闰秒表长度不一致: %d vs %d", len(utcLeapJDEs), len(utcLeapOffsets))
}
for i := range utcLeapJDEs {
if got := TTMinusUTCSeconds(utcLeapJDEs[i]); got != utcLeapOffsets[i] {
t.Fatalf("第 %d 条生效时刻 %v 起 TT−UTC=%v, want %v", i, utcLeapJDEs[i], got, utcLeapOffsets[i])
}
if i > 0 {
if descending := utcLeapJDEs[i-1] > utcLeapJDEs[i]; !descending {
t.Fatalf("闰秒表应按时刻降序, 第 %d 条顺序错误", i)
}
if step := utcLeapOffsets[i-1] - utcLeapOffsets[i]; step != 1 {
t.Fatalf("历史闰秒步长应为 1 秒, 第 %d 条为 %v", i, step)
}
}
}
if got := TTMinusUTCSeconds(utcLeapJDEs[len(utcLeapJDEs)-1] - 1); got != utcLeapBaseSeconds {
t.Fatalf("1972 年以前应回退基值 %v, got %v", utcLeapBaseSeconds, got)
}
}
// 若将来采用"闰时"(|UTC−UT1| 容限放宽到 H 秒、积满一次才校正),本库不需要改代码:
// 注入一条规则驱动的偏移即可。这里把这个能力锁死——交接连续、|DUT1| 不越界、往返一致、可还原。
func TestLeapHourScheduleIsExpressibleThroughHook(t *testing.T) {
resetTimeScaleState(t)
const hour = 3600.0
base := TTMinusUTCSeconds(timeScaleExactEndJDE)
SetTTMinusUTCFn(func(jd float64) float64 {
drift := base - DeltaT(jd, true) // 未校正的 DUT1
steps := math.Floor(math.Abs(drift) / hour)
if drift < 0 {
return base + hour*steps
}
return base - hour*steps
})
if got := TTMinusUTCSeconds(timeScaleExactEndJDE); got != base {
t.Fatalf("交接处应连续:got %v, want %v", got, base)
}
for _, year := range []int{2030, 2100, 2500, 2909, 3000, 3500} {
dut1 := DUT1Seconds(JDCalc(year, 1, 1))
if math.Abs(dut1) >= hour {
t.Errorf("%d 年 |DUT1|=%v 越过容限 %v", year, math.Abs(dut1), hour)
}
}
for _, jd := range []float64{JDCalc(2050, 1, 1), JDCalc(2909, 1, 1), JDCalc(3000, 1, 1)} {
if got := TT2UTC(UTC2TT(jd)); math.Abs(got-jd) > 1e-9 {
t.Errorf("jd=%v 往返差 %g 秒", jd, (got-jd)*86400)
}
}
SetTTMinusUTCFn(nil)
if got := TTMinusUTCSeconds(timeScaleExactEndJDE); got != base {
t.Fatalf("还原默认后 got %v, want %v", got, base)
}
}
func TestTimeScaleLeapTableAnchors(t *testing.T) {
resetTimeScaleState(t)
cases := []struct {
year, month int
want float64
}{
{1972, 1, 42.184},
{1972, 7, 43.184},
{2017, 1, 69.184},
{2026, 4, 69.184},
}
for _, item := range cases {
if got := TTMinusUTCSeconds(JDCalc(item.year, item.month, 1)); got != item.want {
t.Errorf("TT−UTC(%d-%02d)=%v, want %v", item.year, item.month, got, item.want)
}
}
}
func TestTimeScaleMonthlyDeltaTAnchors(t *testing.T) {
resetTimeScaleState(t)
// 秒级偏移直接锚定;JDE 级取值受 jd≈2.5e6 的浮点抵消限制(约 2e-5 秒)。
if got := DeltaT(timeScaleExactEndJDE, true); math.Abs(got-69.1816) > 1e-12 {
t.Errorf("2026-09 TT−UT1=%v, want 69.1816", got)
}
if got := DeltaT(JDCalc(1973, 2, 1), true); math.Abs(got-43.4724) > 1e-12 {
t.Errorf("1973-02 TT−UT1=%v, want 43.4724", got)
}
if got := (UT12TT(timeScaleExactEndJDE) - timeScaleExactEndJDE) * 86400; math.Abs(got-69.1816) > 1e-4 {
t.Errorf("2026-09 UT12TT 偏移=%v, want 69.1816", got)
}
mid := (deltaTMonthlyJDE[10] + deltaTMonthlyJDE[11]) / 2
want := (deltaTMonthlyObserved[10] + deltaTMonthlyObserved[11]) / 2
if got := DeltaT(mid, true); math.Abs(got-want) > 1e-12 {
t.Errorf("中点插值=%v, want %v", got, want)
}
if _, ok := deltaTMonthlyAt(deltaTMonthlyJDE[0] - 0.5); ok {
t.Error("月度表不应覆盖首项之前")
}
}
// 月度表首与表尾一样要做常值锚定:表外外推在表首处必须与实测值连续,
// 否则表首两侧 DUT1 跳 0.227 秒、UT1 往返在 TT 侧丢掉同样多。
func TestTimeScaleMonthlyDeltaTTableHeadAnchored(t *testing.T) {
resetTimeScaleState(t)
head := deltaTMonthlyJDE[0]
if got := deltaTModelSecondsAtUT(head); math.Abs(got-deltaTMonthlyObserved[0]) > 1e-9 {
t.Errorf("表首 ΔT=%v, want 实测值 %v", got, deltaTMonthlyObserved[0])
}
if gap := deltaTModelSecondsAtUT(head-1e-9) - deltaTModelSecondsAtUT(head+1e-9); math.Abs(gap) > 1e-3 {
t.Errorf("表首两侧 ΔT 跳变 %g 秒", gap)
}
// 表首之前的取值是一个常值平移:平移量等于表首实测值与样条之差,量级 0.227 秒。
shift := deltaTModelSecondsAtUT(head-1) - deltaTSplineAtJDE(head-1)
if want := deltaTMonthlyObserved[0] - deltaTSplineAtJDE(head); math.Abs(shift-want) > 1e-9 {
t.Errorf("表首之前的 ΔT 平移量=%v, want %v", shift, want)
}
if math.Abs(shift) > 0.227 {
t.Errorf("表首之前的 ΔT 平移量 %v 超出预期量级 0.227 秒", shift)
}
worst, worstAt := 0.0, 0.0
for x := head - 0.02; x <= head+0.02; x += 2e-7 {
for _, err := range []float64{
(UT12TT(TT2UT1(x)) - x) * 86400,
(UT12UTC(UTC2UT1(x)) - x) * 86400,
} {
if e := math.Abs(err); e > worst {
worst, worstAt = e, x
}
}
}
if worst > 1e-6 {
t.Errorf("表首 ±0.02 天 UT1 往返最大误差 %g 秒 @ %v, want ≤1e-6 秒", worst, worstAt)
}
}
// 容限是整个窗口的历史事实:按 1e-3 天全窗扫描,抽样点会漏掉月度 ΔT 表首之前的越界。
func TestTimeScaleDUT1Identity(t *testing.T) {
resetTimeScaleState(t)
if got := DUT1Seconds(timeScaleExactEndJDE); math.Abs(got-0.0024) > 1e-12 {
t.Errorf("DUT1(2026-09)=%v, want 0.0024", got)
}
worst, worstAt := 0.0, 0.0
for jd := utcEraStartJDE; jd <= timeScaleExactEndJDE; jd += 1e-3 {
dut1 := DUT1Seconds(jd)
if want := TTMinusUTCSeconds(jd) - DeltaT(jd, true); dut1 != want {
t.Fatalf("恒等式不成立 jd=%v: DUT1=%v want=%v", jd, dut1, want)
}
if mag := math.Abs(dut1); mag > worst {
worst, worstAt = mag, jd
}
}
if worst > utcDUT1ToleranceSeconds {
t.Errorf("窗口内 |DUT1| 最大 %v 秒 @ %v, 超过现行容限 %v", worst, worstAt, utcDUT1ToleranceSeconds)
}
if got := math.Abs(DUT1Seconds(JDCalc(1973, 1, 1))); got > utcDUT1ToleranceSeconds {
t.Errorf("1973-01-01 |DUT1|=%v 超过容限 %v", got, utcDUT1ToleranceSeconds)
}
}
// 窗口内 UTC 方向只做整数秒跳变:ΔT 的小数部分不得混进 TT−UTC。
func TestTimeScaleUTCOffsetJumps(t *testing.T) {
resetTimeScaleState(t)
const wantJumps = 27
jumps, maxFractional := 0, 0.0
prev := utcToTTOffsetSeconds(utcEraStartJDE)
for jd := utcEraStartJDE + 1e-3; jd <= timeScaleExactEndJDE; jd += 1e-3 {
current := utcToTTOffsetSeconds(jd)
if current == prev {
continue
}
jumps++
if fractional := math.Abs((current - prev) - math.Round(current-prev)); fractional > maxFractional {
maxFractional = fractional
}
prev = current
}
if jumps != wantJumps {
t.Errorf("窗口内跳变次数=%d, want %d", jumps, wantJumps)
}
if maxFractional != 0 {
t.Errorf("跳变的非整数残差最大 %v, want 0", maxFractional)
}
}
// 闰秒让 UTC2TT 不连续:每个闰秒在 TT 侧都有一段没有原像的区间,宽度恰好 1 秒。
// 宽度只能测到 jd≈2.4e6 的 1 ULP(约 4e-5 秒),故容差取 1e-4 秒。
func TestTimeScaleLeapSecondUnrepresentableBand(t *testing.T) {
resetTimeScaleState(t)
const ulpToleranceSeconds = 1e-4
inBand := func(tt float64) bool { return math.Abs((UTC2TT(TT2UTC(tt))-tt)*86400) > 1e-6 }
bands := 0
for _, leap := range utcLeapJDEs {
before, after := TTMinusUTCSeconds(leap-1), TTMinusUTCSeconds(leap)
if before == after {
continue
}
if step := after - before; step != 1 {
t.Errorf("闰秒 %v 的偏移步长=%v, want 1 秒", leap, step)
continue
}
bands++
lower, upper := leap+(before-0.001)/86400, leap+(after+0.001)/86400
if inBand(lower) || inBand(upper) {
t.Errorf("闰秒 %v:带外 1 毫秒处往返应精确", leap)
continue
}
center := leap + (before+0.5)/86400
first := bisectFloat(lower, center, inBand)
last := bisectFloat(center, upper, inBand)
if width := (last - first) * 86400; math.Abs(width-1) > ulpToleranceSeconds {
t.Errorf("闰秒 %v 的 TT 不可表示带宽=%v 秒, want 1±%v", leap, width, ulpToleranceSeconds)
}
}
if bands != 27 {
t.Errorf("可测带宽的闰秒 %d 个, want 27", bands)
}
}
// bisectFloat 定位 f 在 [lo, hi] 上的单调跳变点:f(lo) 为假时返回首个为真的点,反之返回最后一个为真的点。
func bisectFloat(lo, hi float64, f func(float64) bool) float64 {
loTrue := f(lo)
for i := 0; i < 80; i++ {
mid := (lo + hi) / 2
if mid <= lo || mid >= hi {
break
}
if f(mid) == loTrue {
lo = mid
} else {
hi = mid
}
}
if loTrue {
return lo
}
return hi
}
func TestTimeScaleRoundTrips(t *testing.T) {
resetTimeScaleState(t)
dates := [][3]float64{{-1000, 1, 1}, {1000, 6, 15}, {1582, 10, 15}, {1972, 1, 1}, {2000, 1, 1}, {2026, 4, 1}, {2030, 1, 1}, {2100, 1, 1}}
for _, item := range dates {
roundTripTimeScales(t, JDCalc(int(item[0]), int(item[1]), item[2]))
}
// 闰秒前后各 0.5 秒:TT 侧不可表示带之外必须可逆,样本要压在跳变旁而不是远离它。
for _, leap := range []float64{utcLeapJDEs[0], utcLeapJDEs[4], utcLeapJDEs[len(utcLeapJDEs)-2]} {
roundTripTimeScales(t, leap-0.5/86400)
roundTripTimeScales(t, leap+0.5/86400)
}
}
func roundTripTimeScales(t *testing.T, jd float64) {
t.Helper()
if got := TT2UT1(UT12TT(jd)); math.Abs(got-jd) > 1e-9 {
t.Errorf("UT1 往返 %v: 差 %g 秒", jd, (got-jd)*86400)
}
if got := TT2UTC(UTC2TT(jd)); math.Abs(got-jd) > 1e-9 {
t.Errorf("UTC 往返 %v: 差 %g 秒", jd, (got-jd)*86400)
}
if got, want := UTC2UT1(jd), TT2UT1(UTC2TT(jd)); got != want {
t.Errorf("UTC2UT1 与复合式不一致 %v: %v vs %v", jd, got, want)
}
if got := UT12UTC(UTC2UT1(jd)); math.Abs(got-jd) > 1e-9 {
t.Errorf("UTC↔UT1 往返 %v: 差 %g 秒", jd, (got-jd)*86400)
}
}
func TestTimeScaleCivilEqualsUT1Before1972(t *testing.T) {
resetTimeScaleState(t)
for _, jd := range []float64{JDCalc(1900, 1, 1), JDCalc(1971, 12, 31)} {
if UTC2TT(jd) != UT12TT(jd) {
t.Errorf("1972 前 UTC2TT 应等于 UT12TT, jd=%v", jd)
}
if got := DUT1Seconds(jd); got != 0 {
t.Errorf("1972 前 DUT1 应为 0, jd=%v got=%v", jd, got)
}
}
}
// 跟随 UT1 政策:窗口外保留末端实测 DUT1,TT−UTC 随外推 ΔT 平滑变化,交界处连续。
func TestTimeScaleFuturePolicy(t *testing.T) {
resetTimeScaleState(t)
SetTimeScaleFuturePolicy(TimeScaleAssumeUT1Tracking)
beyond := timeScaleExactEndJDE + 1
frozenDUT1 := dut1AtTimeScaleExactEnd()
if got := DUT1Seconds(beyond); math.Abs(got-frozenDUT1) > 1e-12 {
t.Errorf("跟随 UT1 政策下窗口外 DUT1 应冻结在 %v, got=%v", frozenDUT1, got)
}
if got, want := utcToTTOffsetSeconds(beyond), ut1ToTTOffsetSeconds(beyond)+frozenDUT1; got != want {
t.Errorf("跟随 UT1 政策下窗口外 TT−UTC=%v, want %v", got, want)
}
before := utcToTTOffsetSeconds(timeScaleExactEndJDE - 1e-9)
after := utcToTTOffsetSeconds(timeScaleExactEndJDE + 1e-9)
if math.Abs(after-before) > 1e-6 {
t.Errorf("跟随 UT1 政策在窗口末端留下 %g 秒跳变", after-before)
}
frozen := TTMinusUTCSeconds(timeScaleExactEndJDE)
SetTimeScaleFuturePolicy(TimeScaleFreezeUTCOffset)
if got, want := UTC2TT(beyond), beyond+frozen/86400; got != want {
t.Errorf("冻结政策下 UTC2TT=%v, want %v", got, want)
}
if got := utcToTTOffsetSeconds(beyond); math.Abs(got-frozen) > 1e-12 {
t.Errorf("冻结政策下 TT−UTC=%v, want %v", got, frozen)
}
near, far := DUT1Seconds(beyond), DUT1Seconds(beyond+365)
if !(far < near) {
t.Errorf("冻结政策下 DUT1 应随 ΔT 增大而下降: %v → %v", near, far)
}
}
// UT1Civil 在全时轴生效,含历史闰秒附近。
func TestTimeScaleUT1CivilPolicy(t *testing.T) {
resetTimeScaleState(t)
SetTimeScaleFuturePolicy(TimeScaleUT1Civil)
for _, jd := range []float64{
JDCalc(1950, 1, 1),
JDCalc(1972, 1, 1),
JDCalc(1999, 1, 1),
JDCalc(2017, 1, 1),
JDCalc(2020, 1, 1),
timeScaleExactEndJDE,
JDCalc(2035, 7, 1),
JDCalc(2200, 1, 1),
} {
if got := DUT1Seconds(jd); got != 0 {
t.Errorf("jd=%v DUT1=%v, want 0", jd, got)
}
if got, want := utcToTTOffsetSeconds(jd), ut1ToTTOffsetSeconds(jd); got != want {
t.Errorf("jd=%v TT−UTC=%v, want ΔT=%v", jd, got, want)
}
if got := UTC2UT1(jd); math.Abs(got-jd) > 1e-9 {
t.Errorf("jd=%v UTC2UT1 差 %g 秒,应为恒等映射", jd, (got-jd)*86400)
}
if got := TT2UTC(UTC2TT(jd)); math.Abs(got-jd) > 1e-9 {
t.Errorf("jd=%v 往返差 %g 秒", jd, (got-jd)*86400)
}
if got, want := TT2UT1(UTC2TT(jd)), UTC2UT1(jd); got != want {
t.Errorf("jd=%v TT2UT1(UTC2TT)=%v, UTC2UT1=%v(该政策下两条路应合流)", jd, got, want)
}
}
// 实测年代必须让位给 ΔT:1999 年闰秒表是 64.184 秒,政策下应等于 ΔT 且与 2017 年闰秒前后连续。
if got := utcToTTOffsetSeconds(JDCalc(1999, 1, 1)); math.Abs(got-64.184) < 1e-9 {
t.Errorf("1999 年 TT−UTC=%v,仍是闰秒表值,政策未覆盖实测窗口", got)
}
before := utcToTTOffsetSeconds(utcLeapJDEs[0] - 1e-6)
after := utcToTTOffsetSeconds(utcLeapJDEs[0] + 1e-6)
if math.Abs(after-before) > 1e-6 {
t.Errorf("2017-01-01 历史闰秒处出现 %g 秒阶跃,该政策下不应有阶跃", after-before)
}
// 注入的 TT−UTC 覆盖仍然优先于政策。
SetTTMinusUTCFn(func(float64) float64 { return 42.184 })
if got := utcToTTOffsetSeconds(JDCalc(2035, 7, 1)); math.Abs(got-42.184) > 1e-9 {
t.Errorf("覆盖后 TT−UTC=%v, want 42.184", got)
}
}
// 现行闰秒政策:偏移只能是末端已宣告值加整数秒;未阶跃 UT1−UTC 越出 ±0.9 秒时补 ±1 秒;
// 首次阶跃之前与冻结政策逐值相同;ΔT 反向漂移时走负闰秒。
func TestTimeScaleLeapSecondPolicy(t *testing.T) {
resetTimeScaleState(t)
SetTimeScaleFuturePolicy(TimeScaleLeapSecond)
base := TTMinusUTCSeconds(timeScaleExactEndJDE)
unstepped := func(jd float64) float64 { return base - ut1ToTTOffsetSeconds(jd) }
low, high := JDCalc(2026, 1, 1), JDCalc(2200, 1, 1)
if unstepped(high) > -utcDUT1ToleranceSeconds {
t.Fatal("2026..2200 内没有触限时刻,测试前提失效")
}
for i := 0; i < 200; i++ {
mid := (low + high) / 2
if unstepped(mid) <= -utcDUT1ToleranceSeconds {
high = mid
} else {
low = mid
}
}
crossing := (low + high) / 2
for _, sample := range []struct {
jd float64
want float64
}{
{crossing - 30, base},
{crossing + 30, base + 1},
{JDCalc(2029, 7, 1), base},
{JDCalc(2035, 7, 1), base},
} {
got := utcToTTOffsetSeconds(sample.jd)
if math.Abs(got-sample.want) > 1e-9 {
t.Errorf("jd=%v TT−UTC=%v, want %v", sample.jd, got, sample.want)
}
if steps := got - base; math.Abs(steps-math.Round(steps)) > 1e-9 {
t.Errorf("jd=%v 相对已宣告偏移 %v 不是整数秒", sample.jd, steps)
}
if dut1 := DUT1Seconds(sample.jd); math.Abs(dut1) > utcDUT1ToleranceSeconds+1e-9 {
t.Errorf("jd=%v |DUT1|=%v 越过容限 %v", sample.jd, math.Abs(dut1), utcDUT1ToleranceSeconds)
}
}
// 逐年扫到 2400:偏移始终是 base 加整数秒,DUT1 全程留在容限内,阶跃只升不降。
prev := utcToTTOffsetSeconds(JDCalc(2027, 1, 1))
for year := 2028; year <= 2400; year++ {
jd := JDCalc(year, 1, 1)
offset := utcToTTOffsetSeconds(jd)
if steps := offset - base; math.Abs(steps-math.Round(steps)) > 1e-9 {
t.Fatalf("%d 年相对偏移 %v 不是整数秒", year, steps)
}
if offset < prev {
t.Fatalf("%d 年偏移从 %v 降到 %v(本库 ΔT 外推下不应出现负闰秒)", year, prev, offset)
}
prev = offset
if dut1 := DUT1Seconds(jd); math.Abs(dut1) > utcDUT1ToleranceSeconds+1e-9 {
t.Fatalf("%d 年 |DUT1|=%v 越过容限", year, math.Abs(dut1))
}
}
// 注入 ΔT 随时间下降的模型:未阶跃 UT1−UTC 变为上升,必须走负闰秒。
SetDeltaTFn(func(jd float64, isJd bool) float64 {
year := jd
if isJd {
year = 2026 + (jd-timeScaleExactEndJDE)/365.25
}
return 69.1816 - 0.2*(year-2026)
})
jd := JDCalc(2032, 1, 1)
if got := utcToTTOffsetSeconds(jd); math.Abs(got-(base-1)) > 1e-9 {
t.Errorf("反向漂移下 TT−UTC=%v, want %v(负闰秒)", got, base-1)
}
if dut1 := DUT1Seconds(jd); math.Abs(dut1) > utcDUT1ToleranceSeconds+1e-9 {
t.Errorf("反向漂移下 |DUT1|=%v 越过容限", math.Abs(dut1))
}
SetDeltaTFn(nil)
}
func TestTimeScaleHooksAndGeneration(t *testing.T) {
resetTimeScaleState(t)
before := deltaTGenerationValue()
SetTTMinusUTCFn(func(float64) float64 { return 100 })
if deltaTGenerationValue() == before {
t.Error("SetTTMinusUTCFn 应递增世代号")
}
if got := TTMinusUTCSeconds(JDCalc(2000, 1, 1)); got != 100 {
t.Errorf("覆盖后 TT−UTC=%v, want 100", got)
}
SetTTMinusUTCFn(nil)
if got := TTMinusUTCSeconds(JDCalc(2000, 1, 1)); math.Abs(got-64.184) > 1e-9 {
t.Errorf("恢复后 TT−UTC=%v, want 64.184", got)
}
before = deltaTGenerationValue()
SetDeltaTFn(func(float64, bool) float64 { return 100 })
if deltaTGenerationValue() == before {
t.Error("SetDeltaTFn 应递增世代号")
}
jd := JDCalc(2000, 1, 1)
if got := ut1ToTTOffsetSeconds(jd); math.Abs(got-100) > 1e-12 {
t.Errorf("注入 ΔT 后 UT1 偏移=%v, want 100", got)
}
SetDeltaTFn(nil)
if got := ut1ToTTOffsetSeconds(jd); math.Abs(got-63.83) > 0.05 {
t.Errorf("恢复后 UT12TT 偏移=%v, 应回到月度实测值附近", got)
}
before = deltaTGenerationValue()
SetTimeScaleFuturePolicy(TimeScaleFreezeUTCOffset)
if deltaTGenerationValue() == before {
t.Error("SetTimeScaleFuturePolicy 应递增世代号")
}
}
// JD 0(−4713-11-24)是合法时刻,不是零值哨兵:TT2UTC(0) 必须与 TT2UT1(0) 给出同一个有限值并互逆。
func TestTimeScaleZeroInstantIsNotSentinel(t *testing.T) {
resetTimeScaleState(t)
utc := TT2UTC(0)
if math.IsNaN(utc) || math.IsInf(utc, 0) {
t.Fatalf("TT2UTC(0)=%v, want 有限值", utc)
}
if got := TT2UT1(0); math.Abs(utc-got) > 1e-9 {
t.Errorf("1972 前民用时标即 UT1:TT2UTC(0)=%v, TT2UT1(0)=%v(差 %g 秒)", utc, got, (utc-got)*86400)
}
if back := UTC2TT(utc); math.Abs(back) > 1e-9 {
t.Errorf("UTC2TT(TT2UTC(0))=%v, want 0(差 %g 秒)", back, back*86400)
}
}
// DeltaT 的第一个参数按 julianDay 解释为 UT 儒略日或十进制年;口径给错时自变量远在模型跨度之外,
// 必须返回 NaN 而不是继续外推成天文数字。
func TestDeltaTArgumentDomain(t *testing.T) {
resetTimeScaleState(t)
for _, year := range []float64{-40000, -2000, -500, 0, 1000.5, 2000, 3000, 40000} {
if got := DeltaT(year, false); math.IsNaN(got) {
t.Errorf("DeltaT(%v, 十进制年)=NaN, 应给出模型值", year)
}
}
for _, jd := range []float64{-1e7, JDCalc(-2000, 1, 1), JDCalc(0, 1, 1), JDCalc(2000, 1, 1), JDCalc(5000, 1, 1), 1e7} {
if got := DeltaT(jd, true); math.IsNaN(got) {
t.Errorf("DeltaT(%v, 儒略日)=NaN, 应给出模型值", jd)
}
}
if got := DeltaT(JDCalc(2026, 3, 1), false); !math.IsNaN(got) {
t.Errorf("儒略日当十进制年应越界返回 NaN, got %v", got)
}
for _, jd := range []float64{1e8, -1e8} {
if got := DeltaT(jd, true); !math.IsNaN(got) {
t.Errorf("DeltaT(%v, 儒略日)=%v, want NaN", jd, got)
}
}
for _, value := range []float64{math.NaN(), math.Inf(1), math.Inf(-1)} {
for _, julianDay := range []bool{false, true} {
if got := DeltaT(value, julianDay); !math.IsNaN(got) {
t.Errorf("DeltaT(%v, %v)=%v, want NaN", value, julianDay, got)
}
}
}
}
func TestTimeScaleSteppedPoliciesRejectInvalidDeltaT(t *testing.T) {
resetTimeScaleState(t)
for _, policy := range []TimeScaleFuturePolicy{TimeScaleLeapSecond, TimeScaleLeapHour} {
SetTimeScaleFuturePolicy(policy)
for _, tc := range []struct {
name string
jd float64
model func(float64, bool) float64
}{
{"nan-model", JDCalc(2030, 1, 1), func(float64, bool) float64 { return math.NaN() }},
{"positive-infinity-model", JDCalc(2030, 1, 1), func(float64, bool) float64 { return math.Inf(1) }},
{"negative-infinity-model", JDCalc(2030, 1, 1), func(float64, bool) float64 { return math.Inf(-1) }},
{"outside-model-range", 1e8, nil},
} {
SetDeltaTFn(tc.model)
for name, got := range map[string]float64{
"offset": utcToTTOffsetSeconds(tc.jd),
"UTC2TT": UTC2TT(tc.jd),
"TT2UTC": TT2UTC(tc.jd),
"DUT1": DUT1Seconds(tc.jd),
} {
if !math.IsNaN(got) {
t.Errorf("policy=%v %s %s=%v, want NaN", policy, tc.name, name, got)
}
}
}
}
}
func TestTimeScalePropagatesNaN(t *testing.T) {
resetTimeScaleState(t)
nan := math.NaN()
for name, got := range map[string]float64{
"UTC2TT": UTC2TT(nan),
"UT12TT": UT12TT(nan),
"TT2UTC": TT2UTC(nan),
"TT2UT1": TT2UT1(nan),
"UTC2UT1": UTC2UT1(nan),
"UT12UTC": UT12UTC(nan),
"DUT1": DUT1Seconds(nan),
} {
if !math.IsNaN(got) {
t.Errorf("%s(NaN)=%v, want NaN", name, got)
}
}
}
// 注入的 TT−UTC 覆盖对窗口之后的查询日期同样生效:未来政策只描述未注入时的形状,
// 否则外部时标模型与实际换算在窗口末端之后会各说各话。
func TestTimeScaleOverrideAppliesBeyondExactWindow(t *testing.T) {
resetTimeScaleState(t)
jd := JDCalc(2027, 1, 1)
SetTTMinusUTCFn(func(at float64) float64 {
if at >= jd {
return 70.184
}
return 69.184
})
for _, policy := range []TimeScaleFuturePolicy{
TimeScaleAssumeUT1Tracking, TimeScaleFreezeUTCOffset, TimeScaleLeapHour,
TimeScaleUT1Civil, TimeScaleLeapSecond,
} {
SetTimeScaleFuturePolicy(policy)
if got := utcToTTOffsetSeconds(jd); math.Abs(got-70.184) > 1e-12 {
t.Errorf("policy=%v: UTC2TT 使用 %.6f 秒,want 70.184", policy, got)
}
// jd 本身只有约 4e-5 秒的分辨率,往返差按秒比较。
if got := (TT2UTC(UTC2TT(jd)) - jd) * 86400; math.Abs(got) > 1e-4 {
t.Errorf("policy=%v: 往返差 %.9f 秒", policy, got)
}
if got := DUT1Seconds(jd); math.Abs(got-(70.184-DeltaT(jd, true))) > 1e-9 {
t.Errorf("policy=%v: DUT1=%.6f 秒,want %.6f", policy, got, 70.184-DeltaT(jd, true))
}
}
SetTTMinusUTCFn(nil)
SetTimeScaleFuturePolicy(TimeScaleFreezeUTCOffset)
if got := utcToTTOffsetSeconds(jd); math.Abs(got-69.184) > 1e-12 {
t.Errorf("未注入时冻结政策应生效:UTC2TT 使用 %.6f 秒", got)
}
}
// 默认 TT−UTC 函数必须忽略注入的覆盖:它是"取默认口径"的唯一入口。
func TestTTMinusUTCSecondsDefaultIgnoresOverride(t *testing.T) {
resetTimeScaleState(t)
jd := timeScaleExactEndJDE
want := TTMinusUTCSecondsDefault(jd)
if math.Abs(want-69.184) > 1e-9 {
t.Fatalf("默认 TT−UTC(窗口末端)=%v, want 69.184", want)
}
SetTTMinusUTCFn(func(float64) float64 { return 42.184 })
if got := TTMinusUTCSeconds(jd); math.Abs(got-42.184) > 1e-9 {
t.Fatalf("覆盖后 TTMinusUTCSeconds=%v, want 42.184", got)
}
if got := TTMinusUTCSecondsDefault(jd); math.Abs(got-want) > 1e-12 {
t.Fatalf("覆盖后默认 TT−UTC=%v, want %v(应忽略覆盖)", got, want)
}
}