diff --git a/.gitmodules b/.gitmodules index 657ef1c..8e93535 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "tempoch"] path = tempoch url = git@github.com:Siderust/tempoch.git + branch = helpers [submodule "qtty-cpp"] path = qtty-cpp url = https://github.com/Siderust/qtty-cpp.git diff --git a/CHANGELOG.md b/CHANGELOG.md index e414fae..ae8a5b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,19 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.0] - 2026-05-18 + +### Breaking + +- Removed `EncodedTime::to_time()` / `to_time_with()` from the public C++ API; decode scalar + transport values into canonical storage using `Time::from_encoded(encoded)` / + `Time::from_encoded_with(encoded, ctx)` (parity with Rust `Time::to_j2000s()` after dropping the + historical aliases). + ## [0.4.0] - 2026-05-15 ### Added -- `include/tempoch/legacy_time.hpp` with opt-in deprecated aliases for pre-redesign names such as `UTC`, `JulianDate`, `MJD`, `TT`, `TAI`, `TDB`, `TCG`, `TCB`, `UT`, `JDE`, and `GPS`. - New FFI exception mappings in `include/tempoch/ffi_core.hpp` for generic conversion failures and invalid format identifiers via `ConversionFailedError` and `InvalidFormatIdError`. ### Changed - Refactored the public time model around explicit physical scales and external encodings: `Time` represents canonical instants, while `EncodedTime` covers representations such as `JulianDate`, `ModifiedJulianDate`, `UnixTime`, and `GpsTime`. - `include/tempoch/time.hpp`, `time_base.hpp`, `scales.hpp`, and `period.hpp` now expose the split scale/format API, including `TimeContext` for UT1 and historical UTC conversions. -- `Period` is now generalized across supported time representations through `TimeTraits`, with the default period type moved to `Period>`. -- README, Doxygen docs, examples, and tests were migrated from implicit aliases (`UTC`, `JulianDate`, `MJD`, `TT`) to the explicit typed API and conversion flow. +- `JulianDate::from_utc(...)`, `MJD::from_utc(...)`, `to_utc()`, `to_mjd()`, `to_jd()`, and direct `+=` / `-=` arithmetic are part of the supported ergonomic surface on top of the typed core. +- `Period` remains generalized across supported time representations through `TimeTraits`. +- README, Doxygen docs, examples, and tests were updated to show the ergonomic TT-default workflow first, with the explicit typed API kept available for advanced mixed-scale use. - Updated the vendored `tempoch` and `qtty-cpp` submodules to the snapshots used by this branch. ## [0.3.1] - 2026-05-11 diff --git a/CMakeLists.txt b/CMakeLists.txt index cd7e93f..cc7c6d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15) -project(tempoch_cpp VERSION 0.4.0 LANGUAGES CXX) +project(tempoch_cpp VERSION 0.5.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -176,6 +176,7 @@ endforeach() # Test executables with Google Test set(TEST_SOURCES tests/main.cpp + tests/test_headers.cpp tests/test_time.cpp tests/test_period.cpp ) diff --git a/README.md b/README.md index 1df3e4b..0b07213 100644 --- a/README.md +++ b/README.md @@ -51,30 +51,35 @@ git submodule update --init --recursive #include #include #include +#include +#include +#include int main() { using namespace tempoch; 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_tt = JulianDate::from_utc(civil); + auto mjd_tt = ModifiedJulianDate::from_jd(jd_tt); + auto utc = Time::from_encoded(jd_tt).to(); auto jd_utc = utc.to(); - auto mjd_tt = tt.to(); 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_tt, - mjd_tt + qtty::Hour(12.0)); + Period observing_window(mjd_tt, mjd_tt + qtty::Hour(12.0)); std::cout << "Duration: " << observing_window.duration() << "\n"; } ``` +The public headers are now split by concept: + +- `tempoch/scales/*.hpp` for physical/civil time-axis tags and scale traits +- `tempoch/formats/*.hpp` for encoding tags and format traits +- `tempoch/tempoch.hpp` as the full umbrella include + ### Durations and Arithmetic Time arithmetic uses `qtty-cpp` quantities. Subtracting two times returns a typed duration, and adding or subtracting a quantity shifts the time value. @@ -87,13 +92,11 @@ auto dt = jd1 - jd0; std::cout << dt.to() << "\n"; ``` -## Migration Notes +## API 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`. +- Default astronomy-facing code should use TT-based `JulianDate`, `MJD`, and `Period`. +- Civil construction is available directly through `JulianDate::from_utc(...)` and `ModifiedJulianDate::from_utc(...)`. +- The explicit typed core remains available through `Time` and `EncodedTime` for mixed-scale work. ## Documentation diff --git a/docs/mainpage.md b/docs/mainpage.md index 5939ffd..9847e98 100644 --- a/docs/mainpage.md +++ b/docs/mainpage.md @@ -16,7 +16,7 @@ time primitives. It wraps the Rust-based | **`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 | +| **`Period`** | Half-open `[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,18 +26,19 @@ time primitives. It wraps the Rust-based ```cpp #include +#include +#include +#include #include int main() { using namespace tempoch; 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_tt = JulianDate::from_utc(civil); + auto mjd_tt = ModifiedJulianDate::from_jd(jd_tt); + auto utc = Time::from_encoded(jd_tt).to(); auto jd_utc = utc.to(); - auto mjd_tt = tt.to(); std::cout << "Civil UTC : " << civil << "\n"; std::cout << "JD(TT) : " << jd_tt << "\n"; @@ -76,17 +77,17 @@ int main() { ## Modules - `tempoch/tempoch.hpp` — umbrella include for the full public API -- `tempoch/time.hpp` — `CivilTime`, `Time`, `JulianDate`, `ModifiedJulianDate` +- `tempoch/scales/scales.hpp` — scale tag index header +- `tempoch/formats/formats.hpp` — format tag index header +- `tempoch/time.hpp` — `CivilTime`, TT-default `JulianDate` / `MJD`, explicit `Time`, and `TimeContext` - `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. +- Default astronomy-facing code should use TT-based `JulianDate`, `MJD`, and `Period`. +- Civil construction is available directly through `JulianDate::from_utc(...)` and `ModifiedJulianDate::from_utc(...)`. +- The explicit typed core remains available through `Time` and `EncodedTime` for mixed-scale work. --- diff --git a/examples/02_scales.cpp b/examples/02_scales.cpp index 6c8c73f..acf7e39 100644 --- a/examples/02_scales.cpp +++ b/examples/02_scales.cpp @@ -25,7 +25,7 @@ int main() { auto ctx = TimeContext::with_builtin_eop(); auto jd_tt = JulianDate::J2000(); - auto tt = jd_tt.to_time(); + auto tt = Time::from_encoded(jd_tt); auto tai = tt.to(); auto utc = tt.to().to_civil(); auto ut1 = tt.to_with(ctx); diff --git a/examples/03_formats.cpp b/examples/03_formats.cpp index cbf8a0e..6005b4c 100644 --- a/examples/03_formats.cpp +++ b/examples/03_formats.cpp @@ -17,34 +17,30 @@ #include #include -// Well-known fixed values (from tempoch-core::constats). -static constexpr double UNIX_EPOCH_JD = 2'440'587.5; // 1970-01-01 JD -static constexpr double UNIX_EPOCH_MJD = 40'587.0; // 1970-01-01 MJD - int main() { using namespace tempoch; // J2000 epoch as canonical TT, then viewed through encodings. - auto j2000_tt = JulianDate::J2000().to_time(); + auto j2000_tt = Time::from_encoded(JulianDate::J2000()); // Sample TT instant: J2000 + 123 456.789 s. auto sample_tt = j2000_tt + qtty::Second(123'456.789); - // J2000 from JulianDate constructor (mirrors JulianDate::::try_new(J2000_JD_TT)). + // J2000 from JD scalar (matches Rust example using `j2000_jd_tt()`). JulianDate j2000_from_jd{constants::j2000_jd_tt()}; - // Unix epoch from JD value. - JulianDate unix_epoch_jd{UNIX_EPOCH_JD}; + // Unix epoch from JD value (ABI-stable constants wrapper). + JulianDate unix_epoch_jd{constants::unix_epoch_jd()}; // Half-day after J2000 via JD. JulianDate half_day_jd{constants::j2000_jd_tt() + 0.5}; // Unix epoch from MJD value. - ModifiedJulianDate unix_epoch_mjd{UNIX_EPOCH_MJD}; + ModifiedJulianDate unix_epoch_mjd{constants::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}; - auto utc_from_ux = ux.to_time().to_civil(); + auto utc_from_ux = Time::from_encoded(ux).to_civil(); std::cout << std::fixed << std::setprecision(9); std::cout << "J2000 TT JD : " << j2000_tt.to() << "\n"; diff --git a/examples/06_runtime_tables.cpp b/examples/06_runtime_tables.cpp index dd17849..0b082d0 100644 --- a/examples/06_runtime_tables.cpp +++ b/examples/06_runtime_tables.cpp @@ -35,7 +35,7 @@ int main() { // ── Unix timestamp roundtrip ───────────────────────────────────────────── UnixTime ux{1'700'000'000.0}; - auto utc_from_ux = ux.to_time().to_civil(); + auto utc_from_ux = Time::from_encoded(ux).to_civil(); auto back = Time::from_civil(utc_from_ux).to(); std::cout << std::fixed << std::setprecision(3); diff --git a/examples/07_conversions.cpp b/examples/07_conversions.cpp index b216440..244d5f6 100644 --- a/examples/07_conversions.cpp +++ b/examples/07_conversions.cpp @@ -26,7 +26,7 @@ int main() { // Start from a Unix timestamp (mirrors UnixTime::try_new(1_700_000_000.25)). UnixTime ux{1'700'000'000.25}; - auto utc = ux.to_time().to_civil(); + auto utc = Time::from_encoded(ux).to_civil(); // Convert across continuous scales. auto tai = ux.to(); diff --git a/include/tempoch/civil_time.hpp b/include/tempoch/civil_time.hpp index 802f492..b024849 100644 --- a/include/tempoch/civil_time.hpp +++ b/include/tempoch/civil_time.hpp @@ -4,8 +4,8 @@ * @file civil_time.hpp * @brief UTC date-time breakdown struct. * - * Forward-declared in scales.hpp and fully defined here to avoid circular - * dependencies. + * Included by the public time headers and kept separate to avoid circular + * dependencies between the civil carrier and the scale/format tag headers. */ #include "ffi_core.hpp" @@ -31,7 +31,7 @@ struct CivilTime { 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, 59]. + uint8_t second; ///< Second [0, 60] (60 only during a positive leap second). uint32_t nanosecond; ///< Nanosecond [0, 999 999 999]. /// Default constructor: J2000 epoch noon-like civil representation. diff --git a/include/tempoch/constants.hpp b/include/tempoch/constants.hpp index 5205d7d..dae4cc0 100644 --- a/include/tempoch/constants.hpp +++ b/include/tempoch/constants.hpp @@ -19,6 +19,12 @@ namespace constants { /// J2000.0 epoch as a Julian Date in TT (2 451 545.0). inline double j2000_jd_tt() noexcept { return tempoch_const_j2000_jd_tt(); } +/// Unix epoch as Julian Date on the UTC axis (`1970-01-01` midnight UTC). +inline double unix_epoch_jd() noexcept { return tempoch_const_unix_epoch_jd(); } + +/// Unix epoch as Modified Julian Day on the UTC axis (`40 587.0`). +inline double unix_epoch_mjd() noexcept { return tempoch_const_unix_epoch_mjd(); } + /// Length of a Julian year in days (365.25 days). inline double julian_year_days() noexcept { return tempoch_const_julian_year_days(); } diff --git a/include/tempoch/formats/base.hpp b/include/tempoch/formats/base.hpp new file mode 100644 index 0000000..9b40f98 --- /dev/null +++ b/include/tempoch/formats/base.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "../ffi_core.hpp" +#include +#include + +namespace tempoch { + +template struct is_format : std::false_type {}; +template inline constexpr bool is_format_v = is_format::value; + +template struct FormatTraits; +template inline constexpr tempoch_format_tag_t format_tag_v = FormatTraits::ffi_tag; + +} // namespace tempoch diff --git a/include/tempoch/formats/formats.hpp b/include/tempoch/formats/formats.hpp new file mode 100644 index 0000000..152bad3 --- /dev/null +++ b/include/tempoch/formats/formats.hpp @@ -0,0 +1,12 @@ +#pragma once + +/** + * @file formats/formats.hpp + * @brief Time-format tag types for the tempoch C++ API. + */ + +#include "gps.hpp" +#include "j2000s.hpp" +#include "jd.hpp" +#include "mjd.hpp" +#include "unix.hpp" diff --git a/include/tempoch/formats/gps.hpp b/include/tempoch/formats/gps.hpp new file mode 100644 index 0000000..cdde4b3 --- /dev/null +++ b/include/tempoch/formats/gps.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace format { +struct GPS {}; +} // namespace format + +template <> struct is_format : std::true_type {}; + +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"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/formats/j2000s.hpp b/include/tempoch/formats/j2000s.hpp new file mode 100644 index 0000000..3cb5c51 --- /dev/null +++ b/include/tempoch/formats/j2000s.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace format { +struct J2000s {}; +} // namespace format + +template <> struct is_format : std::true_type {}; + +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"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/formats/jd.hpp b/include/tempoch/formats/jd.hpp new file mode 100644 index 0000000..d7624ca --- /dev/null +++ b/include/tempoch/formats/jd.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace format { +struct JD {}; +} // namespace format + +template <> struct is_format : std::true_type {}; + +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"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/formats/mjd.hpp b/include/tempoch/formats/mjd.hpp new file mode 100644 index 0000000..050b193 --- /dev/null +++ b/include/tempoch/formats/mjd.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace format { +struct MJD {}; +} // namespace format + +template <> struct is_format : std::true_type {}; + +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"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/formats/unix.hpp b/include/tempoch/formats/unix.hpp new file mode 100644 index 0000000..0c63591 --- /dev/null +++ b/include/tempoch/formats/unix.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace format { +struct Unix {}; +} // namespace format + +template <> struct is_format : std::true_type {}; + +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"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/legacy_time.hpp b/include/tempoch/legacy_time.hpp deleted file mode 100644 index 0771b22..0000000 --- a/include/tempoch/legacy_time.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#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 7311feb..72f3932 100644 --- a/include/tempoch/period.hpp +++ b/include/tempoch/period.hpp @@ -17,7 +17,9 @@ template struct TimeTraits> { return time.template to().value(); } - static Time from_mjd_value(double mjd) { return ModifiedJulianDate(mjd).to_time(); } + static Time from_mjd_value(double mjd) { + return Time::from_encoded(ModifiedJulianDate(mjd)); + } }; template struct TimeTraits> { @@ -25,7 +27,7 @@ template struct TimeTraits> { if constexpr (std::is_same_v) { return time.value(); } else { - return time.to_time().template to().value(); + return Time::from_encoded(time).template to().value(); } } @@ -44,7 +46,7 @@ template <> struct TimeTraits { } static CivilTime from_mjd_value(double mjd) { - return ModifiedJulianDate(mjd).to_time().to_civil(); + return Time::from_encoded(ModifiedJulianDate(mjd)).to_civil(); } }; @@ -54,6 +56,10 @@ template > class Period { explicit Period(const tempoch_period_mjd_t &inner) : m_inner(inner) {} public: + /// Construct a half-open interval [start, end). + /// + /// The period is half-open: @p start is included, @p end is excluded. + /// @throws InvalidPeriodError if start > 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), @@ -71,6 +77,12 @@ template > class Period { return qtty::Quantity(qty.value).template to(); } + /// Returns the length of the period (primary name; `duration()` is a backward-compat alias). + template + qtty::Quantity::type> length() const { + return duration(); + } + Period intersection(const Period &other) const { tempoch_period_mjd_t out{}; check_status(tempoch_period_mjd_intersection(m_inner, other.m_inner, &out), @@ -97,20 +109,17 @@ template > class Period { std::vector> complement_of(const std::vector> &others) const { std::vector raw; raw.reserve(others.size()); - for (const auto &period : others) - raw.push_back(period.c_inner()); - - tempoch_period_mjd_t *out_ptr = nullptr; - std::size_t out_count = 0; - check_status( - tempoch_period_list_complement(m_inner, raw.data(), raw.size(), &out_ptr, &out_count), - "Period::complement_of"); - + for (const auto &p : others) + raw.push_back(p.c_inner()); + tempoch_period_mjd_t *out = nullptr; + std::size_t n = 0; + check_status(tempoch_period_list_complement(m_inner, raw.data(), raw.size(), &out, &n), + "Period::complement_of"); std::vector> result; - result.reserve(out_count); - for (std::size_t i = 0; i < out_count; ++i) - result.push_back(from_c(out_ptr[i])); - tempoch_period_mjd_free(out_ptr, out_count); + result.reserve(n); + for (std::size_t i = 0; i < n; ++i) + result.push_back(from_c(out[i])); + tempoch_period_mjd_free(out, n); return result; } @@ -122,86 +131,68 @@ template Period(T, T) -> Period; using TTMjdPeriod = Period>; using UTCPeriod = Period; -template inline void validate_periods(const std::vector> &periods) { +namespace detail { + +template +inline std::vector to_raw(const std::vector> &periods) { std::vector raw; raw.reserve(periods.size()); - for (const auto &period : periods) - raw.push_back(period.c_inner()); + for (const auto &p : periods) + raw.push_back(p.c_inner()); + return raw; +} + +template +inline std::vector> from_alloc(tempoch_period_mjd_t *ptr, std::size_t count) { + std::vector> result; + result.reserve(count); + for (std::size_t i = 0; i < count; ++i) + result.push_back(Period::from_c(ptr[i])); + tempoch_period_mjd_free(ptr, count); + return result; +} + +} // namespace detail + +template inline void validate_periods(const std::vector> &periods) { + auto raw = detail::to_raw(periods); check_status(tempoch_period_list_validate(raw.data(), raw.size()), "validate_periods"); } 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 &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; - check_status(tempoch_period_list_intersect(ra.data(), ra.size(), rb.data(), rb.size(), &out_ptr, - &out_count), + auto ra = detail::to_raw(a), rb = detail::to_raw(b); + tempoch_period_mjd_t *out = nullptr; + std::size_t n = 0; + check_status(tempoch_period_list_intersect(ra.data(), ra.size(), rb.data(), rb.size(), &out, &n), "intersect_periods"); - - std::vector> result; - result.reserve(out_count); - for (std::size_t i = 0; i < out_count; ++i) - result.push_back(Period::from_c(out_ptr[i])); - tempoch_period_mjd_free(out_ptr, out_count); - return result; + return detail::from_alloc(out, n); } 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 &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; - check_status( - tempoch_period_list_union(ra.data(), ra.size(), rb.data(), rb.size(), &out_ptr, &out_count), - "union_periods"); - - std::vector> result; - result.reserve(out_count); - for (std::size_t i = 0; i < out_count; ++i) - result.push_back(Period::from_c(out_ptr[i])); - tempoch_period_mjd_free(out_ptr, out_count); - return result; + auto ra = detail::to_raw(a), rb = detail::to_raw(b); + tempoch_period_mjd_t *out = nullptr; + std::size_t n = 0; + check_status(tempoch_period_list_union(ra.data(), ra.size(), rb.data(), rb.size(), &out, &n), + "union_periods"); + return detail::from_alloc(out, n); } template inline std::vector> normalize_periods(const std::vector> &periods) { - std::vector raw; - raw.reserve(periods.size()); - for (const auto &period : periods) - raw.push_back(period.c_inner()); - - tempoch_period_mjd_t *out_ptr = nullptr; - std::size_t out_count = 0; - check_status(tempoch_period_list_normalize(raw.data(), raw.size(), &out_ptr, &out_count), + auto raw = detail::to_raw(periods); + tempoch_period_mjd_t *out = nullptr; + std::size_t n = 0; + check_status(tempoch_period_list_normalize(raw.data(), raw.size(), &out, &n), "normalize_periods"); - - std::vector> result; - result.reserve(out_count); - for (std::size_t i = 0; i < out_count; ++i) - result.push_back(Period::from_c(out_ptr[i])); - tempoch_period_mjd_free(out_ptr, out_count); - return result; + return detail::from_alloc(out, n); } template inline std::ostream &operator<<(std::ostream &os, const Period &period) { - return os << '[' << period.start() << ", " << period.end() << ']'; + return os << '[' << period.start() << ", " << period.end() << ')'; } } // namespace tempoch diff --git a/include/tempoch/scales.hpp b/include/tempoch/scales.hpp deleted file mode 100644 index 0db4260..0000000 --- a/include/tempoch/scales.hpp +++ /dev/null @@ -1,131 +0,0 @@ -#pragma once - -/** - * @file scales.hpp - * @brief Time-scale and time-format tag types for the tempoch C++ API. - * - * The C++ surface follows the Rust `tempoch` model: physical *scale* and - * external *format* are orthogonal type parameters. - */ - -#include "ffi_core.hpp" -#include -#include - -namespace tempoch { - -namespace scale { - -struct TT {}; -struct TAI {}; -struct UTC {}; -struct UT1 {}; -struct TDB {}; -struct TCG {}; -struct TCB {}; - -} // namespace scale - -namespace format { - -struct JD {}; -struct MJD {}; -struct J2000s {}; -struct Unix {}; -struct GPS {}; - -} // namespace format - -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 {}; - -template inline constexpr bool is_scale_v = is_scale::value; - -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 {}; - -template inline constexpr bool is_format_v = is_format::value; - -template struct ScaleTraits; - -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 ScaleTraits { - static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TAI; - static constexpr const char *name() { return "TAI"; } -}; - -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 ScaleTraits { - static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_UT1; - static constexpr const char *name() { return "UT1"; } -}; - -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 ScaleTraits { - static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TCG; - static constexpr const char *name() { 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 FormatTraits; - -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 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 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 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"; } -}; - -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"; } -}; - -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/scales/base.hpp b/include/tempoch/scales/base.hpp new file mode 100644 index 0000000..e61b89c --- /dev/null +++ b/include/tempoch/scales/base.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "../ffi_core.hpp" +#include + +namespace tempoch { + +template struct is_scale : std::false_type {}; +template inline constexpr bool is_scale_v = is_scale::value; + +template struct ScaleTraits; +template inline constexpr tempoch_scale_tag_t scale_tag_v = ScaleTraits::ffi_tag; + +} // namespace tempoch diff --git a/include/tempoch/scales/scales.hpp b/include/tempoch/scales/scales.hpp new file mode 100644 index 0000000..3ef410e --- /dev/null +++ b/include/tempoch/scales/scales.hpp @@ -0,0 +1,14 @@ +#pragma once + +/** + * @file scales/scales.hpp + * @brief Time-scale tag types for the tempoch C++ API. + */ + +#include "tai.hpp" +#include "tcb.hpp" +#include "tcg.hpp" +#include "tdb.hpp" +#include "tt.hpp" +#include "ut1.hpp" +#include "utc.hpp" diff --git a/include/tempoch/scales/tai.hpp b/include/tempoch/scales/tai.hpp new file mode 100644 index 0000000..08bbf6a --- /dev/null +++ b/include/tempoch/scales/tai.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct TAI {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TAI; + static constexpr const char *name() { return "TAI"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/tcb.hpp b/include/tempoch/scales/tcb.hpp new file mode 100644 index 0000000..c4e6f75 --- /dev/null +++ b/include/tempoch/scales/tcb.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct TCB {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TCB; + static constexpr const char *name() { return "TCB"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/tcg.hpp b/include/tempoch/scales/tcg.hpp new file mode 100644 index 0000000..d3ffc00 --- /dev/null +++ b/include/tempoch/scales/tcg.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct TCG {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TCG; + static constexpr const char *name() { return "TCG"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/tdb.hpp b/include/tempoch/scales/tdb.hpp new file mode 100644 index 0000000..95b261f --- /dev/null +++ b/include/tempoch/scales/tdb.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct TDB {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TDB; + static constexpr const char *name() { return "TDB"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/tt.hpp b/include/tempoch/scales/tt.hpp new file mode 100644 index 0000000..ba7cf33 --- /dev/null +++ b/include/tempoch/scales/tt.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct TT {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_TT; + static constexpr const char *name() { return "TT"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/ut1.hpp b/include/tempoch/scales/ut1.hpp new file mode 100644 index 0000000..837ada0 --- /dev/null +++ b/include/tempoch/scales/ut1.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct UT1 {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_UT1; + static constexpr const char *name() { return "UT1"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/scales/utc.hpp b/include/tempoch/scales/utc.hpp new file mode 100644 index 0000000..eb6fcde --- /dev/null +++ b/include/tempoch/scales/utc.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "base.hpp" + +namespace tempoch { + +namespace scale { +struct UTC {}; +} // namespace scale + +template <> struct is_scale : std::true_type {}; + +template <> struct ScaleTraits { + static constexpr tempoch_scale_tag_t ffi_tag = TEMPOCH_SCALE_TAG_T_UTC; + static constexpr const char *name() { return "UTC"; } +}; + +} // namespace tempoch diff --git a/include/tempoch/tempoch.hpp b/include/tempoch/tempoch.hpp index 877c504..ea03c4a 100644 --- a/include/tempoch/tempoch.hpp +++ b/include/tempoch/tempoch.hpp @@ -35,7 +35,8 @@ #include "constants.hpp" #include "eop.hpp" #include "ffi_core.hpp" +#include "formats/formats.hpp" #include "period.hpp" -#include "scales.hpp" +#include "scales/scales.hpp" #include "time.hpp" #include "time_base.hpp" diff --git a/include/tempoch/time_base.hpp b/include/tempoch/time_base.hpp index 6a7d6ab..1d7bb36 100644 --- a/include/tempoch/time_base.hpp +++ b/include/tempoch/time_base.hpp @@ -7,7 +7,8 @@ #include "civil_time.hpp" #include "ffi_core.hpp" -#include "scales.hpp" +#include "formats/formats.hpp" +#include "scales/scales.hpp" #include #include #include @@ -161,6 +162,17 @@ template class Time { static Time from_raw_j2000_seconds(qtty::Second seconds) { return from_split_seconds(seconds); } + /// Decode a scalar encoding @p Fmt into canonical split storage on scale @p S (default context). + template static Time from_encoded(const EncodedTime &encoded) { + return Time(detail::decode_time(encoded.value(), nullptr)); + } + + /// Decode using explicit UTC / UT1 policy from @p ctx when required by format @p Fmt. + template + static Time from_encoded_with(const EncodedTime &encoded, const TimeContext &ctx) { + return Time(detail::decode_time(encoded.value(), ctx.get())); + } + std::pair split_seconds() const noexcept { return {qtty::Second(raw_.hi_seconds), qtty::Second(raw_.lo_seconds)}; } @@ -248,6 +260,16 @@ template class Time { template Time operator-(const Q &delta) const { return *this + Q(-delta.value()); } + template Time &operator+=(const Q &delta) { + raw_ = detail::add_seconds(raw_, delta); + return *this; + } + + template Time &operator-=(const Q &delta) { + raw_ = detail::add_seconds(raw_, Q(-delta.value())); + return *this; + } + qtty::Second operator-(const Time &other) const { return detail::difference_seconds(raw_, other.raw_); } @@ -302,7 +324,10 @@ template class EncodedTime { return EncodedTime(raw); } - static EncodedTime from_raw_unchecked(quantity_type raw) { return EncodedTime(raw); } + /// Construct directly from a quantity, validating that the value is finite. + /// + /// Unlike `try_new`, this throws `TempochException` on non-finite input. + static EncodedTime from_raw(quantity_type raw) { return EncodedTime(raw); } quantity_type raw() const noexcept { return raw_; } quantity_type quantity() const noexcept { return raw_; } @@ -313,30 +338,24 @@ template class EncodedTime { return EncodedTime(tempoch_const_j2000_jd_tt()); } - Time to_time() const { return Time(detail::decode_time(raw_.value(), nullptr)); } - - Time to_time_with(const TimeContext &ctx) const { - return Time(detail::decode_time(raw_.value(), ctx.get())); - } - template , int> = 0> - auto to() const -> decltype(this->to_time().template to()) { - return to_time().template to(); + auto to() const { + return Time::from_encoded(*this).template to(); } template , int> = 0> Time to_with(const TimeContext &ctx) const { - return to_time_with(ctx).template to_with(ctx); + return Time::from_encoded_with(*this, ctx).template to_with(ctx); } template , int> = 0> EncodedTime to() const { - return to_time().template to(); + return Time::from_encoded(*this).template to(); } template , int> = 0> EncodedTime to_with(const TimeContext &ctx) const { - return to_time_with(ctx).template to_with(ctx); + return Time::from_encoded_with(*this, ctx).template to_with(ctx); } template , int> = 0> @@ -358,15 +377,26 @@ template class EncodedTime { } template EncodedTime operator+(const Q &delta) const { - return (to_time() + delta).template to(); + return (Time::from_encoded(*this) + delta).template to(); } template EncodedTime operator-(const Q &delta) const { - return (to_time() - delta).template to(); + return (Time::from_encoded(*this) - delta).template to(); + } + + template EncodedTime &operator+=(const Q &delta) { + *this = *this + delta; + return *this; + } + + template EncodedTime &operator-=(const Q &delta) { + *this = *this - delta; + return *this; } quantity_type operator-(const EncodedTime &other) const { - return (to_time() - other.to_time()).template to(); + return (Time::from_encoded(*this) - Time::from_encoded(other)) + .template to(); } bool operator==(const EncodedTime &other) const noexcept { return raw_ == other.raw_; } @@ -375,6 +405,77 @@ template class EncodedTime { bool operator<=(const EncodedTime &other) const noexcept { return raw_ <= other.raw_; } bool operator>(const EncodedTime &other) const noexcept { return raw_ > other.raw_; } bool operator>=(const EncodedTime &other) const noexcept { return raw_ >= other.raw_; } + + EncodedTime min(const EncodedTime &other) const noexcept { + return *this <= other ? *this : other; + } + EncodedTime max(const EncodedTime &other) const noexcept { + return *this >= other ? *this : other; + } + + EncodedTime mean(const EncodedTime &other) const { + return EncodedTime(quantity_type((raw_.value() + other.raw_.value()) * 0.5)); + } + + template , int> = 0> + double jd_value() const noexcept { + return raw_.value(); + } + + template , int> = 0> + double mjd_value() const noexcept { + return raw_.value(); + } + + template , int> = 0> + qtty::JulianCentury julian_centuries_qty() const noexcept { + return qtty::JulianCentury((raw_.value() - 2451545.0) / 36525.0); + } + + template , int> = 0> + double julian_centuries() const noexcept { + return julian_centuries_qty().value(); + } + + template , int> = 0> + EncodedTime to_mjd() const { + return this->template to(); + } + + template , int> = 0> + EncodedTime to_jd() const { + return this->template to(); + } + + template , int> = 0> + static EncodedTime from_mjd(const EncodedTime &mjd) { + return mjd.template to(); + } + + template , int> = 0> + static EncodedTime from_jd(const EncodedTime &jd) { + return jd.template to(); + } + + template , int> = 0> + static EncodedTime from_utc(const CivilTime &civil) { + return Time::from_civil(civil).template to().template to(); + } + + template , int> = 0> + static EncodedTime from_utc(const CivilTime &civil, const TimeContext &ctx) { + return Time::from_civil(civil, ctx).template to().template to(); + } + + template , int> = 0> + CivilTime to_utc() const { + return Time::from_encoded(*this).template to().to_civil(); + } + + template , int> = 0> + CivilTime to_utc(const TimeContext &ctx) const { + return Time::from_encoded_with(*this, ctx).template to().to_civil(ctx); + } }; template diff --git a/tempoch b/tempoch index 39abd9b..bf22fdc 160000 --- a/tempoch +++ b/tempoch @@ -1 +1 @@ -Subproject commit 39abd9bd7a9163227bbb06517d03b9d74d43c1ab +Subproject commit bf22fdc36313d4e64aecd391f4a580edafa16409 diff --git a/tests/test_headers.cpp b/tests/test_headers.cpp new file mode 100644 index 0000000..f859224 --- /dev/null +++ b/tests/test_headers.cpp @@ -0,0 +1,38 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +TEST(TempochHeaderLayout, ExposesPerScaleAndFormatTraits) { + static_assert(tempoch::is_scale_v); + static_assert(tempoch::is_scale_v); + static_assert(tempoch::is_scale_v); + static_assert(tempoch::is_scale_v); + static_assert(tempoch::is_scale_v); + static_assert(tempoch::is_scale_v); + static_assert(tempoch::is_scale_v); + + static_assert(tempoch::is_format_v); + static_assert(tempoch::is_format_v); + static_assert(tempoch::is_format_v); + static_assert(tempoch::is_format_v); + static_assert(tempoch::is_format_v); + + static_assert(std::is_same_v::quantity_type, + qtty::Day>); + static_assert(std::is_same_v::quantity_type, + qtty::Second>); +} diff --git a/tests/test_time.cpp b/tests/test_time.cpp index fbb9d85..8904544 100644 --- a/tests/test_time.cpp +++ b/tests/test_time.cpp @@ -12,9 +12,9 @@ TEST(Time, SplitRoundtripThroughFormats) { auto mjd = tt.to(); auto j2000 = tt.to(); - auto jd_back = jd.to_time(); - auto mjd_back = mjd.to_time(); - auto j2000_back = j2000.to_time(); + auto jd_back = Time::from_encoded(jd); + auto mjd_back = Time::from_encoded(mjd); + auto j2000_back = Time::from_encoded(j2000); EXPECT_NEAR((jd_back - tt).value(), 0.0, 2e-5); EXPECT_NEAR((mjd_back - tt).value(), 0.0, 5e-7); @@ -26,14 +26,14 @@ TEST(Time, UnixAndGpsRemainSecondFormats) { auto unix = utc.to(); EXPECT_NEAR(unix.value(), 9.43e-06, 1e-9); - auto unix_back = unix.to_time().to_civil(); + auto unix_back = Time::from_encoded(unix).to_civil(); EXPECT_EQ(unix_back.year, 1970); EXPECT_EQ(unix_back.month, 1); EXPECT_EQ(unix_back.day, 1); auto tai = utc.to(); auto gps = tai.to(); - auto gps_back = gps.to_time(); + auto gps_back = Time::from_encoded(gps); EXPECT_NEAR((gps_back - tai).value(), 0.0, 1e-9); } @@ -69,7 +69,7 @@ TEST(Time, PreDefinitionUtcRequiresOptIn) { } TEST(Time, Ut1HorizonErrorsSurfaceThroughContextBackedConversion) { - auto far_future = JulianDate(2'500'000.0).to_time(); + auto far_future = Time::from_encoded(JulianDate(2'500'000.0)); auto ctx = TimeContext::with_builtin_eop(); EXPECT_THROW((far_future.to_with(ctx)), Ut1HorizonExceededError); } @@ -82,3 +82,55 @@ TEST(Time, ArithmeticIsAffineAndSecondBased) { EXPECT_NEAR(diff.to().value(), 3600.0, 1e-9); EXPECT_NEAR(tt.split_seconds().first.value(), 10.0, 1e-12); } + +TEST(Time, TtJulianDateConvenienceUtcRoundtrip) { + auto jd = JulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + auto utc = jd.to_utc(); + + EXPECT_EQ(utc.year, 2026); + EXPECT_EQ(utc.month, 7); + EXPECT_EQ(utc.day, 15); + EXPECT_NEAR(utc.hour, 22, 1); +} + +TEST(Time, TtMjdConvenienceUtcRoundtrip) { + auto mjd = ModifiedJulianDate::from_utc({2026, 7, 15, 22, 0, 0}); + auto utc = mjd.to_utc(); + + EXPECT_EQ(utc.year, 2026); + EXPECT_EQ(utc.month, 7); + EXPECT_EQ(utc.day, 15); + EXPECT_NEAR(utc.hour, 22, 1); +} + +TEST(Time, EncodedDateArithmeticSupportsAssignOperators) { + auto jd = JulianDate::J2000(); + jd += qtty::Hour(24.0); + jd -= qtty::Hour(12.0); + + EXPECT_NEAR(jd.jd_value(), 2451545.5, 1e-10); + + auto mjd = jd.to_mjd(); + mjd += qtty::Minute(720.0); + EXPECT_NEAR(mjd.to_jd().jd_value(), 2451546.0, 1e-10); +} + +TEST(Time, EncodedDateHelpersExposeExpectedValues) { + auto jd = JulianDate::J2000(); + auto next = jd + qtty::Day(36525.0); + auto mjd = ModifiedJulianDate::from_jd(jd); + + EXPECT_NEAR(jd.julian_centuries(), 0.0, 1e-12); + EXPECT_NEAR(next.julian_centuries(), 1.0, 1e-12); + EXPECT_NEAR(mjd.mjd_value(), jd.jd_value() - 2400000.5, 1e-12); + EXPECT_NEAR(JulianDate::from_mjd(mjd).jd_value(), jd.jd_value(), 1e-12); +} + +TEST(Time, EncodedDateMinMaxAndMeanRemainAvailable) { + auto a = JulianDate::J2000(); + auto b = a + qtty::Day(2.0); + + EXPECT_EQ(a.min(b), a); + EXPECT_EQ(a.max(b), b); + EXPECT_NEAR(a.mean(b).jd_value(), a.jd_value() + 1.0, 1e-12); +}