diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8a5b6..1bab13e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.1] - 2026-06-01 + +### Changed + +- Refreshed the vendored `qtty-cpp` submodule to the latest patch release, + including the new angular trig helpers and the additional `Ratio` FFI unit + support. +- Expanded the C++ time adapter with the new GNSS scale and week helpers + introduced on the Rust side. + ## [0.5.0] - 2026-05-18 ### Breaking diff --git a/CMakeLists.txt b/CMakeLists.txt index cc7c6d4..c800d4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15) -project(tempoch_cpp VERSION 0.5.0 LANGUAGES CXX) +project(tempoch_cpp VERSION 0.5.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -162,6 +162,8 @@ foreach(_ex 04_periods # Period list operations: complement, intersect, normalize 06_runtime_tables # EOP table access, UT1 conversion, constants 07_conversions # Full chain: Unix → UTC → TAI → TT → TDB / UT1 / GPS + 08_gnss_scales # GNSS and SPICE-compatibility scales (GPST, GST, QZSST, BDT, ET) + 09_gnss_week # GNSS week-number decomposition (GnssWeek) ) add_executable(${_ex} examples/${_ex}.cpp) target_link_libraries(${_ex} PRIVATE tempoch_cpp) @@ -179,6 +181,10 @@ set(TEST_SOURCES tests/test_headers.cpp tests/test_time.cpp tests/test_period.cpp + tests/test_new_scales.cpp + tests/test_constants.cpp + tests/test_data_status.cpp + tests/test_gnss_week.cpp ) add_executable(test_tempoch ${TEST_SOURCES}) diff --git a/examples/08_gnss_scales.cpp b/examples/08_gnss_scales.cpp new file mode 100644 index 0000000..30741f8 --- /dev/null +++ b/examples/08_gnss_scales.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +/** + * @file 08_gnss_scales.cpp + * @example 08_gnss_scales.cpp + * @brief GNSS and SPICE-compatibility time scales (GPST, GST, QZSST, BDT, ET). + * + * These scales were added to mirror the full tempoch scale set. GPST, GST and + * QZSST share the nominal `TAI - 19 s` offset; BDT is `TAI - 33 s`; ET is a + * SPICE-compatibility marker that tracks TDB. + * + * Build and run: + * cmake -B build && cmake --build build + * ./build/08_gnss_scales + */ + +#include +#include +#include +#include +#include + +int main() { + using namespace tempoch; + + auto tai = Time::from_split_seconds(qtty::Second(1.0e9), qtty::Second(0.0)); + + const double tai_s = tai.to().value(); + const double gpst_s = tai.to().to().value(); + const double gst_s = tai.to().to().value(); + const double qzsst_s = tai.to().to().value(); + const double bdt_s = tai.to().to().value(); + const double et_s = tai.to().to().value(); + const double tdb_s = tai.to().to().value(); + + std::cout << std::fixed << std::setprecision(6); + std::cout << "TAI - GPST : " << (tai_s - gpst_s) << " s\n"; + std::cout << "TAI - GST : " << (tai_s - gst_s) << " s\n"; + std::cout << "TAI - QZSST : " << (tai_s - qzsst_s) << " s\n"; + std::cout << "TAI - BDT : " << (tai_s - bdt_s) << " s\n"; + std::cout << "GPST - BDT : " << (gpst_s - bdt_s) << " s\n"; + std::cout << "ET - TDB : " << (et_s - tdb_s) << " s\n"; + + assert(std::abs((tai_s - gpst_s) - 19.0) < 1e-6); + assert(std::abs((tai_s - bdt_s) - 33.0) < 1e-6); + assert(std::abs((gpst_s - bdt_s) - 14.0) < 1e-6); + assert(std::abs(et_s - tdb_s) < 1e-9); + + return 0; +} diff --git a/examples/09_gnss_week.cpp b/examples/09_gnss_week.cpp new file mode 100644 index 0000000..b542b35 --- /dev/null +++ b/examples/09_gnss_week.cpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +/** + * @file 09_gnss_week.cpp + * @example 09_gnss_week.cpp + * @brief GNSS week-number decomposition (GnssWeek) mirroring tempoch. + * + * Decomposes a GNSS-scale instant into `(week, seconds_of_week, + * subsecond_nanos)` since the constellation's defined epoch, and rebuilds the + * instant from that decomposition. Defined for GPST, GST, BDT and QZSST. + * + * Build and run: + * cmake -B build && cmake --build build + * ./build/09_gnss_week + */ + +#include +#include +#include + +int main() { + using namespace tempoch; + + // GPST week-0/second-0 epoch (1980-01-06 UTC) expressed in J2000 seconds. + constexpr double kGpstEpochJ2000Seconds = -630'763'200.0; + + auto epoch = Time::from_raw_j2000_seconds(qtty::Second(kGpstEpochJ2000Seconds)); + GnssWeek gw0 = to_gnss_week(epoch); + std::cout << "GPST epoch -> week=" << gw0.week << " sow=" << gw0.seconds_of_week + << " ns=" << gw0.subsecond_nanos << '\n'; + assert(gw0.week == 0u && gw0.seconds_of_week == 0u && gw0.subsecond_nanos == 0u); + + // Build an instant two weeks + 1 hour past the epoch, then round-trip it. + GnssWeek target{2u, 3'600u, 0u}; + auto t = from_gnss_week(target); + GnssWeek back = to_gnss_week(t); + std::cout << "round-trip -> week=" << back.week << " sow=" << back.seconds_of_week << '\n'; + assert(back.week == target.week); + assert(back.seconds_of_week == target.seconds_of_week); + + // QZSST shares the GPST week numbering. + auto qz = Time::from_raw_j2000_seconds(qtty::Second(kGpstEpochJ2000Seconds)); + GnssWeek qzw = to_gnss_week(qz); + std::cout << "QZSST epoch -> week=" << qzw.week << " sow=" << qzw.seconds_of_week << '\n'; + assert(qzw.week == gw0.week && qzw.seconds_of_week == gw0.seconds_of_week); + + std::cout << "GNSS week-number decomposition OK\n"; + return 0; +} diff --git a/include/tempoch/constants.hpp b/include/tempoch/constants.hpp index dae4cc0..6610ccf 100644 --- a/include/tempoch/constants.hpp +++ b/include/tempoch/constants.hpp @@ -61,5 +61,35 @@ inline double modern_delta_t_observed_end_mjd() noexcept { return tempoch_const_modern_delta_t_observed_end_mjd(); } +/// Constant TT − TAI offset in seconds (32.184 s). +inline double tt_minus_tai_seconds() noexcept { return tempoch_const_tt_minus_tai_seconds(); } + +/// Number of nanoseconds in one SI second (1e9). +inline double nanos_per_second() noexcept { return tempoch_const_nanos_per_second(); } + +/// IAU time-scale epoch T0 as a Julian Date on the TT axis (1977-01-01 TAI). +inline double iau_time_epoch_t0_jd() noexcept { return tempoch_const_iau_time_epoch_t0_jd(); } + +/// First JD(TT) of the high-accuracy TDB−TT model validity window. +inline double tdb_tt_model_high_accuracy_start_jd() noexcept { + return tempoch_const_tdb_tt_model_high_accuracy_start_jd(); +} + +/// Last JD(TT) of the high-accuracy TDB−TT model validity window. +inline double tdb_tt_model_high_accuracy_end_jd() noexcept { + return tempoch_const_tdb_tt_model_high_accuracy_end_jd(); +} + } // namespace constants + +/// ΔT = TT − UT1 in seconds for a UT1 Julian Day, using the compiled USNO +/// model. Returns NaN when the requested epoch is outside the model domain. +inline double delta_t_seconds(double jd_ut1) noexcept { return tempoch_delta_t_seconds(jd_ut1); } + +/// ΔT = TT − UT1 in seconds for a UT1 Julian Day, extrapolating beyond the +/// tabulated range with the long-term parabola (always finite). +inline double delta_t_seconds_extrapolated(double jd_ut1) noexcept { + return tempoch_delta_t_seconds_extrapolated(jd_ut1); +} + } // namespace tempoch diff --git a/include/tempoch/data_status.hpp b/include/tempoch/data_status.hpp new file mode 100644 index 0000000..6453241 --- /dev/null +++ b/include/tempoch/data_status.hpp @@ -0,0 +1,81 @@ +#pragma once + +/** + * @file data_status.hpp + * @brief Active time-data status mirroring `tempoch::time_data_status`. + * + * Exposes the validity horizons of the currently active time-data bundle + * (EOP and ΔT MJD horizons) together with the source the bundle was loaded + * from. Optional EOP horizons surface as NaN when no EOP data is loaded, + * matching the `Option` fields on the Rust side. + */ + +#include "ffi_core.hpp" + +#include +#include + +namespace tempoch { + +/// Origin of the currently active time-data bundle. +/// +/// Mirrors `tempoch::ActiveTimeDataSource`. +enum class TimeDataSource : int { + /// Compiled archive snapshot bundled at build time. + Bundled = 0, + /// Bundle loaded through the runtime fetch/cache path. + RuntimeCache = 1, + /// Test or caller-provided override is active. + Override = 2, +}; + +/// Documented validity horizons of the active time-data bundle, in MJD (UTC). +/// +/// Mirrors `tempoch::DataHorizons`: EOP horizons are absent (`std::nullopt`) +/// when no EOP data is loaded; the ΔT horizons are always present. +struct DataHorizons { + /// First MJD covered by the EOP series, or `std::nullopt` when unloaded. + std::optional eop_start_mjd; + /// Last observed (non-predicted) EOP MJD, or `std::nullopt` when unloaded. + std::optional eop_observed_end_mjd; + /// Last EOP MJD including predictions, or `std::nullopt` when unloaded. + std::optional eop_end_mjd; + /// Last MJD with observed ΔT in the archive-provided modern table. + double modern_delta_t_observed_end_mjd; + /// Last MJD covered by the ΔT prediction table. + double delta_t_prediction_horizon_mjd; +}; + +/// Active time-data status: validity horizons plus the active bundle source. +/// +/// Mirrors the numeric and provenance-source portions of +/// `tempoch::TimeDataStatus`. +struct TimeDataStatus { + /// Validity horizons of the active bundle. + DataHorizons horizons; + /// Source the active bundle was loaded from. + TimeDataSource source; +}; + +/// Capture the status of the currently active time-data bundle. +/// +/// Mirrors `tempoch::time_data_status()`. +inline TimeDataStatus time_data_status() { + TempochDataHorizons raw{}; + check_status(tempoch_time_data_status(&raw), "tempoch::time_data_status"); + + auto opt = [](double v) -> std::optional { + return std::isnan(v) ? std::nullopt : std::optional(v); + }; + + TimeDataStatus status; + status.horizons.eop_start_mjd = opt(raw.eop_start_mjd); + status.horizons.eop_observed_end_mjd = opt(raw.eop_observed_end_mjd); + status.horizons.eop_end_mjd = opt(raw.eop_end_mjd); + status.horizons.modern_delta_t_observed_end_mjd = raw.modern_delta_t_observed_end_mjd; + status.horizons.delta_t_prediction_horizon_mjd = raw.delta_t_prediction_horizon_mjd; + status.source = static_cast(raw.source); + return status; +} + +} // namespace tempoch diff --git a/include/tempoch/gnss_week.hpp b/include/tempoch/gnss_week.hpp new file mode 100644 index 0000000..b4ba9bf --- /dev/null +++ b/include/tempoch/gnss_week.hpp @@ -0,0 +1,64 @@ +#pragma once + +/** + * @file gnss_week.hpp + * @brief GNSS week-number decomposition mirroring `tempoch::GnssWeek`. + * + * Provides the `GnssWeek` value type plus `to_gnss_week` / `from_gnss_week` + * free functions, defined only for the GNSS coordinate scales (`GPST`, `GST`, + * `BDT`, `QZSST`), matching the `Time::::to_gnss_week` / + * `Time::::from_gnss_week` conversions on the Rust side. + */ + +#include "scales/scales.hpp" +#include "time_base.hpp" + +#include +#include + +namespace tempoch { + +/// Trait marking the GNSS coordinate scales that support week decomposition. +template struct is_gnss_scale : std::false_type {}; +template <> struct is_gnss_scale : std::true_type {}; +template <> struct is_gnss_scale : std::true_type {}; +template <> struct is_gnss_scale : std::true_type {}; +template <> struct is_gnss_scale : std::true_type {}; + +template inline constexpr bool is_gnss_scale_v = is_gnss_scale::value; + +/// Decomposed GNSS week-number form since the constellation's defined epoch. +/// +/// Mirrors `tempoch::GnssWeek`. `week` is full (no rollover applied), +/// `seconds_of_week` lies in `[0, 604'800)` and `subsecond_nanos` in +/// `[0, 1'000'000'000)`. +struct GnssWeek { + /// Full week number since the constellation's epoch (no rollover applied). + std::uint32_t week; + /// Seconds since the start of `week`, in `[0, 604'800)`. + std::uint32_t seconds_of_week; + /// Subsecond nanoseconds remainder, in `[0, 1'000'000'000)`. + std::uint32_t subsecond_nanos; +}; + +/// Decompose a GNSS-scale instant into its week-number form. +template , int> = 0> +inline GnssWeek to_gnss_week(const Time &time) { + TempochGnssWeek raw{}; + check_status( + tempoch_time_to_gnss_week(time.c_inner(), static_cast(scale_tag_v), &raw), + "tempoch::to_gnss_week"); + return GnssWeek{raw.week, raw.seconds_of_week, raw.subsecond_nanos}; +} + +/// Build a GNSS-scale instant from a week-number decomposition. +template , int> = 0> +inline Time from_gnss_week(const GnssWeek &gw) { + TempochGnssWeek raw{gw.week, gw.seconds_of_week, gw.subsecond_nanos}; + tempoch_time_t out{}; + check_status(tempoch_time_from_gnss_week(raw, static_cast(scale_tag_v), &out), + "tempoch::from_gnss_week"); + return Time::from_split_seconds(qtty::Second(out.hi_seconds), qtty::Second(out.lo_seconds)); +} + +} // namespace tempoch diff --git a/include/tempoch/scales/bdt.hpp b/include/tempoch/scales/bdt.hpp new file mode 100644 index 0000000..bf4b7ab --- /dev/null +++ b/include/tempoch/scales/bdt.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct BDT {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_BDT; + static constexpr const char *name() { return "BDT"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/et.hpp b/include/tempoch/scales/et.hpp new file mode 100644 index 0000000..0bc72ee --- /dev/null +++ b/include/tempoch/scales/et.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct ET {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_ET; + static constexpr const char *name() { return "ET"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/gpst.hpp b/include/tempoch/scales/gpst.hpp new file mode 100644 index 0000000..94cc722 --- /dev/null +++ b/include/tempoch/scales/gpst.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct GPST {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_GPST; + static constexpr const char *name() { return "GPST"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/gst.hpp b/include/tempoch/scales/gst.hpp new file mode 100644 index 0000000..dd29539 --- /dev/null +++ b/include/tempoch/scales/gst.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct GST {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_GST; + static constexpr const char *name() { return "GST"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/qzsst.hpp b/include/tempoch/scales/qzsst.hpp new file mode 100644 index 0000000..1a360e7 --- /dev/null +++ b/include/tempoch/scales/qzsst.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct QZSST {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_QZSST; + static constexpr const char *name() { return "QZSST"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/scales.hpp b/include/tempoch/scales/scales.hpp index 3ef410e..9b64fa3 100644 --- a/include/tempoch/scales/scales.hpp +++ b/include/tempoch/scales/scales.hpp @@ -12,3 +12,10 @@ #include "tt.hpp" #include "ut1.hpp" #include "utc.hpp" + +// GNSS and SPICE-compatibility scales. +#include "bdt.hpp" +#include "et.hpp" +#include "gpst.hpp" +#include "gst.hpp" +#include "qzsst.hpp" diff --git a/include/tempoch/tempoch.hpp b/include/tempoch/tempoch.hpp index ea03c4a..0581b67 100644 --- a/include/tempoch/tempoch.hpp +++ b/include/tempoch/tempoch.hpp @@ -33,9 +33,11 @@ */ #include "constants.hpp" +#include "data_status.hpp" #include "eop.hpp" #include "ffi_core.hpp" #include "formats/formats.hpp" +#include "gnss_week.hpp" #include "period.hpp" #include "scales/scales.hpp" #include "time.hpp" diff --git a/qtty-cpp b/qtty-cpp index 594f41c..35a679e 160000 --- a/qtty-cpp +++ b/qtty-cpp @@ -1 +1 @@ -Subproject commit 594f41ce2269c6a446efa0be0ac83b31a95f177c +Subproject commit 35a679e8575599c5a56feea4a21a9789a601a46f diff --git a/tempoch b/tempoch index bf22fdc..bde3c08 160000 --- a/tempoch +++ b/tempoch @@ -1 +1 @@ -Subproject commit bf22fdc36313d4e64aecd391f4a580edafa16409 +Subproject commit bde3c0891ec0a221b9a804f1ae6ff73572bf63fe diff --git a/tests/test_constants.cpp b/tests/test_constants.cpp new file mode 100644 index 0000000..29d23fb --- /dev/null +++ b/tests/test_constants.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +// Tests for the named constants and ΔT helpers newly mirrored from tempoch. + +#include + +#include +#include + +using namespace tempoch; + +TEST(Constants, NewNamedConstants) { + EXPECT_DOUBLE_EQ(constants::tt_minus_tai_seconds(), 32.184); + EXPECT_DOUBLE_EQ(constants::nanos_per_second(), 1.0e9); + EXPECT_DOUBLE_EQ(constants::iau_time_epoch_t0_jd(), 2'443'144.500'372'5); + EXPECT_DOUBLE_EQ(constants::tdb_tt_model_high_accuracy_start_jd(), 2'305'447.5); + EXPECT_DOUBLE_EQ(constants::tdb_tt_model_high_accuracy_end_jd(), 2'524'598.5); +} + +TEST(Constants, DeltaTAtJ2000) { + // ΔT near J2000.0 is approximately 63.8 s. + double dt = delta_t_seconds(constants::j2000_jd_tt()); + ASSERT_TRUE(std::isfinite(dt)); + EXPECT_NEAR(dt, 63.83, 1.0); +} + +TEST(Constants, DeltaTExtrapolatedIsFinite) { + // Far-future epoch: tabulated model is exceeded, extrapolation stays finite. + double dt = delta_t_seconds_extrapolated(3'000'000.0); + EXPECT_TRUE(std::isfinite(dt)); +} diff --git a/tests/test_data_status.cpp b/tests/test_data_status.cpp new file mode 100644 index 0000000..05cfd55 --- /dev/null +++ b/tests/test_data_status.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +// Tests for the active time-data status API mirrored from tempoch. + +#include +#include + +using namespace tempoch; + +TEST(DataStatus, DeltaTHorizonsAreFinite) { + TimeDataStatus status = time_data_status(); + EXPECT_TRUE(std::isfinite(status.horizons.modern_delta_t_observed_end_mjd)); + EXPECT_TRUE(std::isfinite(status.horizons.delta_t_prediction_horizon_mjd)); + // The prediction horizon must not precede the last observed ΔT MJD. + EXPECT_GE(status.horizons.delta_t_prediction_horizon_mjd, + status.horizons.modern_delta_t_observed_end_mjd); +} + +TEST(DataStatus, SourceIsValidEnum) { + TimeDataStatus status = time_data_status(); + EXPECT_TRUE(status.source == TimeDataSource::Bundled || + status.source == TimeDataSource::RuntimeCache || + status.source == TimeDataSource::Override); +} + +TEST(DataStatus, EopHorizonsConsistent) { + TimeDataStatus status = time_data_status(); + // EOP horizons are all-present or all-absent together. + const bool has_start = status.horizons.eop_start_mjd.has_value(); + const bool has_obs = status.horizons.eop_observed_end_mjd.has_value(); + const bool has_end = status.horizons.eop_end_mjd.has_value(); + EXPECT_EQ(has_start, has_obs); + EXPECT_EQ(has_obs, has_end); + if (has_start && has_end) { + EXPECT_LE(*status.horizons.eop_start_mjd, *status.horizons.eop_end_mjd); + } +} diff --git a/tests/test_gnss_week.cpp b/tests/test_gnss_week.cpp new file mode 100644 index 0000000..79d5052 --- /dev/null +++ b/tests/test_gnss_week.cpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +// Tests for the GNSS week-number decomposition mirrored from tempoch. + +#include +#include + +using namespace tempoch; + +namespace { + +// GPST week-0/second-0 epoch (1980-01-06T00:00:00 UTC) in J2000 seconds. +constexpr double kGpstEpochJ2000Seconds = -630'763'200.0; + +} // namespace + +TEST(GnssWeek, GpstEpochIsWeekZero) { + auto t = Time::from_raw_j2000_seconds(qtty::Second(kGpstEpochJ2000Seconds)); + GnssWeek gw = to_gnss_week(t); + EXPECT_EQ(gw.week, 0u); + EXPECT_EQ(gw.seconds_of_week, 0u); + EXPECT_EQ(gw.subsecond_nanos, 0u); +} + +TEST(GnssWeek, RoundTripPreservesInstant) { + // Two weeks and 3661 seconds after the GPST epoch. + GnssWeek original{2u, 3'661u, 250'000'000u}; + auto t = from_gnss_week(original); + GnssWeek decoded = to_gnss_week(t); + EXPECT_EQ(decoded.week, original.week); + EXPECT_EQ(decoded.seconds_of_week, original.seconds_of_week); + EXPECT_NEAR(static_cast(decoded.subsecond_nanos), + static_cast(original.subsecond_nanos), 1.0e3); +} + +TEST(GnssWeek, QzsstAlignedWithGpst) { + auto gp = Time::from_raw_j2000_seconds(qtty::Second(kGpstEpochJ2000Seconds)); + auto qz = Time::from_raw_j2000_seconds(qtty::Second(kGpstEpochJ2000Seconds)); + GnssWeek g = to_gnss_week(gp); + GnssWeek q = to_gnss_week(qz); + EXPECT_EQ(g.week, q.week); + EXPECT_EQ(g.seconds_of_week, q.seconds_of_week); +} diff --git a/tests/test_new_scales.cpp b/tests/test_new_scales.cpp new file mode 100644 index 0000000..f722805 --- /dev/null +++ b/tests/test_new_scales.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +// Tests for the GNSS and SPICE-compatibility time scales newly mirrored from +// the Rust tempoch crate (ET, GPST, GST, BDT, QZSST). + +#include +#include + +using namespace tempoch; + +namespace { + +// J2000-second value of an instant on its own scale axis. +template double j2000s_of(const Time &t) { + return t.template to().value(); +} + +} // namespace + +TEST(NewScales, GnssOffsetsFromTai) { + // Pick an instant well after every GNSS epoch. + auto tai = Time::from_split_seconds(qtty::Second(1.0e9), qtty::Second(0.0)); + double tai_s = j2000s_of(tai); + + // GPST = TAI - 19 s (the GPST axis reads 19 s less than TAI). + EXPECT_NEAR(tai_s - j2000s_of(tai.to()), 19.0, 1e-6); + // GST and QZSST share the GPST offset. + EXPECT_NEAR(tai_s - j2000s_of(tai.to()), 19.0, 1e-6); + EXPECT_NEAR(tai_s - j2000s_of(tai.to()), 19.0, 1e-6); + // BDT = TAI - 33 s. + EXPECT_NEAR(tai_s - j2000s_of(tai.to()), 33.0, 1e-6); +} + +TEST(NewScales, EtMatchesTdb) { + auto tdb = Time::from_split_seconds(qtty::Second(5.0e8), qtty::Second(0.0)); + // ET is a SPICE-compatibility marker routing through TDB; same axis value. + EXPECT_NEAR(j2000s_of(tdb.to()), j2000s_of(tdb), 1e-9); +} + +TEST(NewScales, RoundTripThroughGpst) { + auto tt = Time::from_split_seconds(qtty::Second(7.5e8), qtty::Second(0.25)); + auto back = tt.to().to(); + EXPECT_NEAR((back - tt).value(), 0.0, 1e-6); +} + +TEST(NewScales, BdtIsGpstMinus14) { + auto tai = Time::from_split_seconds(qtty::Second(1.0e9), qtty::Second(0.0)); + double gpst_s = j2000s_of(tai.to()); + double bdt_s = j2000s_of(tai.to()); + // BDT = GPST - 14 s. + EXPECT_NEAR(gpst_s - bdt_s, 14.0, 1e-6); +}