Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>::to<TargetScale>()`, `Period<T>::to<TargetFormat>()`, and
`Period<T>::to<TargetScale, TargetFormat>()` so typed periods can be
converted directly by applying the same conversion to both endpoints.
- Added `Period<T>::to_with<...>(ctx)` overloads for context-backed conversion
routes such as UT1.
- Added `Period<CivilTime>` 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
Expand Down
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<EncodedTime<scale::UTC, format::JD>> window(
EncodedTime<scale::UTC, format::JD>(2460000.0),
EncodedTime<scale::UTC, format::JD>(2460001.0)
);

auto tt_mjd_window = window.to<scale::TT, format::MJD>();

UTCPeriod civil_window(
CivilTime(2026, 1, 1),
CivilTime(2026, 1, 2)
);

auto civil_tt_mjd_window = civil_window.to<scale::TT, format::MJD>();
```

The public headers are now split by concept:

- `tempoch/scales/*.hpp` for physical/civil time-axis tags and scale traits
Expand Down Expand Up @@ -103,6 +124,7 @@ std::cout << dt.to<qtty::Hour>() << "\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
Expand Down
74 changes: 74 additions & 0 deletions examples/10_transformations.cpp
Original file line number Diff line number Diff line change
@@ -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 <iomanip>
#include <iostream>
#include <tempoch/tempoch.hpp>

int main() {
using namespace tempoch;

auto ctx = TimeContext::with_builtin_eop();

CivilTime civil{2026, 1, 1, 0, 0, 0};
auto utc = Time<scale::UTC>::from_civil(civil);

// 1. Transform only the scale: Time<UTC> -> Time<TT>.
auto tt = utc.to<scale::TT>();

// 2. Transform only the format: Time<UTC> -> EncodedTime<UTC, JD>.
auto utc_jd = utc.to<format::JD>();
auto utc_mjd = utc_jd.to<format::MJD>();

// 3. Transform scale and format in one call:
// Time<UTC> -> EncodedTime<TT, MJD>.
auto tt_mjd = utc.to<scale::TT, format::MJD>();

// 4. Encoded values can be decoded and transformed as well.
JulianDate<scale::UTC> jd_utc{2'460'676.5};
auto from_jd_to_tt = jd_utc.to<scale::TT>();
auto from_jd_to_tt_mjd = jd_utc.to<scale::TT, format::MJD>();

// 5. UT1 uses an explicit context.
auto ut1 = utc.to_with<scale::UT1>(ctx);
auto ut1_mjd = utc.to_with<scale::UT1, format::MJD>(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<scale::TT>();
auto utc_mjd_window = utc_window.to<format::MJD>();
auto tt_mjd_window = utc_window.to<scale::TT, format::MJD>();

UTCPeriod civil_window(CivilTime(2026, 1, 1), CivilTime(2026, 1, 2));
auto civil_as_tt_mjd = civil_window.to<scale::TT, format::MJD>();

std::cout << std::fixed << std::setprecision(9);
std::cout << "Civil UTC input : " << civil << "\n";
std::cout << "Time<UTC> as JD : " << utc_jd << "\n";
std::cout << "Time<UTC> as MJD : " << utc_mjd << "\n";
std::cout << "Time<UTC> -> Time<TT> : " << tt.to<format::JD>() << "\n";
std::cout << "Time<UTC> -> TT MJD : " << tt_mjd << "\n";
std::cout << "JD(UTC) -> Time<TT> : " << from_jd_to_tt.to<format::JD>() << "\n";
std::cout << "JD(UTC) -> TT MJD : " << from_jd_to_tt_mjd << "\n";
std::cout << "Time<UTC> -> UT1 JD : " << ut1.to<format::JD>() << "\n";
std::cout << "Time<UTC> -> UT1 MJD : " << ut1_mjd << "\n";

std::cout << "\nPeriod conversions\n";
std::cout << "UTC window start JD : " << utc_window.start().to<format::JD>() << "\n";
std::cout << "TT window start JD : " << tt_window.start().to<format::JD>() << "\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;
}
67 changes: 67 additions & 0 deletions include/tempoch/period.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,45 @@ template <> struct TimeTraits<CivilTime> {
}
};

