Files
astro/basic/timescale_test.go
T

715 lines
26 KiB
Go
Raw Normal View History

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