diff --git a/README.md b/README.md index acb3471..1df3e4b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # tempoch-cpp -tempoch-cpp is a C++17 library for astronomical time primitives. It provides a header-only C++ API backed by the Rust `tempoch-ffi` engine (via a C FFI), with typed time scales, UTC conversions, and period operations. +tempoch-cpp is a C++17 library for astronomical time primitives. It provides a header-only C++ API backed by the Rust `tempoch-ffi` engine (via a C FFI), with explicit time scales, explicit encodings, UTC civil conversion, and generic period operations. [![C++17](https://img.shields.io/badge/C%2B%2B-17-blue.svg)](https://en.cppreference.com/w/cpp/17) [![CMake](https://img.shields.io/badge/CMake-3.15%2B-064F8C.svg)](https://cmake.org/) @@ -8,11 +8,14 @@ tempoch-cpp is a C++17 library for astronomical time primitives. It provides a h ## Features -- Strongly typed time values such as `JulianDate`, `MJD`, `UTC`, `TT`, `TAI`, `TDB`, `TCG`, `TCB`, `GPS`, `UT`, `JDE`, and `UnixTime` +- Split scale/format model: + `Time`, `JulianDate`, `JulianDate`, + `ModifiedJulianDate`, `UnixTime`, and `GpsTime` - Civil UTC representation with nanosecond precision via `tempoch::CivilTime` -- Cross-scale conversion helpers such as `from_utc()`, `to_utc()`, `to_jd()`, and `to_mjd()` +- Explicit conversion flow: + `CivilTime -> Time -> Time -> to()` +- `TimeContext` for UT1 and historical UTC routes - Time arithmetic using `qtty-cpp` duration quantities such as `qtty::Day`, `qtty::Hour`, and `qtty::Minute` -- `UT::delta_t()` with checked UT1 horizon errors from the Rust FFI - `Period` intervals with typed start/end access, intersection, and duration conversion - CMake target (`tempoch_cpp`) for straightforward integration @@ -33,7 +36,7 @@ cd tempoch-cpp cmake -S . -B build cmake --build build --parallel ctest --test-dir build --output-on-failure -./build/time_example +./build/01_quickstart ``` If you cloned without submodules: @@ -49,22 +52,26 @@ git submodule update --init --recursive #include #include -using namespace tempoch; - int main() { - UTC utc(2026, 7, 15, 22, 0, 0); + using namespace tempoch; + + CivilTime civil{2026, 7, 15, 22, 0, 0}; + auto utc = Time::from_civil(civil); + auto tt = utc.to(); - auto jd = JulianDate::from_utc(utc); - auto mjd = MJD::from_jd(jd); + auto jd_tt = tt.to(); + auto jd_utc = utc.to(); + auto mjd_tt = tt.to(); - std::cout << "UTC: " << utc << "\n"; - std::cout << "JD: " << jd << "\n"; - std::cout << "MJD: " << mjd << "\n"; + std::cout << "Civil UTC : " << civil << "\n"; + std::cout << "JD(TT) : " << jd_tt << "\n"; + std::cout << "JD(UTC) : " << jd_utc << "\n"; + std::cout << "MJD(TT) : " << mjd_tt << "\n"; - Period observing_window(MJD(60200.0), MJD(60200.5)); - std::cout << "Duration: " - << observing_window.duration() - << "\n"; + Period> observing_window( + mjd_tt, + mjd_tt + qtty::Hour(12.0)); + std::cout << "Duration: " << observing_window.duration() << "\n"; } ``` @@ -73,18 +80,26 @@ int main() { Time arithmetic uses `qtty-cpp` quantities. Subtracting two times returns a typed duration, and adding or subtracting a quantity shifts the time value. ```cpp -auto jd0 = JulianDate::J2000(); +auto jd0 = tempoch::JulianDate::J2000(); auto jd1 = jd0 + qtty::Hour(12.0); auto dt = jd1 - jd0; std::cout << dt.to() << "\n"; ``` +## Migration Notes + +- Old `JulianDate` means `JulianDate`. +- Old `MJD` means `ModifiedJulianDate`. +- Old `TT` means `Time`, not a JD-valued wrapper. +- Old `UTC` civil construction is now `CivilTime` plus `Time::from_civil(...)`. +- Legacy names remain available only through opt-in shim headers such as `tempoch/legacy_time.hpp`. + ## Documentation - `docs/mainpage.md` (API overview) -- `examples/time_example.cpp` (basic usage) -- `examples/timescales.cpp` (all supported scale conversions plus `ΔT`) +- `examples/01_quickstart.cpp` (civil UTC -> canonical time -> explicit encodings) +- `examples/02_scales.cpp` (all supported scale conversions) - `include/tempoch/tempoch.hpp` (umbrella public header) ## Integration diff --git a/docs/mainpage.md b/docs/mainpage.md index 47ac0a4..5939ffd 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -3,9 +3,8 @@ `tempoch-cpp` is a **modern, header-only C++17 library** for astronomical time primitives. It wraps the Rust-based [`tempoch`](https://github.com/siderust/tempoch) through its C FFI layer -(`tempoch-ffi`), exposing `UTC`, `JulianDate`, `MJD`, and `Period` as -idiomatic C++ value types with arithmetic, comparisons, and exception-based -error reporting — no manual FFI plumbing required. +(`tempoch-ffi`), exposing the same two-axis model as Rust: physical +**scale** and external **format** are separate type parameters. --- @@ -13,10 +12,11 @@ error reporting — no manual FFI plumbing required. | Feature | Description | |---------|-------------| -| **`UTC`** | Civil date-time struct (year/month/day/hour/min/sec) with nanosecond precision | -| **`JulianDate`** | Strongly-typed Julian Date with arithmetic and UTC conversion | -| **`MJD`** | Strongly-typed Modified Julian Date; interchangeable with `JulianDate` | -| **`Period`** | Inclusive `[start, end]` MJD interval with intersection, duration, and iteration helpers | +| **`CivilTime`** | Civil UTC date-time struct (year/month/day/hour/min/sec) with nanosecond precision | +| **`Time`** | Canonical instant on a physical timescale, stored as split J2000 seconds | +| **`EncodedTime`** | Typed external encoding such as `JulianDate` or `ModifiedJulianDate` | +| **`TimeContext`** | Explicit context for UT1 and historical UTC routes | +| **`Period`** | Inclusive `[start, end]` interval over any supported time representation | | **Exception hierarchy** | All FFI status codes map to typed C++ exceptions | | **Header-only** | Drop into any project — no separate compilation step | @@ -26,37 +26,23 @@ error reporting — no manual FFI plumbing required. ```cpp #include -#include +#include int main() { using namespace tempoch; - // Construct a UTC civil time - UTC utc{2026, 7, 15, 22, 0, 0}; + CivilTime civil{2026, 7, 15, 22, 0, 0}; + auto utc = Time::from_civil(civil); + auto tt = utc.to(); - // Convert to Julian Date and MJD - JulianDate jd = JulianDate::from_utc(utc); - MJD mjd = MJD::from_jd(jd); + auto jd_tt = tt.to(); + auto jd_utc = utc.to(); + auto mjd_tt = tt.to(); - std::printf("JD = %.6f\n", jd.value()); - std::printf("MJD = %.6f\n", mjd.value()); - - // Create a one-day window and check containment - MJD start = mjd; - MJD end = mjd + 1.0; - Period night{start, end}; - - std::printf("Duration: %.2f days\n", night.duration()); - std::printf("Contains midpoint: %s\n", - night.contains(mjd + 0.5) ? "yes" : "no"); - - // Round-trip back to UTC - UTC back = jd.to_utc(); - std::printf("UTC: %04d-%02d-%02d %02d:%02d:%02d\n", - back.year, back.month, back.day, - back.hour, back.minute, back.second); - - return 0; + std::cout << "Civil UTC : " << civil << "\n"; + std::cout << "JD(TT) : " << jd_tt << "\n"; + std::cout << "JD(UTC) : " << jd_utc << "\n"; + std::cout << "MJD(TT) : " << mjd_tt << "\n"; } ``` @@ -71,7 +57,7 @@ int main() { └──────┬───────┘ │ header-only (inline) ┌──────▼───────┐ -│ tempoch-cpp │ C++17 value types: UTC, JulianDate, MJD, Period +│ tempoch-cpp │ C++17 value types: CivilTime, Time, EncodedTime, Period │ (headers) │ └──────┬───────┘ │ extern "C" calls @@ -90,10 +76,18 @@ int main() { ## Modules - `tempoch/tempoch.hpp` — umbrella include for the full public API -- `tempoch/time.hpp` — `UTC`, `JulianDate`, `MJD` types +- `tempoch/time.hpp` — `CivilTime`, `Time`, `JulianDate`, `ModifiedJulianDate` - `tempoch/period.hpp` — `Period` interval type - `tempoch/ffi_core.hpp` — FFI helpers and exception hierarchy +## Migration Notes + +- Old `JulianDate` means `JulianDate`. +- Old `MJD` means `ModifiedJulianDate`. +- Old `TT` means `Time`. +- Old `UTC` civil construction is now `CivilTime` plus `Time::from_civil(...)`. +- Legacy names are available only through opt-in compatibility headers. + --- ## Error Model @@ -128,7 +122,7 @@ cmake -S . -B build cmake --build build --parallel ctest --test-dir build --output-on-failure -./build/time_example +./build/01_quickstart ``` --- diff --git a/examples/01_quickstart.cpp b/examples/01_quickstart.cpp index 9e50e17..5902a6e 100644 --- a/examples/01_quickstart.cpp +++ b/examples/01_quickstart.cpp @@ -4,7 +4,8 @@ /** * @file 01_quickstart.cpp * @example 01_quickstart.cpp - * @brief Quick start: a fixed UTC instant expressed as TT, JD, and MJD. + * @brief Quick start: civil UTC promoted to canonical time, then encoded as + * JD(TT), JD(UTC), and MJD(TT). * * Mirrors tempoch example 01_quickstart.rs. chrono system-time integration * is not available through the C FFI, so a fixed reference date is used. @@ -21,16 +22,20 @@ int main() { using namespace tempoch; - UTC utc_now{2026, 7, 15, 22, 0, 0}; - JulianDate jd = JulianDate::from_utc(utc_now); - TT tt = jd.to(); - MJD mjd = jd.to(); + CivilTime civil{2026, 7, 15, 22, 0, 0}; + auto utc = Time::from_civil(civil); + auto tt = utc.to(); + auto jd_tt = tt.to(); + auto jd_utc = utc.to(); + auto mjd_tt = tt.to(); std::cout << std::fixed << std::setprecision(9); - std::cout << "UTC : " << utc_now << "\n"; - std::cout << "TT JD : " << tt << "\n"; - std::cout << "JD : " << jd << "\n"; - std::cout << "MJD : " << mjd << "\n"; + std::cout << "Civil UTC : " << civil << "\n"; + std::cout << "UTC axis : " << utc << "\n"; + std::cout << "TT axis : " << tt << "\n"; + std::cout << "JD(TT) : " << jd_tt << "\n"; + std::cout << "JD(UTC) : " << jd_utc << "\n"; + std::cout << "MJD(TT) : " << mjd_tt << "\n"; return 0; } diff --git a/examples/02_scales.cpp b/examples/02_scales.cpp index 1f74638..6c8c73f 100644 --- a/examples/02_scales.cpp +++ b/examples/02_scales.cpp @@ -22,27 +22,29 @@ int main() { using namespace tempoch; - JulianDate jd{constants::j2000_jd_tt()}; - TT tt = jd.to(); - TAI tai = tt.to(); - UTC utc = tt.to_utc(); - UT ut1 = tt.to(); - TDB tdb = tt.to(); - TCG tcg = tt.to(); - TCB tcb = tt.to(); + auto ctx = TimeContext::with_builtin_eop(); + + auto jd_tt = JulianDate::J2000(); + auto tt = jd_tt.to_time(); + auto tai = tt.to(); + auto utc = tt.to().to_civil(); + auto ut1 = tt.to_with(ctx); + auto tdb = tt.to(); + auto tcg = tt.to(); + auto tcb = tt.to(); std::cout << std::fixed << std::setprecision(9); - std::cout << "TT JD : " << tt << "\n"; - std::cout << "TAI JD : " << tai << "\n"; - std::cout << "UT1 JD : " << ut1 << "\n"; - std::cout << "TDB JD : " << tdb << "\n"; - std::cout << "TCG JD : " << tcg << "\n"; - std::cout << "TCB JD : " << tcb << "\n"; + std::cout << "TT JD : " << tt.to() << "\n"; + std::cout << "TAI JD : " << tai.to() << "\n"; + std::cout << "UT1 JD : " << ut1.to() << "\n"; + std::cout << "TDB JD : " << tdb.to() << "\n"; + std::cout << "TCG JD : " << tcg.to() << "\n"; + std::cout << "TCB JD : " << tcb.to() << "\n"; std::cout << "UTC : " << utc << "\n"; constexpr double SPD = 86'400.0; - const double tt_tai = (tt.value() - tai.value()) * SPD; - const double tt_ut1 = (tt.value() - ut1.value()) * SPD; + const double tt_tai = (tt.to().value() - tai.to().value()) * SPD; + const double tt_ut1 = (tt.to().value() - ut1.to().value()) * SPD; std::cout << std::fixed << std::setprecision(6); std::cout << "TT-TAI : " << tt_tai << " s\n"; diff --git a/examples/03_formats.cpp b/examples/03_formats.cpp index 1e0534e..cbf8a0e 100644 --- a/examples/03_formats.cpp +++ b/examples/03_formats.cpp @@ -24,32 +24,32 @@ static constexpr double UNIX_EPOCH_MJD = 40'587.0; // 1970-01-01 MJD int main() { using namespace tempoch; - // J2000 epoch via direct JD construction. - TT j2000_tt{constants::j2000_jd_tt()}; + // J2000 epoch as canonical TT, then viewed through encodings. + auto j2000_tt = JulianDate::J2000().to_time(); // Sample TT instant: J2000 + 123 456.789 s. - TT sample_tt{constants::j2000_jd_tt() + 123'456.789 / 86'400.0}; + auto sample_tt = j2000_tt + qtty::Second(123'456.789); // J2000 from JulianDate constructor (mirrors JulianDate::::try_new(J2000_JD_TT)). - JulianDate j2000_from_jd{constants::j2000_jd_tt()}; + JulianDate j2000_from_jd{constants::j2000_jd_tt()}; // Unix epoch from JD value. - JulianDate unix_epoch_jd{UNIX_EPOCH_JD}; + JulianDate unix_epoch_jd{UNIX_EPOCH_JD}; // Half-day after J2000 via JD. - JulianDate half_day_jd{constants::j2000_jd_tt() + 0.5}; + JulianDate half_day_jd{constants::j2000_jd_tt() + 0.5}; // Unix epoch from MJD value. - MJD unix_epoch_mjd{UNIX_EPOCH_MJD}; + ModifiedJulianDate unix_epoch_mjd{UNIX_EPOCH_MJD}; // Unix time from seconds (mirrors UnixTime::try_new(Second::new(1_700_000_000.25))). UnixTime ux{1'700'000'000.25}; - UTC utc_from_ux = ux.to_utc(); + auto utc_from_ux = ux.to_time().to_civil(); std::cout << std::fixed << std::setprecision(9); - std::cout << "J2000 TT JD : " << j2000_tt << "\n"; - std::cout << "Sample TT JD : " << sample_tt << "\n"; - std::cout << "Sample TT MJD : " << sample_tt.to() << "\n"; + std::cout << "J2000 TT JD : " << j2000_tt.to() << "\n"; + std::cout << "Sample TT JD : " << sample_tt.to() << "\n"; + std::cout << "Sample TT MJD : " << sample_tt.to() << "\n"; std::cout << "J2000 from JD : " << j2000_from_jd << "\n"; std::cout << "Unix epoch JD(TT) : " << unix_epoch_jd << "\n"; std::cout << "Half-day JD(TT) : " << half_day_jd << "\n"; diff --git a/examples/04_periods.cpp b/examples/04_periods.cpp index ff1615c..b6c1e47 100644 --- a/examples/04_periods.cpp +++ b/examples/04_periods.cpp @@ -19,22 +19,25 @@ int main() { using namespace tempoch; - auto mjd = [](double v) { return MJD{v}; }; + using TTMjd = ModifiedJulianDate; + using TTMjdPeriod = Period; - MJDPeriod day{mjd(61'000.0), mjd(61'001.0)}; + auto mjd = [](double v) { return TTMjd{v}; }; - std::vector a{ + TTMjdPeriod day{mjd(61'000.0), mjd(61'001.0)}; + + std::vector a{ {mjd(61'000.10), mjd(61'000.30)}, {mjd(61'000.60), mjd(61'000.85)}, }; - std::vector b{ + std::vector b{ {mjd(61'000.00), mjd(61'000.20)}, {mjd(61'000.70), mjd(61'001.00)}, }; auto overlaps = intersect_periods(a, b); auto gaps = day.complement_of(a); - auto merged = normalize_periods({a[0], a[1], overlaps[0]}); + auto merged = normalize_periods({a[0], a[1], overlaps[0]}); validate_periods(a); // no-throw = valid @@ -43,7 +46,7 @@ int main() { std::cout << "merged : " << merged.size() << "\n"; std::cout << "contains 61000.5? " << std::boolalpha << a[1].contains(mjd(61'000.5)) << "\n"; - auto joined = day.union_with(MJDPeriod{mjd(60'999.0), mjd(61'001.5)}); + auto joined = day.union_with(TTMjdPeriod{mjd(60'999.0), mjd(61'001.5)}); std::cout << "union : " << joined.size() << " period(s)\n"; return 0; diff --git a/examples/06_runtime_tables.cpp b/examples/06_runtime_tables.cpp index 91c03a3..dd17849 100644 --- a/examples/06_runtime_tables.cpp +++ b/examples/06_runtime_tables.cpp @@ -22,19 +22,21 @@ int main() { using namespace tempoch; using namespace tempoch::constants; + auto ctx = TimeContext::with_builtin_eop(); + // ── TT → UT1 (mirrors probe_tt → probe_ut1 in 06_runtime_tables.rs) ──── // JD TT 2 460 000.25 ≈ 2023-05-15T18:00:00 TT - JulianDate probe_tt{2'460'000.25}; - UT probe_ut1 = probe_tt.to(); + JulianDate probe_tt{2'460'000.25}; + auto probe_ut1 = probe_tt.to_with(ctx); std::cout << std::fixed << std::setprecision(9); std::cout << "probe TT JD : " << probe_tt << "\n"; - std::cout << "probe UT1 JD : " << probe_ut1 << "\n"; + std::cout << "probe UT1 JD : " << probe_ut1.to() << "\n"; // ── Unix timestamp roundtrip ───────────────────────────────────────────── UnixTime ux{1'700'000'000.0}; - UTC utc_from_ux = ux.to_utc(); - UnixTime back = UnixTime::from_utc(utc_from_ux); + auto utc_from_ux = ux.to_time().to_civil(); + auto back = Time::from_civil(utc_from_ux).to(); std::cout << std::fixed << std::setprecision(3); std::cout << "Unix roundtrip: " << back << "\n\n"; @@ -43,8 +45,8 @@ int main() { std::cout << std::fixed << std::setprecision(1); std::cout << "EOP range : [" << eop_start_mjd() << ", " << eop_end_mjd() << "] MJD (UTC)\n"; - // Query EOP at the probe epoch (need UTC MJD; approximate TT→UTC via MJD). - const double probe_mjd_utc = probe_tt.to().value(); + // Query EOP at the probe epoch in UTC MJD. + const double probe_mjd_utc = probe_tt.to().to().value(); if (auto eop = eop_at(probe_mjd_utc)) { std::cout << std::fixed << std::setprecision(6); std::cout << "DUT1 at probe: " << eop->ut1_minus_utc << " s\n"; diff --git a/examples/07_conversions.cpp b/examples/07_conversions.cpp index 5cbcf84..b216440 100644 --- a/examples/07_conversions.cpp +++ b/examples/07_conversions.cpp @@ -22,32 +22,34 @@ int main() { using namespace tempoch; + auto ctx = TimeContext::with_builtin_eop(); + // Start from a Unix timestamp (mirrors UnixTime::try_new(1_700_000_000.25)). UnixTime ux{1'700'000'000.25}; - UTC utc = ux.to_utc(); + auto utc = ux.to_time().to_civil(); // Convert across continuous scales. - TAI tai = ux.to(); - TT tt = tai.to(); - TDB tdb = tt.to(); - UT ut1 = ux.to(); + auto tai = ux.to(); + auto tt = tai.to(); + auto tdb = tt.to(); + auto ut1 = ux.to_with(ctx); // GPS bridge. - GPS gps = tai.to(); - TAI tai_from_gps = gps.to(); + auto gps = tai.to(); + auto tai_from_gps = gps.to(); std::cout << std::fixed << std::setprecision(9); std::cout << "UTC : " << utc << "\n"; - std::cout << "Unix seconds : " << std::fixed << std::setprecision(3) << ux << "\n"; + std::cout << "Unix seconds : " << std::fixed << std::setprecision(3) << ux.value() << "\n"; std::cout << std::fixed << std::setprecision(9); - std::cout << "TAI JD : " << tai << "\n"; - std::cout << "TT JD : " << tt << "\n"; - std::cout << "TDB JD : " << tdb << "\n"; - std::cout << "UT1 JD : " << ut1 << "\n"; - std::cout << "GPS days : " << gps << "\n"; // days since GPS epoch + std::cout << "TAI JD : " << tai.to() << "\n"; + std::cout << "TT JD : " << tt.to() << "\n"; + std::cout << "TDB JD : " << tdb.to() << "\n"; + std::cout << "UT1 JD : " << ut1.to() << "\n"; + std::cout << "GPS seconds : " << gps.value() << "\n"; // GPS → TAI round-trip residual (mirrors the assert in 07_conversions.rs). - const double residual = std::abs(tai_from_gps.value() - tai.value()) * 86'400.0; + const double residual = std::abs((tai_from_gps - tai).value()); std::cout << std::scientific; std::cout << "GPS→TAI residual: " << residual << " s\n"; assert(residual < 1e-9); diff --git a/include/tempoch/civil_time.hpp b/include/tempoch/civil_time.hpp index 1a9765a..802f492 100644 --- a/include/tempoch/civil_time.hpp +++ b/include/tempoch/civil_time.hpp @@ -21,7 +21,8 @@ namespace tempoch { * * @code * tempoch::CivilTime noon(2000, 1, 1, 12, 0, 0); - * auto jd = tempoch::JulianDate::from_utc(noon); + * auto utc = tempoch::Time::from_civil(noon); + * auto jd = utc.to().to(); * @endcode */ struct CivilTime { diff --git a/include/tempoch/ffi_core.hpp b/include/tempoch/ffi_core.hpp index 322edc4..1fd614f 100644 --- a/include/tempoch/ffi_core.hpp +++ b/include/tempoch/ffi_core.hpp @@ -77,6 +77,22 @@ class InvalidDurationUnitError : public TempochException { explicit InvalidDurationUnitError(const std::string &msg) : TempochException(msg) {} }; +/** + * @brief A generic scale/format conversion failed. + */ +class ConversionFailedError : public TempochException { +public: + explicit ConversionFailedError(const std::string &msg) : TempochException(msg) {} +}; + +/** + * @brief The supplied raw format identifier is invalid. + */ +class InvalidFormatIdError : public TempochException { +public: + explicit InvalidFormatIdError(const std::string &msg) : TempochException(msg) {} +}; + /** * @brief Rust caught a panic before unwinding across the FFI boundary. */ @@ -138,6 +154,10 @@ inline void check_status(tempoch_status_t status, const char *operation) { throw InvalidScaleIdError(msg + "invalid scale id"); case TEMPOCH_STATUS_T_INVALID_DURATION_UNIT: throw InvalidDurationUnitError(msg + "invalid duration unit"); + case TEMPOCH_STATUS_T_CONVERSION_FAILED: + throw ConversionFailedError(msg + "conversion failed"); + case TEMPOCH_STATUS_T_INVALID_FORMAT_ID: + throw InvalidFormatIdError(msg + "invalid format id"); case TEMPOCH_STATUS_T_INTERNAL_PANIC: throw InternalPanicError(msg + "internal panic"); case TEMPOCH_STATUS_T_UT1_HORIZON_EXCEEDED: diff --git a/include/tempoch/legacy_time.hpp b/include/tempoch/legacy_time.hpp new file mode 100644 index 0000000..0771b22 --- /dev/null +++ b/include/tempoch/legacy_time.hpp @@ -0,0 +1,36 @@ +#pragma once + +/** + * @file legacy_time.hpp + * @brief Opt-in legacy aliases for pre-redesign tempoch-cpp call sites. + * + * This header is intentionally not included by `tempoch/tempoch.hpp`. + */ + +#include "time.hpp" + +namespace tempoch::legacy { + +using UTC [[deprecated("Use tempoch::CivilTime or tempoch::Time.")]] = + CivilTime; +using JulianDate [[deprecated("Use tempoch::JulianDate.")]] = + tempoch::JulianDate; +using MJD [[deprecated("Use tempoch::ModifiedJulianDate.")]] = + tempoch::ModifiedJulianDate; +using TT [[deprecated("Use tempoch::Time.")]] = + tempoch::Time; +using TAI [[deprecated("Use tempoch::Time.")]] = + tempoch::Time; +using TDB [[deprecated("Use tempoch::Time.")]] = + tempoch::Time; +using TCG [[deprecated("Use tempoch::Time.")]] = + tempoch::Time; +using TCB [[deprecated("Use tempoch::Time.")]] = + tempoch::Time; +using UT [[deprecated("Use tempoch::Time.")]] = + tempoch::Time; +using JDE [[deprecated("Use tempoch::JulianDate.")]] = + tempoch::JulianDate; +using GPS [[deprecated("Use tempoch::GpsTime.")]] = tempoch::GpsTime; + +} // namespace tempoch::legacy diff --git a/include/tempoch/period.hpp b/include/tempoch/period.hpp index 2af74b0..7311feb 100644 --- a/include/tempoch/period.hpp +++ b/include/tempoch/period.hpp @@ -2,16 +2,7 @@ /** * @file period.hpp - * @brief C++ wrapper for time periods, generic over any time type. - * - * Wraps the tempoch-ffi Period API with a value-semantic, exception-safe C++ - * class template `Period`. The underlying storage is always - * `tempoch_period_mjd_t`; `TimeTraits` handles conversion to/from the raw - * MJD doubles. - * - * `TimeTraits` specialisations for every `Time` are provided automatically - * by `time_base.hpp`. A manual `CivilTime` specialisation is kept here for - * `Period` (i.e. `UTCPeriod`). + * @brief Generic time periods parameterised by a typed time representation. */ #include "qtty/qtty.hpp" @@ -21,140 +12,76 @@ namespace tempoch { -// ============================================================================ -// TimeTraits — CivilTime specialisation -// ============================================================================ -// TimeTraits> for all scale-based types is already defined in -// time_base.hpp. We only need the CivilTime-specific one here. +template struct TimeTraits> { + static double to_mjd_value(const Time &time) { + return time.template to().value(); + } -template <> struct TimeTraits { - static double to_mjd_value(const CivilTime &t) { - return TimeScaleTraits::from_civil(t); + static Time from_mjd_value(double mjd) { return ModifiedJulianDate(mjd).to_time(); } +}; + +template struct TimeTraits> { + static double to_mjd_value(const EncodedTime &time) { + if constexpr (std::is_same_v) { + return time.value(); + } else { + return time.to_time().template to().value(); + } + } + + static EncodedTime from_mjd_value(double mjd) { + if constexpr (std::is_same_v) { + return EncodedTime(mjd); + } else { + return ModifiedJulianDate(mjd).template to(); + } } - static CivilTime from_mjd_value(double m) { return TimeScaleTraits::to_civil(m); } }; -// ============================================================================ -// Period -// ============================================================================ +template <> struct TimeTraits { + static double to_mjd_value(const CivilTime &time) { + return Time::from_civil(time).template to().value(); + } -/** - * @brief A time period [start, end] parameterised on a time type @p T. - * - * Internally stores start/end as raw MJD days (matching the FFI layer) and - * converts to/from @p T on demand via `TimeTraits`. - * - * @tparam T A time type for which `TimeTraits` is defined: - * `MJD` (default), `JulianDate`, or `UTC`. - * - * @code - * // MJD period — must use explicit MJD wrappers to avoid JD/MJD ambiguity - * tempoch::Period night(tempoch::MJD(60200.0), tempoch::MJD(60200.5)); - * - * // Julian-Date period - * auto start = tempoch::JulianDate::from_utc({2026, 1, 1}); - * auto end = tempoch::JulianDate::from_utc({2026, 6, 1}); - * tempoch::Period jd_run(start, end); - * - * // UTC period - * tempoch::Period utc_run(tempoch::UTC(2026, 1, 1), tempoch::UTC(2026, 6, 1)); - * @endcode - */ -template class Period { + static CivilTime from_mjd_value(double mjd) { + return ModifiedJulianDate(mjd).to_time().to_civil(); + } +}; + +template > class Period { tempoch_period_mjd_t m_inner; - /// Private struct-based constructor used by from_c(); skips re-validation. explicit Period(const tempoch_period_mjd_t &inner) : m_inner(inner) {} public: - /** - * @brief Construct from typed start/end values. - * - * Raw `double` values are intentionally not accepted to prevent the - * JD-vs-MJD ambiguity. Wrap the value in the appropriate type first: - * `MJD(mjd_value)`, `JulianDate(jd_value)`, or a `UTC{…}` struct. - * - * @param start Inclusive start instant. - * @param end Inclusive end instant. - * @throws InvalidPeriodError If @p start is later than @p end. - */ Period(const T &start, const T &end) { check_status(tempoch_period_mjd_new(TimeTraits::to_mjd_value(start), TimeTraits::to_mjd_value(end), &m_inner), "Period::Period"); } - /// Construct from the C FFI struct (bypasses public validation; FFI output is - /// trusted). static Period from_c(const tempoch_period_mjd_t &c) { return Period(c); } - /// Inclusive period start as a value of type @p T. T start() const { return TimeTraits::from_mjd_value(m_inner.start_mjd); } - - /// Inclusive period end as a value of type @p T. T end() const { return TimeTraits::from_mjd_value(m_inner.end_mjd); } - /** - * @brief Duration as a qtty time quantity. - * - * Returns the period length converted to the requested unit. - * The raw FFI value (in days) is wrapped as a `qtty::Day` and then - * converted via the qtty unit-conversion layer. - * - * @tparam TargetType A qtty unit tag (e.g., `qtty::DayTag`) or its - * convenience alias (e.g., `qtty::Day`, `qtty::Second`, - * `qtty::Hour`). Defaults to `qtty::DayTag`. - * @return The duration as a `qtty::Quantity` in the requested unit. - * - * @code - * tempoch::Period p(tempoch::MJD(60200.0), tempoch::MJD(60201.5)); // 1.5-day - * period auto d = p.duration(); // qtty::Day → 1.5 - * d auto h = p.duration(); // qtty::Hour → 36 h - * auto s = p.duration(); // qtty::Second → 129600 s - * @endcode - */ template qtty::Quantity::type> duration() const { auto qty = tempoch_period_mjd_duration_qty(m_inner); return qtty::Quantity(qty.value).template to(); } - /** - * @brief Compute the overlapping interval with another period. - * @param other The period to intersect with. - * @return The overlap as a `Period`. - * @throws NoIntersectionError If the two periods do not overlap. - */ Period intersection(const Period &other) const { - tempoch_period_mjd_t out; + tempoch_period_mjd_t out{}; check_status(tempoch_period_mjd_intersection(m_inner, other.m_inner, &out), "Period::intersection"); return from_c(out); } - /** - * @brief Test whether an instant falls within this period. - * - * Uses a half-open [start, end) convention consistent with the FFI layer. - * - * @param point The instant to test. - * @return `true` when @p point is in `[start(), end())`. - */ bool contains(const T &point) const noexcept { return tempoch_period_mjd_contains(m_inner, TimeTraits::to_mjd_value(point)); } - /** - * @brief Compute the union of this period and @p other. - * - * If the two periods overlap or touch, the result is a single merged period - * (`result.size() == 1`). If they are disjoint, the result contains both - * periods in chronological order (`result.size() == 2`). - * - * @param other The period to unite with. - * @return A `std::vector>` with one or two elements. - * @throws InvalidPeriodError If either period is malformed. - */ std::vector> union_with(const Period &other) const { tempoch_period_mjd_t buf[2]; std::size_t count = 0; @@ -167,23 +94,11 @@ template class Period { return result; } - /** - * @brief Compute the complement of a period list within this period. - * - * Returns the gaps inside `*this` that are not covered by any period in - * @p others. @p others must be sorted and non-overlapping. - * - * @param others Sorted, non-overlapping inner periods (may be empty). - * @return The gaps as a `std::vector>`. - * @throws InvalidPeriodError If any period is malformed. - * @throws PeriodListUnsortedError If @p others is not sorted. - * @throws PeriodListOverlappingError If @p others has overlapping intervals. - */ std::vector> complement_of(const std::vector> &others) const { std::vector raw; raw.reserve(others.size()); - for (const auto &p : others) - raw.push_back(p.c_inner()); + for (const auto &period : others) + raw.push_back(period.c_inner()); tempoch_period_mjd_t *out_ptr = nullptr; std::size_t out_count = 0; @@ -199,66 +114,32 @@ template class Period { return result; } - /// Access the underlying FFI POD value. - const tempoch_period_mjd_t &c_inner() const { return m_inner; } + const tempoch_period_mjd_t &c_inner() const noexcept { return m_inner; } }; -// ============================================================================ -// C++17 deduction guides -// ============================================================================ - -/// Typed time values → Period (covers MJD, JulianDate, UTC, …). template Period(T, T) -> Period; -// ============================================================================ -// Convenience type aliases -// ============================================================================ - -using MJDPeriod = Period; ///< Period expressed in Modified Julian Date. -using JDPeriod = Period; ///< Period expressed in Julian Date. -using UTCPeriod = Period; ///< Period expressed in UTC civil time. - -// ============================================================================ -// Period list free functions -// ============================================================================ +using TTMjdPeriod = Period>; +using UTCPeriod = Period; -/** - * @brief Validate that a list of periods is sorted and non-overlapping. - * - * @tparam T Time type (e.g. `MJD`, `JulianDate`, `CivilTime`). - * @param periods The list to check (may be empty). - * @throws InvalidPeriodError If any period has start > end. - * @throws PeriodListUnsortedError If the list is not sorted. - * @throws PeriodListOverlappingError If the list has overlapping intervals. - */ template inline void validate_periods(const std::vector> &periods) { std::vector raw; raw.reserve(periods.size()); - for (const auto &p : periods) - raw.push_back(p.c_inner()); + for (const auto &period : periods) + raw.push_back(period.c_inner()); check_status(tempoch_period_list_validate(raw.data(), raw.size()), "validate_periods"); } -/** - * @brief Intersect two sorted, non-overlapping period lists. - * - * @tparam T Time type. - * @param a First sorted, non-overlapping list. - * @param b Second sorted, non-overlapping list. - * @return Sorted intersection of the two lists. - * @throws InvalidPeriodError, PeriodListUnsortedError, PeriodListOverlappingError - * if either input list is invalid. - */ template inline std::vector> intersect_periods(const std::vector> &a, const std::vector> &b) { std::vector ra, rb; ra.reserve(a.size()); rb.reserve(b.size()); - for (const auto &p : a) - ra.push_back(p.c_inner()); - for (const auto &p : b) - rb.push_back(p.c_inner()); + for (const auto &period : a) + ra.push_back(period.c_inner()); + for (const auto &period : b) + rb.push_back(period.c_inner()); tempoch_period_mjd_t *out_ptr = nullptr; std::size_t out_count = 0; @@ -274,28 +155,16 @@ inline std::vector> intersect_periods(const std::vector> &a, return result; } -/** - * @brief Merge two period lists, combining overlapping and adjacent intervals. - * - * The inputs need not be sorted or non-overlapping; the output is always - * sorted and non-overlapping. - * - * @tparam T Time type. - * @param a First list. - * @param b Second list. - * @return Merged, normalised period list. - * @throws InvalidPeriodError if any input period is malformed. - */ template inline std::vector> union_periods(const std::vector> &a, const std::vector> &b) { std::vector ra, rb; ra.reserve(a.size()); rb.reserve(b.size()); - for (const auto &p : a) - ra.push_back(p.c_inner()); - for (const auto &p : b) - rb.push_back(p.c_inner()); + for (const auto &period : a) + ra.push_back(period.c_inner()); + for (const auto &period : b) + rb.push_back(period.c_inner()); tempoch_period_mjd_t *out_ptr = nullptr; std::size_t out_count = 0; @@ -311,20 +180,12 @@ inline std::vector> union_periods(const std::vector> &a, return result; } -/** - * @brief Sort and merge overlapping or adjacent periods in a single list. - * - * @tparam T Time type. - * @param periods The list to normalise (may be unsorted / overlapping). - * @return Sorted, non-overlapping list. - * @throws InvalidPeriodError if any period is malformed. - */ template inline std::vector> normalize_periods(const std::vector> &periods) { std::vector raw; raw.reserve(periods.size()); - for (const auto &p : periods) - raw.push_back(p.c_inner()); + for (const auto &period : periods) + raw.push_back(period.c_inner()); tempoch_period_mjd_t *out_ptr = nullptr; std::size_t out_count = 0; @@ -339,13 +200,8 @@ inline std::vector> normalize_periods(const std::vector> &pe return result; } -// ============================================================================ -// operator<< -// ============================================================================ - -/// Stream a Period as [start, end] using T's own operator<<. -template inline std::ostream &operator<<(std::ostream &os, const Period &p) { - return os << '[' << p.start() << ", " << p.end() << ']'; +template inline std::ostream &operator<<(std::ostream &os, const Period &period) { + return os << '[' << period.start() << ", " << period.end() << ']'; } } // namespace tempoch diff --git a/include/tempoch/scales.hpp b/include/tempoch/scales.hpp index f56a5af..0db4260 100644 --- a/include/tempoch/scales.hpp +++ b/include/tempoch/scales.hpp @@ -2,276 +2,130 @@ /** * @file scales.hpp - * @brief Time-scale tag types and traits for the tempoch Time template. + * @brief Time-scale and time-format tag types for the tempoch C++ API. * - * Scale tag structs live in `tempoch::scales::` (e.g., `scales::TT`, - * `scales::JD`). Convenience type aliases (`TT = Time` etc.) - * are provided in `time.hpp`. - * - * Mirrors the Rust scale set while keeping the C++ API strongly typed. - * The C ABI underneath is intentionally simpler: all generic dispatch goes - * through `double` values plus a raw scale id. + * The C++ surface follows the Rust `tempoch` model: physical *scale* and + * external *format* are orthogonal type parameters. */ -#include "civil_time.hpp" // CivilTime struct definition -#include "ffi_core.hpp" // tempoch_ffi.h + check_status -#include +#include "ffi_core.hpp" +#include +#include namespace tempoch { -// ============================================================================ -// Scale Tags — in tempoch::scales:: -// ============================================================================ - -namespace scales { - -/// Julian Date (days since −4712‑01‑01T12:00 TT). -struct JD {}; - -/// Modified Julian Date (JD − 2 400 000.5). -struct MJD {}; - -/// UTC, internally stored as MJD days for arithmetic. -struct UTC {}; +namespace scale { -/// Terrestrial Time (TT), stored as JD days in TT scale. struct TT {}; - -/// International Atomic Time (TAI), stored as JD days in TAI scale. struct TAI {}; - -/// Barycentric Dynamical Time (TDB), stored as JD days in TDB scale. +struct UTC {}; +struct UT1 {}; struct TDB {}; - -/// Geocentric Coordinate Time (TCG), stored as JD days in TCG scale. struct TCG {}; - -/// Barycentric Coordinate Time (TCB), stored as JD days in TCB scale. struct TCB {}; -/// GPS Time, stored as JD days in GPS scale. -struct GPS {}; - -/// Universal Time (UT1), stored as JD days in UT1 scale. -struct UT {}; +} // namespace scale -/// Julian Ephemeris Date (JDE ≡ TDB), stored as JD days. -struct JDE {}; +namespace format { -/// Unix Time (seconds since 1970-01-01T00:00:00 UTC), stored as Unix seconds. +struct JD {}; +struct MJD {}; +struct J2000s {}; struct Unix {}; +struct GPS {}; -} // namespace scales - -// ============================================================================ -// TimeScaleTraits — per-scale FFI dispatch -// ============================================================================ - -/** - * @brief Primary template — must be specialised for every supported scale. - * - * Required static members: - * - `const char* label()` - * - `double from_utc(const CivilTime&)` — civil time → raw days - * - `CivilTime to_utc(double days)` — raw days → civil time - * - `double add_days(double days, double delta)` - * - `double difference(double a, double b)` — a − b in days - */ -template struct TimeScaleTraits { - static_assert(sizeof(S) == 0, "TimeScaleTraits must be specialised for this scale."); -}; - -namespace detail { - -template struct ScaleIdOf; - -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_JD; -}; -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_MJD; -}; -// UTC is stored as MJD days in the FFI layer (TempochScaleId::MJD = 1). -// tempoch_time_from_utc/to_utc with scale_id=1 perform the UTC↔civil -// conversion. This aliasing is intentional: UTC and MJD share the same -// underlying numeric representation in the C ABI. -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_MJD; -}; -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_TDB; -}; -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_TT; -}; -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_TAI; -}; -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_TCG; -}; -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_TCB; -}; -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_GPS; -}; -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_UT; -}; -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_JDE; -}; -template <> struct ScaleIdOf { - static constexpr int32_t value = TEMPOCH_SCALE_ID_T_UNIX_TIME; -}; - -template inline constexpr int32_t scale_id_v = ScaleIdOf::value; - -inline double time_from_utc(const CivilTime &ct, int32_t scale_id, const char *operation) { - double out = 0.0; - auto raw = ct.to_c(); - check_status(tempoch_time_from_utc(raw, scale_id, &out), operation); - return out; -} - -inline CivilTime time_to_utc(double value, int32_t scale_id, const char *operation) { - tempoch_utc_t out{}; - check_status(tempoch_time_to_utc(value, scale_id, &out), operation); - return CivilTime::from_c(out); -} - -inline double time_add_days(double value, int32_t scale_id, double delta, const char *operation) { - double out = 0.0; - check_status(tempoch_time_add_days(value, scale_id, delta, &out), operation); - return out; -} - -inline double time_difference_days(double lhs, double rhs, int32_t scale_id, - const char *operation) { - double out = 0.0; - check_status(tempoch_time_difference_days(lhs, rhs, scale_id, &out), operation); - return out; -} - -inline qtty_quantity_t time_difference_qty(double lhs, double rhs, int32_t scale_id, - const char *operation) { - qtty_quantity_t out{0.0, UNIT_ID_DAY}; - check_status(tempoch_time_difference_qty(lhs, rhs, scale_id, &out), operation); - return out; -} - -inline void time_add_qty(double value, int32_t scale_id, qtty_quantity_t duration, double &out, - const char *operation) { - check_status(tempoch_time_add_qty(value, scale_id, duration, &out), operation); -} - -template struct GenericScaleTraits { - static double from_civil(const CivilTime &ct) { - return time_from_utc(ct, scale_id_v, "tempoch_time_from_utc"); - } - - static CivilTime to_civil(double value) { - return time_to_utc(value, scale_id_v, "tempoch_time_to_utc"); - } - - static double add_days(double value, double delta) { - return time_add_days(value, scale_id_v, delta, "tempoch_time_add_days"); - } - - static double difference(double a, double b) { - return time_difference_days(a, b, scale_id_v, "tempoch_time_difference_days"); - } - - static qtty_quantity_t difference_qty(double a, double b) { - return time_difference_qty(a, b, scale_id_v, "tempoch_time_difference_qty"); - } +} // namespace format - static void add_qty(double value, qtty_quantity_t duration, double &out) { - time_add_qty(value, scale_id_v, duration, out, "tempoch_time_add_qty"); - } -}; +template struct is_scale : std::false_type {}; +template <> struct is_scale : std::true_type {}; +template <> struct is_scale : std::true_type {}; +template <> struct is_scale : std::true_type {}; +template <> struct is_scale : std::true_type {}; +template <> struct is_scale : std::true_type {}; +template <> struct is_scale : std::true_type {}; +template <> struct is_scale : std::true_type {}; -} // namespace detail +template inline constexpr bool is_scale_v = is_scale::value; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "JD"; } +template struct is_format : std::false_type {}; +template <> struct is_format : std::true_type {}; +template <> struct is_format : std::true_type {}; +template <> struct is_format : std::true_type {}; +template <> struct is_format : std::true_type {}; +template <> struct is_format : std::true_type {}; - static double j2000() { return tempoch_jd_j2000(); } +template inline constexpr bool is_format_v = is_format::value; - static double julian_centuries(double jd) { return tempoch_jd_julian_centuries(jd); } +template struct ScaleTraits; - static qtty_quantity_t julian_centuries_qty(double jd) { - return tempoch_jd_julian_centuries_qty(jd); - } +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TT; + static constexpr const char *name() { return "TT"; } }; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "MJD"; } +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TAI; + static constexpr const char *name() { return "TAI"; } }; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "UTC"; } +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_UTC; + static constexpr const char *name() { return "UTC"; } }; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "TT"; } +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_UT1; + static constexpr const char *name() { return "UT1"; } }; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "TAI"; } +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TDB; + static constexpr const char *name() { return "TDB"; } }; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "TDB"; } +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TCG; + static constexpr const char *name() { return "TCG"; } }; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "TCG"; } +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TCB; + static constexpr const char *name() { return "TCB"; } }; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "TCB"; } -}; +template struct FormatTraits; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "GPS"; } +template <> struct FormatTraits { + using quantity_type = qtty::Day; + static constexpr tempoch_format_tag_t ffi_tag = TEMPOCH_FORMAT_TAG_T_JD; + static constexpr const char *name() { return "JD"; } }; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "UT1"; } +template <> struct FormatTraits { + using quantity_type = qtty::Day; + static constexpr tempoch_format_tag_t ffi_tag = TEMPOCH_FORMAT_TAG_T_MJD; + static constexpr const char *name() { return "MJD"; } }; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "JDE"; } +template <> struct FormatTraits { + using quantity_type = qtty::Second; + static constexpr tempoch_format_tag_t ffi_tag = TEMPOCH_FORMAT_TAG_T_J2000_SECONDS; + static constexpr const char *name() { return "J2000s"; } }; -template <> struct TimeScaleTraits : detail::GenericScaleTraits { - static constexpr const char *label() { return "Unix"; } +template <> struct FormatTraits { + using quantity_type = qtty::Second; + static constexpr tempoch_format_tag_t ffi_tag = TEMPOCH_FORMAT_TAG_T_UNIX; + static constexpr const char *name() { return "Unix"; } }; -// ============================================================================ -// TimeConvertTraits — cross-scale conversion -// ============================================================================ - -/** - * @brief Primary template — routes any A→B conversion through JD. - * - * All cross-scale conversion goes through the generic `tempoch_time_convert` - * ABI entrypoint and compile-time scale-id mapping. - */ -template struct TimeConvertTraits { - static double convert(double src) { - double out = 0.0; - check_status(tempoch_time_convert(src, detail::scale_id_v, detail::scale_id_v, &out), - "tempoch_time_convert"); - return out; - } +template <> struct FormatTraits { + using quantity_type = qtty::Second; + static constexpr tempoch_format_tag_t ffi_tag = TEMPOCH_FORMAT_TAG_T_GPS; + static constexpr const char *name() { return "GPS"; } }; -/// Identity conversion — zero cost. -template struct TimeConvertTraits { - static double convert(double v) { return v; } -}; +template inline constexpr tempoch_scale_tag_t scale_tag_v = ScaleTraits::ffi_tag; +template inline constexpr tempoch_format_tag_t format_tag_v = FormatTraits::ffi_tag; } // namespace tempoch diff --git a/include/tempoch/tempoch.hpp b/include/tempoch/tempoch.hpp index b41822c..877c504 100644 --- a/include/tempoch/tempoch.hpp +++ b/include/tempoch/tempoch.hpp @@ -6,12 +6,12 @@ * * Include this single header to get the full tempoch C++ API: * - * - `tempoch::Time` — generic time-point template (core) - * - `tempoch::JulianDate` = `Time` - * - `tempoch::MJD` = `Time` - * - `tempoch::CivilTime` — UTC civil date-time breakdown - * - `tempoch::UTC` — alias for `CivilTime` - * - `tempoch::Period` — time period [start, end] with list operations + * - `tempoch::Time` — split-storage instant on scale `S` + * - `tempoch::JulianDate` — JD encoding on scale `S` + * - `tempoch::ModifiedJulianDate` + * - `tempoch::CivilTime` — civil UTC calendar label + * - `tempoch::TimeContext` — explicit UT1 / historical UTC context + * - `tempoch::Period` — time period [start, end] with list operations * - `tempoch::EopValues` — IERS Earth Orientation Parameter values * - `tempoch::eop_at()` — interpolate EOP at a UTC MJD * - `tempoch::eop_covers()` — check EOP data availability @@ -20,8 +20,10 @@ * @code * #include * - * auto jd = tempoch::JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - * auto mjd = jd.to(); // cross-scale conversion + * auto utc = tempoch::Time::from_civil({2026, 7, 15, 22, 0, 0}); + * auto tt = utc.to(); + * auto jd = tt.to(); + * auto mjd = tt.to(); * * if (auto eop = tempoch::eop_at(mjd.value())) * std::cout << "DUT1 = " << eop->ut1_minus_utc << " s\n"; diff --git a/include/tempoch/time.hpp b/include/tempoch/time.hpp index e1532de..3d8da41 100644 --- a/include/tempoch/time.hpp +++ b/include/tempoch/time.hpp @@ -2,59 +2,18 @@ /** * @file time.hpp - * @brief Public type aliases for tempoch time types. - * - * All implementation lives in `time_base.hpp` (the `Time` class template) - * and `scales.hpp` (scale tags and traits). This header provides the - * backward-compatible names that the rest of the codebase expects: - * - * - `tempoch::JulianDate` = `Time` - * - `tempoch::MJD` = `Time` - * - `tempoch::UTC` = `CivilTime` (civil date-time breakdown) - * - `tempoch::CivilTime` (canonical name for the civil struct) + * @brief Public aliases for tempoch time encodings. */ #include "time_base.hpp" namespace tempoch { -/// Julian Date — days since −4712-01-01T12:00 TT. -using JulianDate = Time; +template using JulianDate = EncodedTime; +template using ModifiedJulianDate = EncodedTime; +template using J2000Seconds = EncodedTime; -/// Modified Julian Date — JD − 2 400 000.5. -using MJD = Time; - -/// Barycentric Dynamical Time. -using TDB = Time; - -/// Terrestrial Time. -using TT = Time; - -/// International Atomic Time. -using TAI = Time; - -/// Geocentric Coordinate Time. -using TCG = Time; - -/// Barycentric Coordinate Time. -using TCB = Time; - -/// GPS Time. -using GPS = Time; - -/// Universal Time (UT1). -using UT = Time; - -/// Alias — mirrors Rust's `UniversalTime`. -using UniversalTime = Time; - -/// Julian Ephemeris Date (≡ TDB expressed as JD). -using JDE = Time; - -/// Unix (POSIX) time — seconds since 1970-01-01T00:00:00 UTC. -using UnixTime = Time; - -// `UTC` and `CivilTime` are already declared in time_base.hpp: -// using UTC = CivilTime; +using UnixTime = EncodedTime; +using GpsTime = EncodedTime; } // namespace tempoch diff --git a/include/tempoch/time_base.hpp b/include/tempoch/time_base.hpp index 3603b2e..6a7d6ab 100644 --- a/include/tempoch/time_base.hpp +++ b/include/tempoch/time_base.hpp @@ -2,243 +2,384 @@ /** * @file time_base.hpp - * @brief Core `Time` class template and `CivilTime` struct. - * - * Mirrors the Rust `tempoch_core::instant::Time` design: - * - A single `double` in the scale's native scalar representation - * (days for astronomical scales, seconds for Unix time) with compile-time - * dispatch via `TimeScaleTraits`. - * - Cross-scale `.to()` via `TimeConvertTraits`. - * - `CivilTime` replaces the old `UTC` aggregate struct. - * - JD-specific extras (`J2000()`, `julian_centuries()`) are enabled only - * for `Time` via SFINAE. + * @brief Core `Time`, `EncodedTime`, and `TimeContext` types. */ #include "civil_time.hpp" -#include "qtty/qtty.hpp" +#include "ffi_core.hpp" #include "scales.hpp" -#include +#include +#include +#include #include #include +#include namespace tempoch { +template class Time; +template class EncodedTime; +template struct TimeTraits; + +namespace detail { + +struct ContextDeleter { + void operator()(tempoch_context_t *ptr) const noexcept { tempoch_context_free(ptr); } +}; + +inline std::shared_ptr make_default_context() { + tempoch_context_t *raw = nullptr; + check_status(tempoch_context_create_default(&raw), "tempoch_context_create_default"); + return std::shared_ptr(raw, ContextDeleter{}); +} + +inline std::shared_ptr make_builtin_eop_context() { + tempoch_context_t *raw = nullptr; + check_status(tempoch_context_create_with_builtin_eop(&raw), + "tempoch_context_create_with_builtin_eop"); + return std::shared_ptr(raw, ContextDeleter{}); +} + +inline std::shared_ptr +make_pre_definition_context(const tempoch_context_t *parent) { + tempoch_context_t *raw = nullptr; + check_status(tempoch_context_allow_pre_definition_utc(parent, &raw), + "tempoch_context_allow_pre_definition_utc"); + return std::shared_ptr(raw, ContextDeleter{}); +} + +inline tempoch_time_t make_time(double hi_seconds, double lo_seconds) { + tempoch_time_t out{}; + check_status(tempoch_time_new(hi_seconds, lo_seconds, &out), "tempoch_time_new"); + return out; +} + +template +inline tempoch_time_t scale_convert(const tempoch_time_t &value, const tempoch_context_t *ctx) { + tempoch_time_t out{}; + check_status(tempoch_time_scale_convert(value, static_cast(scale_tag_v), + static_cast(scale_tag_v), ctx, &out), + "tempoch_time_scale_convert"); + return out; +} + +template +inline double encode_time(const tempoch_time_t &value, const tempoch_context_t *ctx) { + double out = 0.0; + check_status(tempoch_time_to_format(value, static_cast(scale_tag_v), + static_cast(format_tag_v), ctx, &out), + "tempoch_time_to_format"); + return out; +} + +template +inline tempoch_time_t decode_time(double raw, const tempoch_context_t *ctx) { + tempoch_time_t out{}; + check_status(tempoch_time_from_format(raw, static_cast(scale_tag_v), + static_cast(format_tag_v), ctx, &out), + "tempoch_time_from_format"); + return out; +} + +inline tempoch_time_t time_from_civil(const CivilTime &civil, const tempoch_context_t *ctx) { + tempoch_time_t out{}; + check_status(tempoch_time_from_civil(civil.to_c(), ctx, &out), "tempoch_time_from_civil"); + return out; +} + +inline CivilTime time_to_civil(const tempoch_time_t &value, const tempoch_context_t *ctx) { + tempoch_utc_t out{}; + check_status(tempoch_time_to_civil(value, ctx, &out), "tempoch_time_to_civil"); + return CivilTime::from_c(out); +} + +template inline tempoch_time_t add_seconds(const tempoch_time_t &value, const Q &qty) { + tempoch_time_t out{}; + qtty_quantity_t raw{qty.value(), qtty::UnitTraits::unit_id()}; + check_status(tempoch_time_add_seconds(value, raw, &out), "tempoch_time_add_seconds"); + return out; +} + +inline qtty::Second difference_seconds(const tempoch_time_t &lhs, const tempoch_time_t &rhs) { + double out = 0.0; + check_status(tempoch_time_difference_seconds(lhs, rhs, &out), "tempoch_time_difference_seconds"); + return qtty::Second(out); +} + +template inline typename FormatTraits::quantity_type quantity_from_raw(double raw) { + return typename FormatTraits::quantity_type(raw); +} + +template inline void ensure_finite_encoded(double raw, const char *operation) { + if (!std::isfinite(raw)) + throw ConversionFailedError(std::string(operation) + " failed: non-finite raw value"); +} + +} // namespace detail + /** - * @brief A point in time on scale @p S, stored as a raw native scalar. - * - * Mirrors `tempoch_core::instant::Time`. Most operations are - * dispatched through `TimeScaleTraits`, keeping this class small and - * reusable across all scales. - * - * @tparam S A scale tag for which `TimeScaleTraits` is specialised. - * - * @code - * using JulianDate = tempoch::Time; - * auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - * auto mjd = jd.to(); - * @endcode + * @brief Immutable conversion context for UT1 and historical UTC routes. + */ +class TimeContext { + std::shared_ptr handle_; + + explicit TimeContext(std::shared_ptr handle) : handle_(std::move(handle)) {} + +public: + TimeContext() : handle_(detail::make_default_context()) {} + + static TimeContext with_builtin_eop() { return TimeContext(detail::make_builtin_eop_context()); } + + TimeContext allow_pre_definition_utc() const { + return TimeContext(detail::make_pre_definition_context(handle_.get())); + } + + const tempoch_context_t *get() const noexcept { return handle_.get(); } +}; + +/** + * @brief A point in time on scale @p S, stored as a split J2000-second pair. */ template class Time { - double m_days; + static_assert(is_scale_v, "Time requires a valid tempoch::scale tag"); + + tempoch_time_t raw_; + + explicit Time(const tempoch_time_t &raw) : raw_(raw) {} + + template friend class Time; + template friend class EncodedTime; public: using scale_type = S; - // ── Constructors ────────────────────────────────────────────────────── - - /// Construct from a raw value in this scale's native representation. - constexpr explicit Time(double value) : m_days(value) {} - - /// Default-construct to zero in the scale's native representation. - constexpr Time() : m_days(0.0) {} - - // Rule of Five (trivial — all defaulted) - Time(const Time &) = default; - Time(Time &&) noexcept = default; - Time &operator=(const Time &) = default; - Time &operator=(Time &&) noexcept = default; - ~Time() = default; + Time() : raw_(detail::make_time(0.0, 0.0)) {} + + static Time from_split_seconds(qtty::Second hi, qtty::Second lo = qtty::Second(0.0)) { + return Time(detail::make_time(hi.value(), lo.value())); + } + + static Time from_raw_j2000_seconds(qtty::Second seconds) { return from_split_seconds(seconds); } - // ── Factory: from civil time ────────────────────────────────────────── + std::pair split_seconds() const noexcept { + return {qtty::Second(raw_.hi_seconds), qtty::Second(raw_.lo_seconds)}; + } - /** - * @brief Create from a UTC civil-time breakdown. - * - * Accepts brace-initialised `CivilTime`, e.g.: - * @code - * auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - * @endcode - */ - static Time from_utc(const CivilTime &ct) { return Time(TimeScaleTraits::from_civil(ct)); } + qtty::Second total_seconds() const noexcept { + return qtty::Second(raw_.hi_seconds + raw_.lo_seconds); + } - // ── Accessors ───────────────────────────────────────────────────────── + const tempoch_time_t &c_inner() const noexcept { return raw_; } - /// Raw value in this scale's native representation. - constexpr double value() const noexcept { return m_days; } + static constexpr const char *label() { return ScaleTraits::name(); } - /// Human-readable label for the scale (e.g. "JD", "MJD", "UTC"). - static constexpr const char *label() { return TimeScaleTraits::label(); } + template , int> = 0> + static Time from_civil(const CivilTime &civil) { + return Time(detail::time_from_civil(civil, nullptr)); + } - // ── Civil-time conversion ───────────────────────────────────────────── + template , int> = 0> + static Time from_civil(const CivilTime &civil, const TimeContext &ctx) { + return Time(detail::time_from_civil(civil, ctx.get())); + } - /// Convert to a UTC civil-time breakdown. - CivilTime to_utc() const { return TimeScaleTraits::to_civil(m_days); } + template , int> = 0> + CivilTime to_civil() const { + return detail::time_to_civil(raw_, nullptr); + } - // ── Cross-scale conversion ──────────────────────────────────────────── + template , int> = 0> + CivilTime to_civil(const TimeContext &ctx) const { + return detail::time_to_civil(raw_, ctx.get()); + } - /** - * @brief Convert to another time scale. - * @tparam T Target scale tag (e.g. `MJDScale`). - */ - template Time to() const { - return Time(TimeConvertTraits::convert(m_days)); + template && !std::is_same_v && + !std::is_same_v, + int> = 0> + Time to() const { + return Time(detail::scale_convert(raw_, nullptr)); } - // ── Arithmetic ──────────────────────────────────────────────────────── + template && (std::is_same_v || + std::is_same_v), + int> = 0> + Time to() const = delete; + + template , int> = 0> + Time to_with(const TimeContext &ctx) const { + return Time(detail::scale_convert(raw_, ctx.get())); + } - /** - * @brief Advance by a typed time quantity. - * - * Accepts any qtty time unit; the value is converted to days internally. - * @code - * auto t2 = jd + qtty::Day(365.25); - * auto t3 = mjd + qtty::Hour(12.0); - * auto t4 = mjd + 30.0_min; // using qtty::literals - * @endcode - */ - template Time operator+(const qtty::Quantity &delta) const { - qtty_quantity_t qty{delta.value(), qtty::UnitTraits::unit_id()}; - double result; - TimeScaleTraits::add_qty(m_days, qty, result); - return Time(result); + template , int> = 0> + EncodedTime to() const { + return EncodedTime(detail::quantity_from_raw( + detail::encode_time(raw_, nullptr))); } - /** - * @brief Retreat by a typed time quantity. - */ - template Time operator-(const qtty::Quantity &delta) const { - qtty_quantity_t neg_qty{-delta.value(), qtty::UnitTraits::unit_id()}; - double result; - TimeScaleTraits::add_qty(m_days, neg_qty, result); - return Time(result); + template , int> = 0> + EncodedTime to_with(const TimeContext &ctx) const { + return EncodedTime(detail::quantity_from_raw( + detail::encode_time(raw_, ctx.get()))); } - /** - * @brief Elapsed duration between two instants, returned as `qtty::Day`. - * - * Convert to other units with `.to()` etc. - * @code - * qtty::Day d = t2 - t1; - * qtty::Hour h = (t2 - t1).to(); - * @endcode - */ - qtty::Day operator-(const Time &other) const { - auto qty = TimeScaleTraits::difference_qty(m_days, other.m_days); - return qtty::Day(qty.value); + template , int> = 0> + std::optional> try_to() const { + try { + return to_with(TimeContext()); + } catch (const std::exception &) { + return std::nullopt; + } } - // ── Comparisons ─────────────────────────────────────────────────────── + template , int> = 0> + std::optional> try_to() const { + try { + return to_with(TimeContext()); + } catch (const std::exception &) { + return std::nullopt; + } + } - bool operator==(const Time &o) const noexcept { return m_days == o.m_days; } - bool operator!=(const Time &o) const noexcept { return m_days != o.m_days; } - bool operator<(const Time &o) const noexcept { return m_days < o.m_days; } - bool operator<=(const Time &o) const noexcept { return m_days <= o.m_days; } - bool operator>(const Time &o) const noexcept { return m_days > o.m_days; } - bool operator>=(const Time &o) const noexcept { return m_days >= o.m_days; } + template Time operator+(const Q &delta) const { + return Time(detail::add_seconds(raw_, delta)); + } - // ── JD-only extras (SFINAE-guarded) ─────────────────────────────────── + template Time operator-(const Q &delta) const { return *this + Q(-delta.value()); } - /// J2000.0 epoch (JD 2 451 545.0). Only available for `Time`. - template static std::enable_if_t, Time> J2000() { - return Time(TimeScaleTraits::j2000()); + qtty::Second operator-(const Time &other) const { + return detail::difference_seconds(raw_, other.raw_); } - /// Julian centuries since J2000. Only available for `Time`. - template - std::enable_if_t, double> julian_centuries() const { - return TimeScaleTraits::julian_centuries(m_days); + bool operator==(const Time &other) const noexcept { + return raw_.hi_seconds == other.raw_.hi_seconds && raw_.lo_seconds == other.raw_.lo_seconds; } - /// Julian centuries since J2000 as a typed quantity. - /// Only for `Time`. - template - std::enable_if_t, qtty::JulianCentury> - julian_centuries_qty() const { - auto qty = TimeScaleTraits::julian_centuries_qty(m_days); - return qtty::JulianCentury(qty.value); + bool operator!=(const Time &other) const noexcept { return !(*this == other); } + + bool operator<(const Time &other) const noexcept { + return raw_.hi_seconds < other.raw_.hi_seconds || + (raw_.hi_seconds == other.raw_.hi_seconds && raw_.lo_seconds < other.raw_.lo_seconds); } - // ── JD ↔ MJD convenience (preserves old API surface) ────────────────── + bool operator<=(const Time &other) const noexcept { return !(other < *this); } + bool operator>(const Time &other) const noexcept { return other < *this; } + bool operator>=(const Time &other) const noexcept { return !(*this < other); } +}; + +template inline std::ostream &operator<<(std::ostream &os, const Time &time) { + return os << Time::label() << " " << time.total_seconds().value() << " s"; +} + +/** + * @brief A typed external encoding of a time instant on scale @p S. + */ +template class EncodedTime { + static_assert(is_scale_v, "EncodedTime requires a valid tempoch::scale tag"); + static_assert(is_format_v, "EncodedTime requires a valid tempoch::format tag"); + +public: + using scale_type = S; + using format_type = F; + using quantity_type = typename FormatTraits::quantity_type; - /// Convert to MJD double. Only available for `Time`. - template std::enable_if_t, double> to_mjd() const { - return TimeConvertTraits::convert(m_days); +private: + quantity_type raw_; + +public: + EncodedTime() : raw_(0.0) {} + + explicit EncodedTime(quantity_type raw) : raw_(raw) { + detail::ensure_finite_encoded(raw_.value(), "EncodedTime::EncodedTime"); } - /// Create from a JulianDate. Only available for `Time`. - template - static std::enable_if_t, Time> - from_jd(const Time &jd) { - return Time(TimeConvertTraits::convert(jd.value())); + explicit EncodedTime(double value) : EncodedTime(quantity_type(value)) {} + + static std::optional try_new(quantity_type raw) { + if (!std::isfinite(raw.value())) + return std::nullopt; + return EncodedTime(raw); } - /// Convert to JulianDate. Only available for `Time`. - template - std::enable_if_t, Time> to_jd() const { - return Time(TimeConvertTraits::convert(m_days)); - } + static EncodedTime from_raw_unchecked(quantity_type raw) { return EncodedTime(raw); } - // ── UT-only extras (SFINAE-guarded) ─────────────────────────────────── + quantity_type raw() const noexcept { return raw_; } + quantity_type quantity() const noexcept { return raw_; } + double value() const noexcept { return raw_.value(); } - /// ΔT = TT − UT1 in seconds. Only available for `Time`. - template - std::enable_if_t, qtty::Second> delta_t() const { - double jd = TimeConvertTraits::convert(m_days); - double seconds = 0.0; - check_status(tempoch_delta_t_seconds_checked(jd, &seconds), "tempoch_delta_t_seconds_checked"); - return qtty::Second(seconds); + template , int> = 0> + static EncodedTime J2000() { + return EncodedTime(tempoch_const_j2000_jd_tt()); } -}; -// ============================================================================ -// operator<< — streams "