From e20e886fbc092276ba8c5945f1fee2f6915e313f Mon Sep 17 00:00:00 2001 From: VPRamon Date: Tue, 24 Feb 2026 01:33:17 +0100 Subject: [PATCH 1/5] feat: implement Time class template and CivilTime struct; update time-related headers and tests --- include/tempoch/period.hpp | 44 ++--- include/tempoch/scales.hpp | 168 ++++++++++++++++++ include/tempoch/tempoch.hpp | 20 ++- include/tempoch/time.hpp | 242 ++----------------------- include/tempoch/time_base.hpp | 326 ++++++++++++++++++++++++++++++++++ tests/test_time.cpp | 8 +- 6 files changed, 543 insertions(+), 265 deletions(-) create mode 100644 include/tempoch/scales.hpp create mode 100644 include/tempoch/time_base.hpp diff --git a/include/tempoch/period.hpp b/include/tempoch/period.hpp index 65ab3ef..8cfb85d 100644 --- a/include/tempoch/period.hpp +++ b/include/tempoch/period.hpp @@ -8,6 +8,10 @@ * 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`). */ #include "qtty/qtty.hpp" @@ -18,32 +22,18 @@ namespace tempoch { // ============================================================================ -// TimeTraits — connect each time type to the MJD-based FFI layer +// TimeTraits — CivilTime specialisation // ============================================================================ +// TimeTraits> for all scale-based types is already defined in +// time_base.hpp. We only need the CivilTime-specific one here. -/** - * @brief Conversion traits between a time type @p T and raw MJD doubles. - * - * Specialise this struct to make `Period` work for a custom time type. - * Each specialisation must provide: - * - `static double to_mjd_value(const T&)` - * - `static T from_mjd_value(double)` - */ -template struct TimeTraits; - -template <> struct TimeTraits { - static double to_mjd_value(const MJD &t) { return t.value(); } - static MJD from_mjd_value(double m) { return MJD(m); } -}; - -template <> struct TimeTraits { - static double to_mjd_value(const JulianDate &t) { return t.to_mjd(); } - static JulianDate from_mjd_value(double m) { return MJD(m).to_jd(); } -}; - -template <> struct TimeTraits { - static double to_mjd_value(const UTC &t) { return MJD::from_utc(t).value(); } - static UTC from_mjd_value(double m) { return MJD(m).to_utc(); } +template <> struct TimeTraits { + static double to_mjd_value(const CivilTime &t) { + return TimeScaleTraits::from_civil(t); + } + static CivilTime from_mjd_value(double m) { + return TimeScaleTraits::to_civil(m); + } }; // ============================================================================ @@ -160,9 +150,9 @@ 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. +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. // ============================================================================ // operator<< diff --git a/include/tempoch/scales.hpp b/include/tempoch/scales.hpp new file mode 100644 index 0000000..36b52c1 --- /dev/null +++ b/include/tempoch/scales.hpp @@ -0,0 +1,168 @@ +#pragma once + +/** + * @file scales.hpp + * @brief Time-scale tag types and traits for the tempoch Time template. + * + * Mirrors the Rust `tempoch_core::scales` module. Each tag is an empty struct + * that selects the FFI functions used by `Time`. + * + * Adding a new scale requires: + * 1. Define a tag struct (e.g. `struct TTScale {};`). + * 2. Specialise `TimeScaleTraits` with the FFI calls. + * 3. Specialise `TimeConvertTraits` for each supported conversion pair. + */ + +#include "ffi_core.hpp" // tempoch_ffi.h + check_status + +namespace tempoch { + +// Forward declaration — defined in time_base.hpp. +struct CivilTime; + +// ============================================================================ +// Scale Tags +// ============================================================================ + +/// Julian Date (days since −4712‑01‑01T12:00 TT). +struct JDScale {}; + +/// Modified Julian Date (JD − 2 400 000.5). +struct MJDScale {}; + +/// UTC, internally stored as MJD days for arithmetic. +struct UTCScale {}; + +// Stubs for future FFI-backed scales — uncomment and specialise traits once +// the FFI exposes the conversion functions. +// struct TTScale {}; // Terrestrial Time +// struct TAIScale {}; // International Atomic Time +// struct TDBScale {}; // Barycentric Dynamical Time +// struct TCGScale {}; // Geocentric Coordinate Time +// struct TCBScale {}; // Barycentric Coordinate Time +// struct GPSScale {}; // GPS Time +// struct UTScale {}; // Universal Time (UT1) + +// ============================================================================ +// 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."); +}; + +// ── JDScale ───────────────────────────────────────────────────────────────── + +template <> struct TimeScaleTraits { + static constexpr const char *label() { return "JD"; } + + static double from_civil(const CivilTime &ct); + static CivilTime to_civil(double jd); + + static double add_days(double jd, double delta) { + return tempoch_jd_add_days(jd, delta); + } + + static double difference(double a, double b) { + return tempoch_jd_difference(a, b); + } + + /// J2000.0 epoch constant (JD 2 451 545.0). + static double j2000() { return tempoch_jd_j2000(); } + + /// Julian centuries elapsed since J2000. + static double julian_centuries(double jd) { + return tempoch_jd_julian_centuries(jd); + } +}; + +// ── MJDScale ──────────────────────────────────────────────────────────────── + +template <> struct TimeScaleTraits { + static constexpr const char *label() { return "MJD"; } + + static double from_civil(const CivilTime &ct); + static CivilTime to_civil(double mjd); + + static double add_days(double mjd, double delta) { + return tempoch_mjd_add_days(mjd, delta); + } + + static double difference(double a, double b) { + return tempoch_mjd_difference(a, b); + } +}; + +// ── UTCScale (internally stored as MJD) ───────────────────────────────────── + +template <> struct TimeScaleTraits { + static constexpr const char *label() { return "UTC"; } + + static double from_civil(const CivilTime &ct); + static CivilTime to_civil(double mjd); + + static double add_days(double mjd, double delta) { + return tempoch_mjd_add_days(mjd, delta); + } + + static double difference(double a, double b) { + return tempoch_mjd_difference(a, b); + } +}; + +// ============================================================================ +// TimeConvertTraits — cross-scale conversion +// ============================================================================ + +/** + * @brief Primary template — specialise for each supported A→B pair. + * + * Required: `static double convert(double src_days)`. + */ +template struct TimeConvertTraits { + static_assert(sizeof(From) == 0, + "TimeConvertTraits is not specialised for this pair."); +}; + +// ── JD ↔ MJD ──────────────────────────────────────────────────────────────── + +template <> struct TimeConvertTraits { + static double convert(double jd) { return tempoch_jd_to_mjd(jd); } +}; + +template <> struct TimeConvertTraits { + static double convert(double mjd) { return tempoch_mjd_to_jd(mjd); } +}; + +// ── JD ↔ UTC (UTC stored as MJD internally) ──────────────────────────────── + +template <> struct TimeConvertTraits { + static double convert(double jd) { return tempoch_jd_to_mjd(jd); } +}; + +template <> struct TimeConvertTraits { + static double convert(double mjd) { return tempoch_mjd_to_jd(mjd); } +}; + +// ── MJD ↔ UTC (identity — both stored as MJD) ────────────────────────────── + +template <> struct TimeConvertTraits { + static double convert(double mjd) { return mjd; } +}; + +template <> struct TimeConvertTraits { + static double convert(double mjd) { return mjd; } +}; + +} // namespace tempoch diff --git a/include/tempoch/tempoch.hpp b/include/tempoch/tempoch.hpp index 84ea18b..50f8292 100644 --- a/include/tempoch/tempoch.hpp +++ b/include/tempoch/tempoch.hpp @@ -5,20 +5,24 @@ * @brief Umbrella header for the tempoch C++ wrapper library. * * Include this single header to get the full tempoch C++ API: - * - `tempoch::UTC` — UTC date-time breakdown - * - `tempoch::JulianDate` — Julian Date wrapper - * - `tempoch::MJD` — Modified Julian Date wrapper - * - `tempoch::Period` — Time period [start, end] in MJD + * + * - `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] * * @code * #include * - * auto jd = tempoch::JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); - * auto mjd = tempoch::MJD::from_jd(jd); - * tempoch::Period night(60200.0, 60200.5); + * auto jd = tempoch::JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + * auto mjd = jd.to(); // cross-scale conversion * @endcode */ #include "ffi_core.hpp" -#include "period.hpp" +#include "scales.hpp" +#include "time_base.hpp" #include "time.hpp" +#include "period.hpp" diff --git a/include/tempoch/time.hpp b/include/tempoch/time.hpp index e92ff26..d99cad3 100644 --- a/include/tempoch/time.hpp +++ b/include/tempoch/time.hpp @@ -2,239 +2,29 @@ /** * @file time.hpp - * @brief C++ wrappers for Julian Date, Modified Julian Date, and UTC. + * @brief Public type aliases for tempoch time types. * - * Wraps the tempoch-ffi C layer with value-semantic, exception-safe C++ types. - */ - -#include "ffi_core.hpp" -#include -#include - -namespace tempoch { - -// ============================================================================ -// UTC -// ============================================================================ - -/** - * @brief UTC date-time breakdown. - * - * A simple value type mirroring the C `tempoch_utc_t` struct. - * - * @code - * tempoch::UTC noon(2000, 1, 1, 12, 0, 0); - * auto jd = tempoch::JulianDate::from_utc(noon); - * @endcode - */ -struct UTC { - /// Gregorian year (astronomical year numbering). - int32_t year; - /// Month in range [1, 12]. - uint8_t month; - /// Day of month in range [1, 31]. - uint8_t day; - /// Hour in range [0, 23]. - uint8_t hour; - /// Minute in range [0, 59]. - uint8_t minute; - /// Second in range [0, 60], leap second aware. - uint8_t second; - /// Nanosecond component in range [0, 999,999,999]. - uint32_t nanosecond; - - /// Default constructor: J2000 epoch noon-like civil representation. - UTC() - : year(2000), month(1), day(1), hour(12), minute(0), second(0), - nanosecond(0) {} - - /** - * @brief Construct from civil UTC components. - * @param y Year. - * @param mo Month [1, 12]. - * @param d Day [1, 31]. - * @param h Hour [0, 23]. - * @param mi Minute [0, 59]. - * @param s Second [0, 60]. - * @param ns Nanoseconds [0, 999,999,999]. - */ - UTC(int32_t y, uint8_t mo, uint8_t d, uint8_t h = 0, uint8_t mi = 0, - uint8_t s = 0, uint32_t ns = 0) - : year(y), month(mo), day(d), hour(h), minute(mi), second(s), - nanosecond(ns) {} - - /// Convert to the C FFI struct. - tempoch_utc_t to_c() const { - return {year, month, day, hour, minute, second, nanosecond}; - } - - /// Create from the C FFI struct. - static UTC from_c(const tempoch_utc_t &c) { - return UTC(c.year, c.month, c.day, c.hour, c.minute, c.second, - c.nanosecond); - } -}; - -// ============================================================================ -// JulianDate -// ============================================================================ - -/** - * @brief Julian Date wrapper (value type). - * - * Wraps a raw `double` Julian Date value and provides conversions to/from UTC, - * MJD, and arithmetic operations. - * - * @code - * auto jd = tempoch::JulianDate::J2000(); // 2451545.0 - * auto utc = jd.to_utc(); - * auto jd2 = jd + 365.25; // add one Julian year - * double centuries = jd.julian_centuries(); // since J2000 - * @endcode - */ -class JulianDate { - double m_value; - -public: - constexpr explicit JulianDate(double v) : m_value(v) {} - - /// J2000.0 epoch (2451545.0). - static JulianDate J2000() { return JulianDate(tempoch_jd_j2000()); } - - /// Create from a UTC date-time. - static JulianDate from_utc(const UTC &utc) { - double jd; - auto c = utc.to_c(); - check_status(tempoch_jd_from_utc(c, &jd), "JulianDate::from_utc"); - return JulianDate(jd); - } - - /// Raw value. - constexpr double value() const { return m_value; } - - /// Convert to MJD. - double to_mjd() const { return tempoch_jd_to_mjd(m_value); } - - /// Convert to UTC. - UTC to_utc() const { - tempoch_utc_t out; - check_status(tempoch_jd_to_utc(m_value, &out), "JulianDate::to_utc"); - return UTC::from_c(out); - } - - /// Difference in days (this – other). - double operator-(const JulianDate &other) const { - return tempoch_jd_difference(m_value, other.m_value); - } - - /// Add days. - JulianDate operator+(double days) const { - return JulianDate(tempoch_jd_add_days(m_value, days)); - } - - /// Julian centuries since J2000. - double julian_centuries() const { - return tempoch_jd_julian_centuries(m_value); - } - - bool operator==(const JulianDate &o) const { return m_value == o.m_value; } - bool operator!=(const JulianDate &o) const { return m_value != o.m_value; } - bool operator<(const JulianDate &o) const { return m_value < o.m_value; } - bool operator<=(const JulianDate &o) const { return m_value <= o.m_value; } - bool operator>(const JulianDate &o) const { return m_value > o.m_value; } - bool operator>=(const JulianDate &o) const { return m_value >= o.m_value; } -}; - -/// Stream a JulianDate as its raw double value. -inline std::ostream &operator<<(std::ostream &os, const JulianDate &jd) { - return os << jd.value(); -} - -// ============================================================================ -// MJD (Modified Julian Date) -// ============================================================================ - -/** - * @brief Modified Julian Date wrapper (value type). - * - * Wraps a raw `double` MJD value and provides conversions to/from UTC, JD, - * and arithmetic operations. + * 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: * - * @code - * auto mjd = tempoch::MJD::from_utc({2026, 7, 15}); - * auto jd = mjd.to_jd(); - * auto mjd2 = mjd + 1.0; // next day - * @endcode + * - `tempoch::JulianDate` = `Time` + * - `tempoch::MJD` = `Time` + * - `tempoch::UTC` = `CivilTime` (civil date-time breakdown) + * - `tempoch::CivilTime` (canonical name for the civil struct) */ -class MJD { - double m_value; - -public: - constexpr MJD() : m_value(0.0) {} - constexpr explicit MJD(double v) : m_value(v) {} - - /// Create from a UTC date-time. - static MJD from_utc(const UTC &utc) { - double mjd; - auto c = utc.to_c(); - check_status(tempoch_mjd_from_utc(c, &mjd), "MJD::from_utc"); - return MJD(mjd); - } - /// Create from a Julian Date. - static MJD from_jd(const JulianDate &jd) { - return MJD(tempoch_jd_to_mjd(jd.value())); - } +#include "time_base.hpp" - /// Raw value. - constexpr double value() const { return m_value; } - - /// Convert to JD. - JulianDate to_jd() const { return JulianDate(tempoch_mjd_to_jd(m_value)); } - - /// Convert to UTC. - UTC to_utc() const { - tempoch_utc_t out; - check_status(tempoch_mjd_to_utc(m_value, &out), "MJD::to_utc"); - return UTC::from_c(out); - } - - /// Difference in days (this – other). - double operator-(const MJD &other) const { - return tempoch_mjd_difference(m_value, other.m_value); - } - - /// Add days. - MJD operator+(double days) const { - return MJD(tempoch_mjd_add_days(m_value, days)); - } +namespace tempoch { - bool operator==(const MJD &o) const { return m_value == o.m_value; } - bool operator!=(const MJD &o) const { return m_value != o.m_value; } - bool operator<(const MJD &o) const { return m_value < o.m_value; } - bool operator<=(const MJD &o) const { return m_value <= o.m_value; } - bool operator>(const MJD &o) const { return m_value > o.m_value; } - bool operator>=(const MJD &o) const { return m_value >= o.m_value; } -}; +/// Julian Date — days since −4712-01-01T12:00 TT. +using JulianDate = Time; -/// Stream a MJD as its raw double value. -inline std::ostream &operator<<(std::ostream &os, const MJD &mjd) { - return os << mjd.value(); -} +/// Modified Julian Date — JD − 2 400 000.5. +using MJD = Time; -/// Stream a UTC date-time as YYYY-MM-DD HH:MM:SS[.nnnnnnnnn]. -inline std::ostream &operator<<(std::ostream &os, const UTC &u) { - const char prev_fill = os.fill(); - os << u.year << '-' << std::setfill('0') << std::setw(2) - << static_cast(u.month) << '-' << std::setw(2) - << static_cast(u.day) << ' ' << std::setw(2) - << static_cast(u.hour) << ':' << std::setw(2) - << static_cast(u.minute) << ':' << std::setw(2) - << static_cast(u.second); - if (u.nanosecond != 0) - os << '.' << std::setw(9) << u.nanosecond; - os.fill(prev_fill); - return os; -} +// `UTC` and `CivilTime` are already declared in time_base.hpp: +// using UTC = CivilTime; } // namespace tempoch diff --git a/include/tempoch/time_base.hpp b/include/tempoch/time_base.hpp new file mode 100644 index 0000000..17aafd2 --- /dev/null +++ b/include/tempoch/time_base.hpp @@ -0,0 +1,326 @@ +#pragma once + +/** + * @file time_base.hpp + * @brief Core `Time` class template and `CivilTime` struct. + * + * Mirrors the Rust `tempoch_core::instant::Time` design: + * - A single `double` (days in the scale's epoch) with compile-time scale + * 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. + */ + +#include "scales.hpp" +#include "qtty/qtty.hpp" +#include +#include +#include + +namespace tempoch { + +// ============================================================================ +// CivilTime (civil UTC breakdown — formerly the `UTC` struct) +// ============================================================================ + +/** + * @brief UTC date-time breakdown. + * + * A simple value type mirroring the C `tempoch_utc_t` struct. + * + * @code + * tempoch::CivilTime noon(2000, 1, 1, 12, 0, 0); + * auto jd = tempoch::JulianDate::from_utc(noon); + * @endcode + */ +struct CivilTime { + int32_t year; ///< Gregorian year (astronomical year numbering). + uint8_t month; ///< Month [1, 12]. + uint8_t day; ///< Day of month [1, 31]. + uint8_t hour; ///< Hour [0, 23]. + uint8_t minute; ///< Minute [0, 59]. + uint8_t second; ///< Second [0, 60] (leap-second aware). + uint32_t nanosecond; ///< Nanosecond [0, 999 999 999]. + + /// Default constructor: J2000 epoch noon-like civil representation. + CivilTime() + : year(2000), month(1), day(1), hour(12), minute(0), second(0), + nanosecond(0) {} + + /** + * @brief Construct from civil UTC components. + */ + CivilTime(int32_t y, uint8_t mo, uint8_t d, uint8_t h = 0, uint8_t mi = 0, + uint8_t s = 0, uint32_t ns = 0) + : year(y), month(mo), day(d), hour(h), minute(mi), second(s), + nanosecond(ns) {} + + /// Convert to the C FFI struct. + tempoch_utc_t to_c() const { + return {year, month, day, hour, minute, second, nanosecond}; + } + + /// Create from the C FFI struct. + static CivilTime from_c(const tempoch_utc_t &c) { + return CivilTime(c.year, c.month, c.day, c.hour, c.minute, c.second, + c.nanosecond); + } +}; + +/// Stream CivilTime as YYYY-MM-DD HH:MM:SS[.nnnnnnnnn]. +inline std::ostream &operator<<(std::ostream &os, const CivilTime &u) { + const char prev = os.fill(); + os << u.year << '-' << std::setfill('0') << std::setw(2) + << static_cast(u.month) << '-' << std::setw(2) + << static_cast(u.day) << ' ' << std::setw(2) + << static_cast(u.hour) << ':' << std::setw(2) + << static_cast(u.minute) << ':' << std::setw(2) + << static_cast(u.second); + if (u.nanosecond != 0) + os << '.' << std::setw(9) << u.nanosecond; + os.fill(prev); + return os; +} + +// ============================================================================ +// TimeScaleTraits deferred implementations (need CivilTime to be complete) +// ============================================================================ + +inline double TimeScaleTraits::from_civil(const CivilTime &ct) { + double jd; + auto c = ct.to_c(); + check_status(tempoch_jd_from_utc(c, &jd), "Time::from_utc"); + return jd; +} + +inline CivilTime TimeScaleTraits::to_civil(double jd) { + tempoch_utc_t out; + check_status(tempoch_jd_to_utc(jd, &out), "Time::to_utc"); + return CivilTime::from_c(out); +} + +inline double TimeScaleTraits::from_civil(const CivilTime &ct) { + double mjd; + auto c = ct.to_c(); + check_status(tempoch_mjd_from_utc(c, &mjd), "Time::from_utc"); + return mjd; +} + +inline CivilTime TimeScaleTraits::to_civil(double mjd) { + tempoch_utc_t out; + check_status(tempoch_mjd_to_utc(mjd, &out), "Time::to_utc"); + return CivilTime::from_c(out); +} + +// UTCScale delegates to MJDScale (same internal representation). +inline double TimeScaleTraits::from_civil(const CivilTime &ct) { + return TimeScaleTraits::from_civil(ct); +} + +inline CivilTime TimeScaleTraits::to_civil(double mjd) { + return TimeScaleTraits::to_civil(mjd); +} + +// ============================================================================ +// Time — the core template +// ============================================================================ + +/** + * @brief A point in time on scale @p S, stored as a raw `double` (days). + * + * 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 + */ +template class Time { + double m_days; + +public: + using scale_type = S; + + // ── Constructors ────────────────────────────────────────────────────── + + /// Construct from a raw day count in this scale. + constexpr explicit Time(double days) : m_days(days) {} + + /// Default-construct to zero (MJD epoch). + constexpr Time() : m_days(0.0) {} + + // ── Factory: from civil time ────────────────────────────────────────── + + /** + * @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)); + } + + // ── Accessors ───────────────────────────────────────────────────────── + + /// Raw value in this scale's day-count. + constexpr double value() const { return m_days; } + + /// Human-readable label for the scale (e.g. "JD", "MJD", "UTC"). + static constexpr const char *label() { return TimeScaleTraits::label(); } + + // ── Civil-time conversion ───────────────────────────────────────────── + + /// Convert to a UTC civil-time breakdown. + CivilTime to_utc() const { return TimeScaleTraits::to_civil(m_days); } + + // ── Cross-scale conversion ──────────────────────────────────────────── + + /** + * @brief Convert to another time scale. + * @tparam T Target scale tag (e.g. `MJDScale`). + */ + template Time to() const { + return Time(TimeConvertTraits::convert(m_days)); + } + + // ── Arithmetic ──────────────────────────────────────────────────────── + + /** + * @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 { + return Time(TimeScaleTraits::add_days( + m_days, delta.template to().value())); + } + + /** + * @brief Retreat by a typed time quantity. + */ + template + Time operator-(const qtty::Quantity &delta) const { + return Time(TimeScaleTraits::add_days( + m_days, -delta.template to().value())); + } + + /** + * @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 { + return qtty::Day(TimeScaleTraits::difference(m_days, other.m_days)); + } + + // ── Comparisons ─────────────────────────────────────────────────────── + + bool operator==(const Time &o) const { return m_days == o.m_days; } + bool operator!=(const Time &o) const { return m_days != o.m_days; } + bool operator<(const Time &o) const { return m_days < o.m_days; } + bool operator<=(const Time &o) const { return m_days <= o.m_days; } + bool operator>(const Time &o) const { return m_days > o.m_days; } + bool operator>=(const Time &o) const { return m_days >= o.m_days; } + + // ── JD-only extras (SFINAE-guarded) ─────────────────────────────────── + + /// J2000.0 epoch (JD 2 451 545.0). Only available for `Time`. + template + static std::enable_if_t, Time> J2000() { + return Time(TimeScaleTraits::j2000()); + } + + /// Julian centuries since J2000. Only available for `Time`. + template + std::enable_if_t, double> + julian_centuries() const { + return TimeScaleTraits::julian_centuries(m_days); + } + + // ── JD ↔ MJD convenience (preserves old API surface) ────────────────── + + /// Convert to MJD double. Only available for `Time`. + template + std::enable_if_t, double> to_mjd() const { + return TimeConvertTraits::convert(m_days); + } + + /// 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())); + } + + /// Convert to JulianDate. Only available for `Time`. + template + std::enable_if_t, Time> + to_jd() const { + return Time( + TimeConvertTraits::convert(m_days)); + } +}; + +// ============================================================================ +// operator<< — streams "