namespace detail {

template <typename Target, typename T> auto convert_period_endpoint(const T &value) {
if constexpr (std::is_same_v<std::decay_t<T>, CivilTime>) {
return Time<scale::UTC>::from_civil(value).template to<Target>();
} else {
return value.template to<Target>();
}
}

template <typename TargetA, typename TargetB, typename T>
auto convert_period_endpoint(const T &value) {
if constexpr (std::is_same_v<std::decay_t<T>, CivilTime>) {
return Time<scale::UTC>::from_civil(value).template to<TargetA, TargetB>();
} else {
return value.template to<TargetA, TargetB>();
}
}

template <typename Target, typename T>
auto convert_period_endpoint_with(const T &value, const TimeContext &ctx) {
if constexpr (std::is_same_v<std::decay_t<T>, CivilTime>) {
return Time<scale::UTC>::from_civil(value, ctx).template to_with<Target>(ctx);
} else {
return value.template to_with<Target>(ctx);
}
}

template <typename TargetA, typename TargetB, typename T>
auto convert_period_endpoint_with(const T &value, const TimeContext &ctx) {
if constexpr (std::is_same_v<std::decay_t<T>, CivilTime>) {
return Time<scale::UTC>::from_civil(value, ctx).template to_with<TargetA, TargetB>(ctx);
} else {
return value.template to_with<TargetA, TargetB>(ctx);
}
}

} // namespace detail

