From 8815945d241745309375d2115e0b1004393c0db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vall=C3=A9s=20Puig=2C=20Ramon?= <74069811+VPRamon@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:55:57 +0200 Subject: [PATCH] feat: add period transformation methods for scale and format conversions, update changelog and examples --- CHANGELOG.md | 14 ++++++ CMakeLists.txt | 3 +- README.md | 22 ++++++++++ examples/10_transformations.cpp | 74 +++++++++++++++++++++++++++++++ include/tempoch/period.hpp | 67 ++++++++++++++++++++++++++++ tests/test_period.cpp | 77 +++++++++++++++++++++++++++++++++ 6 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 examples/10_transformations.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f83944..0c40d4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ 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.4] - 2026-06-13 + +### Added + +- Added `Period::to()`, `Period::to()`, and + `Period::to()` so typed periods can be + converted directly by applying the same conversion to both endpoints. +- Added `Period::to_with<...>(ctx)` overloads for context-backed conversion + routes such as UT1. +- Added `Period` conversion support by treating civil endpoints as + UTC before converting to typed scales and formats. +- Added `examples/10_transformations.cpp`, showing scale-only, format-only, + combined scale/format, context-backed UT1, and period transformations. + ## [0.5.3] - 2026-06-06 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index 09bbddb..4a3e929 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15) -project(tempoch_cpp VERSION 0.5.3 LANGUAGES CXX) +project(tempoch_cpp VERSION 0.5.4 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -164,6 +164,7 @@ foreach(_ex 07_conversions # Full chain: Unix → UTC → TAI → TT → TDB / UT1 / GPS 08_gnss_scales # GNSS and SPICE-compatibility scales (GPST, GST, QZSST, BDT, ET) 09_gnss_week # GNSS week-number decomposition (GnssWeek) + 10_transformations # Transform times and periods by format, scale, or both ) add_executable(${_ex} examples/${_ex}.cpp) target_link_libraries(${_ex} PRIVATE tempoch_cpp) diff --git a/README.md b/README.md index 0b07213..e61f704 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,27 @@ int main() { } ``` +Typed periods can be converted directly between scales and formats by applying +the same conversion to both endpoints: + +```cpp +using namespace tempoch; + +Period> window( + EncodedTime(2460000.0), + EncodedTime(2460001.0) +); + +auto tt_mjd_window = window.to(); + +UTCPeriod civil_window( + CivilTime(2026, 1, 1), + CivilTime(2026, 1, 2) +); + +auto civil_tt_mjd_window = civil_window.to(); +``` + The public headers are now split by concept: - `tempoch/scales/*.hpp` for physical/civil time-axis tags and scale traits @@ -103,6 +124,7 @@ std::cout << dt.to() << "\n"; - `docs/mainpage.md` (API overview) - `examples/01_quickstart.cpp` (civil UTC -> canonical time -> explicit encodings) - `examples/02_scales.cpp` (all supported scale conversions) +- `examples/10_transformations.cpp` (scale-only, format-only, and combined conversions) - `include/tempoch/tempoch.hpp` (umbrella public header) ## Integration diff --git a/examples/10_transformations.cpp b/examples/10_transformations.cpp new file mode 100644 index 0000000..5723a04 --- /dev/null +++ b/examples/10_transformations.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +/** + * @file 10_transformations.cpp + * @example 10_transformations.cpp + * @brief Transform times and periods by format, by scale, or by both at once. + * + * Build and run: + * cmake -B build && cmake --build build + * ./build/10_transformations + */ + +#include +#include +#include + +int main() { + using namespace tempoch; + + auto ctx = TimeContext::with_builtin_eop(); + + CivilTime civil{2026, 1, 1, 0, 0, 0}; + auto utc = Time::from_civil(civil); + + // 1. Transform only the scale: Time -> Time. + auto tt = utc.to(); + + // 2. Transform only the format: Time -> EncodedTime. + auto utc_jd = utc.to(); + auto utc_mjd = utc_jd.to(); + + // 3. Transform scale and format in one call: + // Time -> EncodedTime. + auto tt_mjd = utc.to(); + + // 4. Encoded values can be decoded and transformed as well. + JulianDate jd_utc{2'460'676.5}; + auto from_jd_to_tt = jd_utc.to(); + auto from_jd_to_tt_mjd = jd_utc.to(); + + // 5. UT1 uses an explicit context. + auto ut1 = utc.to_with(ctx); + auto ut1_mjd = utc.to_with(ctx); + + // 6. Periods use the same conversion shape on both endpoints. + Period utc_window(utc, utc + qtty::Hour(12.0)); + auto tt_window = utc_window.to(); + auto utc_mjd_window = utc_window.to(); + auto tt_mjd_window = utc_window.to(); + + UTCPeriod civil_window(CivilTime(2026, 1, 1), CivilTime(2026, 1, 2)); + auto civil_as_tt_mjd = civil_window.to(); + + std::cout << std::fixed << std::setprecision(9); + std::cout << "Civil UTC input : " << civil << "\n"; + std::cout << "Time as JD : " << utc_jd << "\n"; + std::cout << "Time as MJD : " << utc_mjd << "\n"; + std::cout << "Time -> Time : " << tt.to() << "\n"; + std::cout << "Time -> TT MJD : " << tt_mjd << "\n"; + std::cout << "JD(UTC) -> Time : " << from_jd_to_tt.to() << "\n"; + std::cout << "JD(UTC) -> TT MJD : " << from_jd_to_tt_mjd << "\n"; + std::cout << "Time -> UT1 JD : " << ut1.to() << "\n"; + std::cout << "Time -> UT1 MJD : " << ut1_mjd << "\n"; + + std::cout << "\nPeriod conversions\n"; + std::cout << "UTC window start JD : " << utc_window.start().to() << "\n"; + std::cout << "TT window start JD : " << tt_window.start().to() << "\n"; + std::cout << "UTC MJD window start : " << utc_mjd_window.start() << "\n"; + std::cout << "TT MJD window start : " << tt_mjd_window.start() << "\n"; + std::cout << "Civil -> TT MJD start : " << civil_as_tt_mjd.start() << "\n"; + + return 0; +} diff --git a/include/tempoch/period.hpp b/include/tempoch/period.hpp index 72f3932..1693839 100644 --- a/include/tempoch/period.hpp +++ b/include/tempoch/period.hpp @@ -50,6 +50,45 @@ template <> struct TimeTraits { } }; +namespace detail { + +template auto convert_period_endpoint(const T &value) { + if constexpr (std::is_same_v, CivilTime>) { + return Time::from_civil(value).template to(); + } else { + return value.template to(); + } +} + +template +auto convert_period_endpoint(const T &value) { + if constexpr (std::is_same_v, CivilTime>) { + return Time::from_civil(value).template to(); + } else { + return value.template to(); + } +} + +template +auto convert_period_endpoint_with(const T &value, const TimeContext &ctx) { + if constexpr (std::is_same_v, CivilTime>) { + return Time::from_civil(value, ctx).template to_with(ctx); + } else { + return value.template to_with(ctx); + } +} + +template +auto convert_period_endpoint_with(const T &value, const TimeContext &ctx) { + if constexpr (std::is_same_v, CivilTime>) { + return Time::from_civil(value, ctx).template to_with(ctx); + } else { + return value.template to_with(ctx); + } +} + +} // namespace detail + template > class Period { tempoch_period_mjd_t m_inner; @@ -71,6 +110,34 @@ template > class Period { T start() const { return TimeTraits::from_mjd_value(m_inner.start_mjd); } T end() const { return TimeTraits::from_mjd_value(m_inner.end_mjd); } + template auto to() const { + auto converted_start = detail::convert_period_endpoint(start()); + auto converted_end = detail::convert_period_endpoint(end()); + using OutT = std::decay_t; + return Period(converted_start, converted_end); + } + + template auto to() const { + auto converted_start = detail::convert_period_endpoint(start()); + auto converted_end = detail::convert_period_endpoint(end()); + using OutT = std::decay_t; + return Period(converted_start, converted_end); + } + + template auto to_with(const TimeContext &ctx) const { + auto converted_start = detail::convert_period_endpoint_with(start(), ctx); + auto converted_end = detail::convert_period_endpoint_with(end(), ctx); + using OutT = std::decay_t; + return Period(converted_start, converted_end); + } + + template auto to_with(const TimeContext &ctx) const { + auto converted_start = detail::convert_period_endpoint_with(start(), ctx); + auto converted_end = detail::convert_period_endpoint_with(end(), ctx); + using OutT = std::decay_t; + return Period(converted_start, converted_end); + } + template qtty::Quantity::type> duration() const { auto qty = tempoch_period_mjd_duration_qty(m_inner); diff --git a/tests/test_period.cpp b/tests/test_period.cpp index 44a6ee4..9ec59b1 100644 --- a/tests/test_period.cpp +++ b/tests/test_period.cpp @@ -1,5 +1,6 @@ #include #include +#include using namespace tempoch; @@ -32,3 +33,79 @@ TEST(Period, CivilPeriodsUseUtcMjdRoundtrip) { EXPECT_EQ(start.day, 1); EXPECT_EQ(end.day, 2); } + +TEST(Period, TimeScaleConversionConvertsEndpoints) { + auto start = Time::from_civil({2026, 1, 1, 0, 0, 0}); + auto end = Time::from_civil({2026, 1, 2, 0, 0, 0}); + Period p(start, end); + + auto out = p.to(); + + static_assert(std::is_same_v>>); + EXPECT_NEAR((out.start() - p.start().to()).value(), 0.0, 1e-5); + EXPECT_NEAR((out.end() - p.end().to()).value(), 0.0, 1e-5); +} + +TEST(Period, TimeScaleAndFormatConversionConvertsEndpoints) { + auto start = Time::from_civil({2026, 1, 1, 0, 0, 0}); + auto end = Time::from_civil({2026, 1, 2, 0, 0, 0}); + Period p(start, end); + + auto out = p.to(); + + static_assert(std::is_same_v>>); + EXPECT_DOUBLE_EQ(out.start().value(), (p.start().to().value())); + EXPECT_DOUBLE_EQ(out.end().value(), (p.end().to().value())); +} + +TEST(Period, EncodedScaleAndFormatConversionConvertsEndpoints) { + using UTCJD = EncodedTime; + Period p(UTCJD(2460000.0), UTCJD(2460001.0)); + + auto out = p.to(); + + static_assert(std::is_same_v>>); + EXPECT_DOUBLE_EQ(out.start().value(), (p.start().to().value())); + EXPECT_DOUBLE_EQ(out.end().value(), (p.end().to().value())); +} + +TEST(Period, EncodedFormatConversionConvertsEndpoints) { + using UTCJD = EncodedTime; + Period p(UTCJD(2460000.0), UTCJD(2460001.0)); + + auto out = p.to(); + + static_assert(std::is_same_v>>); + EXPECT_DOUBLE_EQ(out.start().value(), p.start().to().value()); + EXPECT_DOUBLE_EQ(out.end().value(), p.end().to().value()); +} + +TEST(Period, CivilPeriodConvertsFromUtcEndpoints) { + UTCPeriod p(CivilTime(2026, 1, 1, 0, 0, 0), CivilTime(2026, 1, 2, 0, 0, 0)); + + auto out = p.to(); + + static_assert(std::is_same_v>>); + EXPECT_DOUBLE_EQ(out.start().value(), + (Time::from_civil(p.start()).to().value())); + EXPECT_DOUBLE_EQ(out.end().value(), + (Time::from_civil(p.end()).to().value())); +} + +TEST(Period, ToWithUsesProvidedContextForBothEndpoints) { + auto ctx = TimeContext::with_builtin_eop(); + auto start = Time::from_civil({2026, 1, 1, 0, 0, 0}); + auto end = Time::from_civil({2026, 1, 2, 0, 0, 0}); + Period p(start, end); + + auto ut1 = p.to_with(ctx); + auto ut1_mjd = p.to_with(ctx); + + static_assert(std::is_same_v>>); + static_assert(std::is_same_v>>); + EXPECT_NEAR((ut1.start() - p.start().to_with(ctx)).value(), 0.0, 1e-5); + EXPECT_NEAR((ut1.end() - p.end().to_with(ctx)).value(), 0.0, 1e-5); + EXPECT_DOUBLE_EQ(ut1_mjd.start().value(), + (p.start().to_with(ctx).value())); + EXPECT_DOUBLE_EQ(ut1_mjd.end().value(), (p.end().to_with(ctx).value())); +}