template <typename T = ModifiedJulianDate<scale::TT>> class Period {
tempoch_period_mjd_t m_inner;

Expand All @@ -71,6 +110,34 @@ template <typename T = ModifiedJulianDate<scale::TT>> class Period {
T start() const { return TimeTraits<T>::from_mjd_value(m_inner.start_mjd); }
T end() const { return TimeTraits<T>::from_mjd_value(m_inner.end_mjd); }

template <typename Target> auto to() const {
auto converted_start = detail::convert_period_endpoint<Target>(start());
auto converted_end = detail::convert_period_endpoint<Target>(end());
using OutT = std::decay_t<decltype(converted_start)>;
return Period<OutT>(converted_start, converted_end);
}

template <typename TargetA, typename TargetB> auto to() const {
auto converted_start = detail::convert_period_endpoint<TargetA, TargetB>(start());
auto converted_end = detail::convert_period_endpoint<TargetA, TargetB>(end());
using OutT = std::decay_t<decltype(converted_start)>;
return Period<OutT>(converted_start, converted_end);
}

template <typename Target> auto to_with(const TimeContext &ctx) const {
auto converted_start = detail::convert_period_endpoint_with<Target>(start(), ctx);
auto converted_end = detail::convert_period_endpoint_with<Target>(end(), ctx);
using OutT = std::decay_t<decltype(converted_start)>;
return Period<OutT>(converted_start, converted_end);
}

template <typename TargetA, typename TargetB> auto to_with(const TimeContext &ctx) const {
auto converted_start = detail::convert_period_endpoint_with<TargetA, TargetB>(start(), ctx);
auto converted_end = detail::convert_period_endpoint_with<TargetA, TargetB>(end(), ctx);
using OutT = std::decay_t<decltype(converted_start)>;
return Period<OutT>(converted_start, converted_end);
}

template <typename TargetType = qtty::DayTag>
qtty::Quantity<typename qtty::ExtractTag<TargetType>::type> duration() const {
auto qty = tempoch_period_mjd_duration_qty(m_inner);
Expand Down
77 changes: 77 additions & 0 deletions tests/test_period.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <gtest/gtest.h>
#include <tempoch/tempoch.hpp>
#include <type_traits>

using namespace tempoch;

Expand Down Expand Up @@ -32,3 +33,79 @@ TEST(Period, CivilPeriodsUseUtcMjdRoundtrip) {
EXPECT_EQ(start.day, 1);
EXPECT_EQ(end.day, 2);
}

TEST(Period, TimeScaleConversionConvertsEndpoints) {
auto start = Time<scale::UTC>::from_civil({2026, 1, 1, 0, 0, 0});
auto end = Time<scale::UTC>::from_civil({2026, 1, 2, 0, 0, 0});
Period p(start, end);

auto out = p.to<scale::TT>();

static_assert(std::is_same_v<decltype(out), Period<Time<scale::TT>>>);
EXPECT_NEAR((out.start() - p.start().to<scale::TT>()).value(), 0.0, 1e-5);
EXPECT_NEAR((out.end() - p.end().to<scale::TT>()).value(), 0.0, 1e-5);
}

TEST(Period, TimeScaleAndFormatConversionConvertsEndpoints) {
auto start = Time<scale::UTC>::from_civil({2026, 1, 1, 0, 0, 0});
auto end = Time<scale::UTC>::from_civil({2026, 1, 2, 0, 0, 0});
Period p(start, end);

auto out = p.to<scale::TT, format::MJD>();

static_assert(std::is_same_v<decltype(out), Period<EncodedTime<scale::TT, format::MJD>>>);
EXPECT_DOUBLE_EQ(out.start().value(), (p.start().to<scale::TT, format::MJD>().value()));
EXPECT_DOUBLE_EQ(out.end().value(), (p.end().to<scale::TT, format::MJD>().value()));
}

TEST(Period, EncodedScaleAndFormatConversionConvertsEndpoints) {
using UTCJD = EncodedTime<scale::UTC, format::JD>;
Period<UTCJD> p(UTCJD(2460000.0), UTCJD(2460001.0));

auto out = p.to<scale::TT, format::MJD>();

static_assert(std::is_same_v<decltype(out), Period<EncodedTime<scale::TT, format::MJD>>>);
EXPECT_DOUBLE_EQ(out.start().value(), (p.start().to<scale::TT, format::MJD>().value()));
EXPECT_DOUBLE_EQ(out.end().value(), (p.end().to<scale::TT, format::MJD>().value()));
}

TEST(Period, EncodedFormatConversionConvertsEndpoints) {
using UTCJD = EncodedTime<scale::UTC, format::JD>;
Period<UTCJD> p(UTCJD(2460000.0), UTCJD(2460001.0));

auto out = p.to<format::MJD>();

static_assert(std::is_same_v<decltype(out), Period<EncodedTime<scale::UTC, format::MJD>>>);
EXPECT_DOUBLE_EQ(out.start().value(), p.start().to<format::MJD>().value());
EXPECT_DOUBLE_EQ(out.end().value(), p.end().to<format::MJD>().value());
}

TEST(Period, CivilPeriodConvertsFromUtcEndpoints) {
UTCPeriod p(CivilTime(2026, 1, 1, 0, 0, 0), CivilTime(2026, 1, 2, 0, 0, 0));

auto out = p.to<scale::TT, format::MJD>();

static_assert(std::is_same_v<decltype(out), Period<EncodedTime<scale::TT, format::MJD>>>);
EXPECT_DOUBLE_EQ(out.start().value(),
(Time<scale::UTC>::from_civil(p.start()).to<scale::TT, format::MJD>().value()));
EXPECT_DOUBLE_EQ(out.end().value(),
(Time<scale::UTC>::from_civil(p.end()).to<scale::TT, format::MJD>().value()));
}

TEST(Period, ToWithUsesProvidedContextForBothEndpoints) {
auto ctx = TimeContext::with_builtin_eop();
auto start = Time<scale::UTC>::from_civil({2026, 1, 1, 0, 0, 0});
auto end = Time<scale::UTC>::from_civil({2026, 1, 2, 0, 0, 0});
Period p(start, end);

auto ut1 = p.to_with<scale::UT1>(ctx);
auto ut1_mjd = p.to_with<scale::UT1, format::MJD>(ctx);

static_assert(std::is_same_v<decltype(ut1), Period<Time<scale::UT1>>>);
static_assert(std::is_same_v<decltype(ut1_mjd), Period<EncodedTime<scale::UT1, format::MJD>>>);
EXPECT_NEAR((ut1.start() - p.start().to_with<scale::UT1>(ctx)).value(), 0.0, 1e-5);
EXPECT_NEAR((ut1.end() - p.end().to_with<scale::UT1>(ctx)).value(), 0.0, 1e-5);
EXPECT_DOUBLE_EQ(ut1_mjd.start().value(),
(p.start().to_with<scale::UT1, format::MJD>(ctx).value()));
EXPECT_DOUBLE_EQ(ut1_mjd.end().value(), (p.end().to_with<scale::UT1, format::MJD>(ctx).value()));
}
Loading