diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..56f42d1 --- /dev/null +++ b/.clang-format @@ -0,0 +1,12 @@ +--- +Language: Cpp +BasedOnStyle: LLVM +IndentWidth: 2 +ColumnLimit: 80 +AllowShortBlocksOnASingleLine: Never +AllowShortFunctionsOnASingleLine: All +AllowShortEnumsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortLambdasOnASingleLine: All +AlwaysBreakTemplateDeclarations: MultiLine diff --git a/.gitmodules b/.gitmodules index 6ad6f34..657ef1c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "tempoch"] path = tempoch url = git@github.com:Siderust/tempoch.git +[submodule "qtty-cpp"] + path = qtty-cpp + url = https://github.com/Siderust/qtty-cpp.git diff --git a/CMakeLists.txt b/CMakeLists.txt index e62e825..6506efe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,9 @@ if(WIN32) endif() add_dependencies(tempoch_ffi build_tempoch_ffi) +# qtty-cpp integration (provides unit types for duration<>) +add_subdirectory(qtty-cpp) + # Header-only C++ wrapper library add_library(tempoch_cpp INTERFACE) target_include_directories(tempoch_cpp INTERFACE @@ -66,7 +69,7 @@ target_include_directories(tempoch_cpp INTERFACE $ $ ) -target_link_libraries(tempoch_cpp INTERFACE tempoch_ffi) +target_link_libraries(tempoch_cpp INTERFACE tempoch_ffi qtty_cpp) add_dependencies(tempoch_cpp build_tempoch_ffi) # Doxygen documentation @@ -77,13 +80,17 @@ if(TEMPOCH_BUILD_DOCS) set(TEMPOCH_DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile.tempoch_cpp) configure_file(${TEMPOCH_DOXYFILE_IN} ${TEMPOCH_DOXYFILE_OUT} @ONLY) - add_custom_target(docs - COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/docs/doxygen - COMMAND ${DOXYGEN_EXECUTABLE} ${TEMPOCH_DOXYFILE_OUT} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Generating API documentation with Doxygen" - VERBATIM - ) + if(NOT TARGET docs) + add_custom_target(docs + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/docs/doxygen + COMMAND ${DOXYGEN_EXECUTABLE} ${TEMPOCH_DOXYFILE_OUT} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Generating API documentation with Doxygen" + VERBATIM + ) + else() + message(STATUS "Top-level 'docs' target already exists; skipping creation to avoid conflict with subprojects") + endif() else() message(STATUS "Doxygen not found; 'docs' target will not be available") endif() diff --git a/examples/time_example.cpp b/examples/time_example.cpp index 2185ae9..f5eb33d 100644 --- a/examples/time_example.cpp +++ b/examples/time_example.cpp @@ -8,55 +8,62 @@ * ./build/time_example */ -#include -#include #include +#include +#include +#include int main() { - using namespace tempoch; - - // --------------------------------------------------------------- - // UTC → JulianDate → MJD round-trip - // --------------------------------------------------------------- - UTC utc(2026, 7, 15, 22, 0, 0); - std::cout << "UTC: " << utc.year << "-" - << std::setfill('0') << std::setw(2) << (int)utc.month << "-" - << std::setw(2) << (int)utc.day << " " - << std::setw(2) << (int)utc.hour << ":" - << std::setw(2) << (int)utc.minute << ":" - << std::setw(2) << (int)utc.second << "\n"; - - auto jd = JulianDate::from_utc(utc); - std::cout << "JD: " << std::fixed << std::setprecision(6) << jd.value() << "\n"; - - auto mjd = MJD::from_jd(jd); - std::cout << "MJD: " << std::fixed << std::setprecision(6) << mjd.value() << "\n"; - - auto utc2 = mjd.to_utc(); - std::cout << "Back: " << utc2.year << "-" - << std::setfill('0') << std::setw(2) << (int)utc2.month << "-" - << std::setw(2) << (int)utc2.day << " " - << std::setw(2) << (int)utc2.hour << ":" - << std::setw(2) << (int)utc2.minute << ":" - << std::setw(2) << (int)utc2.second << "\n\n"; - - // --------------------------------------------------------------- - // J2000 epoch and Julian centuries - // --------------------------------------------------------------- - auto j2000 = JulianDate::J2000(); - std::cout << "J2000.0: " << j2000.value() << "\n"; - std::cout << "Centuries since J2000: " << jd.julian_centuries() << "\n\n"; - - // --------------------------------------------------------------- - // Period intersection - // --------------------------------------------------------------- - Period night(60200.0, 60200.5); - Period obs(60200.2, 60200.8); - auto overlap = night.intersection(obs); - std::cout << "Night: [" << night.start_mjd() << ", " << night.end_mjd() << "]\n"; - std::cout << "Obs: [" << obs.start_mjd() << ", " << obs.end_mjd() << "]\n"; - std::cout << "Overlap: [" << overlap.start_mjd() << ", " << overlap.end_mjd() << "]\n"; - std::cout << "Overlap duration: " << overlap.duration_days() * 24.0 << " hours\n"; - - return 0; + using namespace tempoch; + + // --------------------------------------------------------------- + // UTC → JulianDate → MJD round-trip + // --------------------------------------------------------------- + UTC utc(2026, 7, 15, 22, 0, 0); + std::cout << "UTC: " << utc << "\n"; + + auto jd = JulianDate::from_utc(utc); + std::cout << "JD: " << std::fixed << std::setprecision(6) << jd << "\n"; + + auto mjd = MJD::from_jd(jd); + std::cout << "MJD: " << std::fixed << std::setprecision(6) << mjd << "\n"; + + auto utc2 = mjd.to_utc(); + std::cout << "Back: " << utc2 << "\n\n"; + + // --------------------------------------------------------------- + // J2000 epoch and Julian centuries + // --------------------------------------------------------------- + auto j2000 = JulianDate::J2000(); + std::cout << "J2000.0: " << j2000 << "\n"; + std::cout << "Centuries since J2000: " << jd.julian_centuries() << "\n\n"; + + // --------------------------------------------------------------- + // Period intersection (MJD — explicit MJD wrappers required) + // --------------------------------------------------------------- + Period night(MJD(60200.0), MJD(60200.5)); + Period obs(MJD(60200.2), MJD(60200.8)); + auto overlap = night.intersection(obs); + std::cout << "Night: " << night << "\n"; + std::cout << "Obs: " << obs << "\n"; + std::cout << "Overlap: " << overlap << "\n"; + std::cout << "Overlap duration: " << overlap.duration() << "\n\n"; + + // --------------------------------------------------------------- + // Period — start/end expressed directly as civil UTC + // --------------------------------------------------------------- + Period utc_semester(UTC(2026, 1, 1), UTC(2026, 7, 1)); + std::cout << "Semester (UTC): " << utc_semester << "\n"; + std::cout << " duration: " << utc_semester.duration() << "\n\n"; + + // --------------------------------------------------------------- + // Period — start/end as Julian Dates + // --------------------------------------------------------------- + auto jd_start = JulianDate::from_utc(UTC(2026, 1, 1)); + auto jd_end = JulianDate::from_utc(UTC(2026, 7, 1)); + Period jd_semester(jd_start, jd_end); + std::cout << "Semester (JD): " << jd_semester << "\n"; + std::cout << " duration: " << jd_semester.duration() << "\n"; + + return 0; } diff --git a/include/tempoch/ffi_core.hpp b/include/tempoch/ffi_core.hpp index 1772565..993d05c 100644 --- a/include/tempoch/ffi_core.hpp +++ b/include/tempoch/ffi_core.hpp @@ -26,7 +26,7 @@ namespace tempoch { */ class TempochException : public std::runtime_error { public: - explicit TempochException(const std::string& msg) : std::runtime_error(msg) {} + explicit TempochException(const std::string &msg) : std::runtime_error(msg) {} }; /** @@ -34,7 +34,7 @@ class TempochException : public std::runtime_error { */ class NullPointerError : public TempochException { public: - explicit NullPointerError(const std::string& msg) : TempochException(msg) {} + explicit NullPointerError(const std::string &msg) : TempochException(msg) {} }; /** @@ -42,7 +42,7 @@ class NullPointerError : public TempochException { */ class UtcConversionError : public TempochException { public: - explicit UtcConversionError(const std::string& msg) : TempochException(msg) {} + explicit UtcConversionError(const std::string &msg) : TempochException(msg) {} }; /** @@ -50,7 +50,7 @@ class UtcConversionError : public TempochException { */ class InvalidPeriodError : public TempochException { public: - explicit InvalidPeriodError(const std::string& msg) : TempochException(msg) {} + explicit InvalidPeriodError(const std::string &msg) : TempochException(msg) {} }; /** @@ -58,7 +58,8 @@ class InvalidPeriodError : public TempochException { */ class NoIntersectionError : public TempochException { public: - explicit NoIntersectionError(const std::string& msg) : TempochException(msg) {} + explicit NoIntersectionError(const std::string &msg) + : TempochException(msg) {} }; // ============================================================================ @@ -68,22 +69,24 @@ class NoIntersectionError : public TempochException { /** * @brief Check a tempoch_status_t and throw the appropriate exception on error. */ -inline void check_status(tempoch_status_t status, const char* operation) { - if (status == TEMPOCH_STATUS_T_OK) return; +inline void check_status(tempoch_status_t status, const char *operation) { + if (status == TEMPOCH_STATUS_T_OK) + return; - std::string msg = std::string(operation) + " failed: "; - switch (status) { - case TEMPOCH_STATUS_T_NULL_POINTER: - throw NullPointerError(msg + "null output pointer"); - case TEMPOCH_STATUS_T_UTC_CONVERSION_FAILED: - throw UtcConversionError(msg + "UTC conversion failed"); - case TEMPOCH_STATUS_T_INVALID_PERIOD: - throw InvalidPeriodError(msg + "invalid period (start > end)"); - case TEMPOCH_STATUS_T_NO_INTERSECTION: - throw NoIntersectionError(msg + "periods do not intersect"); - default: - throw TempochException(msg + "unknown error (" + std::to_string(status) + ")"); - } + std::string msg = std::string(operation) + " failed: "; + switch (status) { + case TEMPOCH_STATUS_T_NULL_POINTER: + throw NullPointerError(msg + "null output pointer"); + case TEMPOCH_STATUS_T_UTC_CONVERSION_FAILED: + throw UtcConversionError(msg + "UTC conversion failed"); + case TEMPOCH_STATUS_T_INVALID_PERIOD: + throw InvalidPeriodError(msg + "invalid period (start > end)"); + case TEMPOCH_STATUS_T_NO_INTERSECTION: + throw NoIntersectionError(msg + "periods do not intersect"); + default: + throw TempochException(msg + "unknown error (" + std::to_string(status) + + ")"); + } } } // namespace tempoch diff --git a/include/tempoch/period.hpp b/include/tempoch/period.hpp index 7e0dfdc..65ab3ef 100644 --- a/include/tempoch/period.hpp +++ b/include/tempoch/period.hpp @@ -2,100 +2,176 @@ /** * @file period.hpp - * @brief C++ wrapper for time periods in MJD. + * @brief C++ wrapper for time periods, generic over any time type. * - * Wraps the tempoch-ffi Period API with a value-semantic, exception-safe C++ type. + * Wraps the tempoch-ffi Period API with a value-semantic, exception-safe C++ + * class template `Period`. The underlying storage is always + * `tempoch_period_mjd_t`; `TimeTraits` handles conversion to/from the raw + * MJD doubles. */ +#include "qtty/qtty.hpp" #include "time.hpp" +#include #include namespace tempoch { // ============================================================================ -// Period +// TimeTraits — connect each time type to the MJD-based FFI layer // ============================================================================ /** - * @brief A time period [start, end] in MJD. + * @brief Conversion traits between a time type @p T and raw MJD doubles. * - * Wraps `tempoch_period_mjd_t` and provides duration, intersection, - * and easy access to the start/end as `MJD` objects. + * 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(); } +}; + +// ============================================================================ +// Period +// ============================================================================ + +/** + * @brief A time period [start, end] parameterised on a time type @p T. + * + * Internally stores start/end as raw MJD days (matching the FFI layer) and + * converts to/from @p T on demand via `TimeTraits`. + * + * @tparam T A time type for which `TimeTraits` is defined: + * `MJD` (default), `JulianDate`, or `UTC`. * * @code - * tempoch::Period night(60200.0, 60200.5); - * double hours = night.duration_days() * 24.0; + * // MJD period — must use explicit MJD wrappers to avoid JD/MJD ambiguity + * tempoch::Period night(tempoch::MJD(60200.0), tempoch::MJD(60200.5)); * - * tempoch::Period obs(60200.2, 60200.8); - * auto overlap = night.intersection(obs); // [0.2, 0.5] + * // Julian-Date period + * auto start = tempoch::JulianDate::from_utc({2026, 1, 1}); + * auto end = tempoch::JulianDate::from_utc({2026, 6, 1}); + * tempoch::Period jd_run(start, end); + * + * // UTC period + * tempoch::Period utc_run(tempoch::UTC(2026, 1, 1), tempoch::UTC(2026, 6, 1)); * @endcode */ -class Period { - tempoch_period_mjd_t m_inner; +template class Period { + tempoch_period_mjd_t m_inner; + + /// Private struct-based constructor used by from_c(); skips re-validation. + explicit Period(const tempoch_period_mjd_t &inner) : m_inner(inner) {} public: - /** - * @brief Construct a period from start/end MJD values. - * @param start_mjd Inclusive start instant, in MJD days. - * @param end_mjd Inclusive end instant, in MJD days. - * @throws InvalidPeriodError If @p start_mjd is greater than @p end_mjd. - */ - Period(double start_mjd, double end_mjd) { - check_status( - tempoch_period_mjd_new(start_mjd, end_mjd, &m_inner), - "Period::Period" - ); - } - - /** - * @brief Construct a period from typed MJD values. - * @param start Inclusive start instant. - * @param end Inclusive end instant. - */ - Period(const MJD& start, const MJD& end) - : Period(start.value(), end.value()) {} - - /// Construct from the C struct (unchecked). - static Period from_c(const tempoch_period_mjd_t& c) { - Period p(0.0, 1.0); // dummy - p.m_inner = c; - return p; - } - - /// Inclusive period start as raw MJD days. - double start_mjd() const { return m_inner.start_mjd; } - - /// Inclusive period end as raw MJD days. - double end_mjd() const { return m_inner.end_mjd; } - - /// Inclusive period start as a typed MJD value. - MJD start() const { return MJD(m_inner.start_mjd); } - - /// Inclusive period end as a typed MJD value. - MJD end() const { return MJD(m_inner.end_mjd); } - - /// Duration in days. - double duration_days() const { - return tempoch_period_mjd_duration_days(m_inner); - } - - /** - * @brief Compute the overlapping interval with another period. - * @param other The period to intersect with. - * @return The overlap interval. - * @throws NoIntersectionError If the two periods do not overlap. - */ - Period intersection(const Period& other) const { - tempoch_period_mjd_t out; - check_status( - tempoch_period_mjd_intersection(m_inner, other.m_inner, &out), - "Period::intersection" - ); - return from_c(out); - } - - /// Access the underlying FFI POD value. - const tempoch_period_mjd_t& c_inner() const { return m_inner; } + /** + * @brief Construct from typed start/end values. + * + * Raw `double` values are intentionally not accepted to prevent the + * JD-vs-MJD ambiguity. Wrap the value in the appropriate type first: + * `MJD(mjd_value)`, `JulianDate(jd_value)`, or a `UTC{…}` struct. + * + * @param start Inclusive start instant. + * @param end Inclusive end instant. + * @throws InvalidPeriodError If @p start is later than @p end. + */ + Period(const T &start, const T &end) { + check_status(tempoch_period_mjd_new(TimeTraits::to_mjd_value(start), + TimeTraits::to_mjd_value(end), + &m_inner), + "Period::Period"); + } + + /// Construct from the C FFI struct (bypasses public validation; FFI output is + /// trusted). + static Period from_c(const tempoch_period_mjd_t &c) { return Period(c); } + + /// Inclusive period start as a value of type @p T. + T start() const { return TimeTraits::from_mjd_value(m_inner.start_mjd); } + + /// Inclusive period end as a value of type @p T. + T end() const { return TimeTraits::from_mjd_value(m_inner.end_mjd); } + + /** + * @brief Duration as a qtty time quantity. + * + * Returns the period length converted to the requested unit. + * The raw FFI value (in days) is wrapped as a `qtty::Day` and then + * converted via the qtty unit-conversion layer. + * + * @tparam TargetType A qtty unit tag (e.g., `qtty::DayTag`) or its + * convenience alias (e.g., `qtty::Day`, `qtty::Second`, + * `qtty::Hour`). Defaults to `qtty::DayTag`. + * @return The duration as a `qtty::Quantity` in the requested unit. + * + * @code + * tempoch::Period p(tempoch::MJD(60200.0), tempoch::MJD(60201.5)); // 1.5-day + * period auto d = p.duration(); // qtty::Day → 1.5 + * d auto h = p.duration(); // qtty::Hour → 36 h + * auto s = p.duration(); // qtty::Second → 129600 s + * @endcode + */ + template + qtty::Quantity::type> duration() const { + double days = tempoch_period_mjd_duration_days(m_inner); + return qtty::Quantity(days).template to(); + } + + /** + * @brief Compute the overlapping interval with another period. + * @param other The period to intersect with. + * @return The overlap as a `Period`. + * @throws NoIntersectionError If the two periods do not overlap. + */ + Period intersection(const Period &other) const { + tempoch_period_mjd_t out; + check_status(tempoch_period_mjd_intersection(m_inner, other.m_inner, &out), + "Period::intersection"); + return from_c(out); + } + + /// Access the underlying FFI POD value. + const tempoch_period_mjd_t &c_inner() const { return m_inner; } }; +// ============================================================================ +// C++17 deduction guides +// ============================================================================ + +/// Typed time values → Period (covers MJD, JulianDate, UTC, …). +template Period(T, T) -> Period; + +// ============================================================================ +// Convenience type aliases +// ============================================================================ + +using MJDPeriod = Period; ///< Period expressed in Modified Julian Date. +using JDPeriod = Period; ///< Period expressed in Julian Date. +using UTCPeriod = Period; ///< Period expressed in UTC civil time. + +// ============================================================================ +// operator<< +// ============================================================================ + +/// Stream a Period as [start, end] using T's own operator<<. +template +inline std::ostream &operator<<(std::ostream &os, const Period &p) { + return os << '[' << p.start() << ", " << p.end() << ']'; +} + } // namespace tempoch diff --git a/include/tempoch/tempoch.hpp b/include/tempoch/tempoch.hpp index 3a2c22c..84ea18b 100644 --- a/include/tempoch/tempoch.hpp +++ b/include/tempoch/tempoch.hpp @@ -20,5 +20,5 @@ */ #include "ffi_core.hpp" -#include "time.hpp" #include "period.hpp" +#include "time.hpp" diff --git a/include/tempoch/time.hpp b/include/tempoch/time.hpp index 94a92d8..e92ff26 100644 --- a/include/tempoch/time.hpp +++ b/include/tempoch/time.hpp @@ -8,6 +8,8 @@ */ #include "ffi_core.hpp" +#include +#include namespace tempoch { @@ -26,47 +28,51 @@ namespace tempoch { * @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); - } + /// 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); + } }; // ============================================================================ @@ -87,58 +93,63 @@ struct UTC { * @endcode */ class JulianDate { - double m_value; + 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; } + 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) // ============================================================================ @@ -156,54 +167,74 @@ class JulianDate { * @endcode */ class MJD { - double m_value; + 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())); - } - - /// 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)); - } - - 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; } + 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())); + } + + /// 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)); + } + + 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; } }; +/// Stream a MJD as its raw double value. +inline std::ostream &operator<<(std::ostream &os, const MJD &mjd) { + return os << mjd.value(); +} + +/// 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; +} + } // namespace tempoch diff --git a/qtty-cpp b/qtty-cpp new file mode 160000 index 0000000..0dea35c --- /dev/null +++ b/qtty-cpp @@ -0,0 +1 @@ +Subproject commit 0dea35c5e671fb2275609e250179b7ee9c31f3b3 diff --git a/tests/main.cpp b/tests/main.cpp index 5ebbc76..4d820af 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -1,6 +1,6 @@ #include -int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/tests/test_period.cpp b/tests/test_period.cpp index 4359c4a..dd9ed11 100644 --- a/tests/test_period.cpp +++ b/tests/test_period.cpp @@ -1,4 +1,5 @@ #include +#include #include using namespace tempoch; @@ -8,53 +9,61 @@ using namespace tempoch; // ============================================================================ TEST(Period, Duration) { - Period p(60200.0, 60201.0); - EXPECT_NEAR(p.duration_days(), 1.0, 1e-10); + Period p(MJD(60200.0), MJD(60201.0)); + EXPECT_NEAR(p.duration().value(), 1.0, 1e-10); +} + +TEST(Period, DurationInSeconds) { + Period p(MJD(60200.0), MJD(60201.0)); // 1 day + EXPECT_NEAR(p.duration().value(), 86400.0, 1e-6); +} + +TEST(Period, DurationInHours) { + Period p(MJD(60200.0), MJD(60201.0)); // 1 day = 24 hours + EXPECT_NEAR(p.duration().value(), 24.0, 1e-10); } TEST(Period, StartEnd) { - Period p(60200.0, 60201.0); - EXPECT_NEAR(p.start_mjd(), 60200.0, 1e-10); - EXPECT_NEAR(p.end_mjd(), 60201.0, 1e-10); - EXPECT_NEAR(p.start().value(), 60200.0, 1e-10); - EXPECT_NEAR(p.end().value(), 60201.0, 1e-10); + Period p(MJD(60200.0), MJD(60201.0)); + EXPECT_NEAR(p.start().value(), 60200.0, 1e-10); + EXPECT_NEAR(p.end().value(), 60201.0, 1e-10); } TEST(Period, FromMJD) { - MJD s(60200.0); - MJD e(60201.0); - Period p(s, e); - EXPECT_NEAR(p.duration_days(), 1.0, 1e-10); + MJD s(60200.0); + MJD e(60201.0); + Period p(s, e); + EXPECT_NEAR(p.duration().value(), 1.0, 1e-10); } TEST(Period, Intersection) { - Period a(60200.0, 60202.0); - Period b(60201.0, 60203.0); - auto c = a.intersection(b); - EXPECT_NEAR(c.start_mjd(), 60201.0, 1e-10); - EXPECT_NEAR(c.end_mjd(), 60202.0, 1e-10); + Period a(MJD(60200.0), MJD(60202.0)); + Period b(MJD(60201.0), MJD(60203.0)); + auto c = a.intersection(b); + EXPECT_NEAR(c.start().value(), 60201.0, 1e-10); + EXPECT_NEAR(c.end().value(), 60202.0, 1e-10); } TEST(Period, NoIntersectionThrows) { - Period a(60200.0, 60201.0); - Period b(60202.0, 60203.0); - EXPECT_THROW(a.intersection(b), NoIntersectionError); + Period a(MJD(60200.0), MJD(60201.0)); + Period b(MJD(60202.0), MJD(60203.0)); + EXPECT_THROW(a.intersection(b), NoIntersectionError); } TEST(Period, InvalidThrows) { - EXPECT_THROW(Period(60203.0, 60200.0), InvalidPeriodError); + EXPECT_THROW((Period(MJD(60203.0), MJD(60200.0))), InvalidPeriodError); } TEST(Period, CInner) { - Period p(60200.0, 60201.0); - auto c = p.c_inner(); - EXPECT_NEAR(c.start_mjd, 60200.0, 1e-10); - EXPECT_NEAR(c.end_mjd, 60201.0, 1e-10); + Period p(MJD(60200.0), MJD(60201.0)); + auto c = p.c_inner(); + EXPECT_NEAR(c.start_mjd, 60200.0, 1e-10); + EXPECT_NEAR(c.end_mjd, 60201.0, 1e-10); } TEST(Period, FromC) { - tempoch_period_mjd_t c{60200.0, 60201.0}; - auto p = Period::from_c(c); - EXPECT_NEAR(p.start_mjd(), 60200.0, 1e-10); - EXPECT_NEAR(p.duration_days(), 1.0, 1e-10); + tempoch_period_mjd_t c{60200.0, 60201.0}; + auto p = Period::from_c(c); + EXPECT_NEAR(p.start().value(), 60200.0, 1e-10); + EXPECT_NEAR(p.duration().value(), 1.0, 1e-10); } diff --git a/tests/test_time.cpp b/tests/test_time.cpp index 582c896..24b3971 100644 --- a/tests/test_time.cpp +++ b/tests/test_time.cpp @@ -8,44 +8,44 @@ using namespace tempoch; // ============================================================================ TEST(Time, JulianDateJ2000) { - auto jd = JulianDate::J2000(); - EXPECT_DOUBLE_EQ(jd.value(), 2451545.0); + auto jd = JulianDate::J2000(); + EXPECT_DOUBLE_EQ(jd.value(), 2451545.0); } TEST(Time, JulianDateFromUtc) { - auto jd = JulianDate::from_utc({2000, 1, 1, 12, 0, 0}); - EXPECT_NEAR(jd.value(), 2451545.0, 0.001); + auto jd = JulianDate::from_utc({2000, 1, 1, 12, 0, 0}); + EXPECT_NEAR(jd.value(), 2451545.0, 0.001); } TEST(Time, JulianDateRoundtripUtc) { - UTC original(2026, 7, 15, 22, 0, 0); - auto jd = JulianDate::from_utc(original); - 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); + UTC original(2026, 7, 15, 22, 0, 0); + auto jd = JulianDate::from_utc(original); + 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, JulianDateArithmetic) { - auto jd1 = JulianDate(2451545.0); - auto jd2 = jd1 + 365.25; - EXPECT_NEAR(jd2 - jd1, 365.25, 1e-10); + auto jd1 = JulianDate(2451545.0); + auto jd2 = jd1 + 365.25; + EXPECT_NEAR(jd2 - jd1, 365.25, 1e-10); } TEST(Time, JulianCenturies) { - auto jd = JulianDate::J2000(); - EXPECT_NEAR(jd.julian_centuries(), 0.0, 1e-10); + auto jd = JulianDate::J2000(); + EXPECT_NEAR(jd.julian_centuries(), 0.0, 1e-10); } TEST(Time, JulianDateComparisons) { - auto a = JulianDate(2451545.0); - auto b = JulianDate(2451546.0); - EXPECT_TRUE(a < b); - EXPECT_TRUE(b > a); - EXPECT_TRUE(a <= b); - EXPECT_TRUE(a == JulianDate(2451545.0)); - EXPECT_TRUE(a != b); + auto a = JulianDate(2451545.0); + auto b = JulianDate(2451546.0); + EXPECT_TRUE(a < b); + EXPECT_TRUE(b > a); + EXPECT_TRUE(a <= b); + EXPECT_TRUE(a == JulianDate(2451545.0)); + EXPECT_TRUE(a != b); } // ============================================================================ @@ -53,41 +53,41 @@ TEST(Time, JulianDateComparisons) { // ============================================================================ TEST(Time, MjdFromJd) { - auto jd = JulianDate::J2000(); - auto mjd = MJD::from_jd(jd); - EXPECT_NEAR(mjd.value(), jd.to_mjd(), 1e-10); + auto jd = JulianDate::J2000(); + auto mjd = MJD::from_jd(jd); + EXPECT_NEAR(mjd.value(), jd.to_mjd(), 1e-10); } TEST(Time, MjdRoundtrip) { - auto mjd1 = MJD(60200.0); - auto jd = mjd1.to_jd(); - auto mjd2 = MJD::from_jd(jd); - EXPECT_NEAR(mjd1.value(), mjd2.value(), 1e-10); + auto mjd1 = MJD(60200.0); + auto jd = mjd1.to_jd(); + auto mjd2 = MJD::from_jd(jd); + EXPECT_NEAR(mjd1.value(), mjd2.value(), 1e-10); } TEST(Time, MjdFromUtc) { - auto mjd = MJD::from_utc({2026, 7, 15, 12, 0, 0}); - auto utc = mjd.to_utc(); - EXPECT_EQ(utc.year, 2026); - EXPECT_EQ(utc.month, 7); - EXPECT_EQ(utc.day, 15); + auto mjd = MJD::from_utc({2026, 7, 15, 12, 0, 0}); + auto utc = mjd.to_utc(); + EXPECT_EQ(utc.year, 2026); + EXPECT_EQ(utc.month, 7); + EXPECT_EQ(utc.day, 15); } TEST(Time, MjdArithmetic) { - auto mjd1 = MJD(60200.0); - auto mjd2 = mjd1 + 1.5; - EXPECT_NEAR(mjd2 - mjd1, 1.5, 1e-10); + auto mjd1 = MJD(60200.0); + auto mjd2 = mjd1 + 1.5; + EXPECT_NEAR(mjd2 - mjd1, 1.5, 1e-10); } TEST(Time, MjdComparisons) { - auto a = MJD(60200.0); - auto b = MJD(60201.0); - EXPECT_TRUE(a < b); - EXPECT_TRUE(b > a); - EXPECT_TRUE(a <= a); - EXPECT_TRUE(a >= a); - EXPECT_TRUE(a == MJD(60200.0)); - EXPECT_TRUE(a != b); + auto a = MJD(60200.0); + auto b = MJD(60201.0); + EXPECT_TRUE(a < b); + EXPECT_TRUE(b > a); + EXPECT_TRUE(a <= a); + EXPECT_TRUE(a >= a); + EXPECT_TRUE(a == MJD(60200.0)); + EXPECT_TRUE(a != b); } // ============================================================================ @@ -95,22 +95,22 @@ TEST(Time, MjdComparisons) { // ============================================================================ TEST(Time, UtcDefaults) { - UTC utc; - EXPECT_EQ(utc.year, 2000); - EXPECT_EQ(utc.month, 1); - EXPECT_EQ(utc.day, 1); - EXPECT_EQ(utc.hour, 12); + UTC utc; + EXPECT_EQ(utc.year, 2000); + EXPECT_EQ(utc.month, 1); + EXPECT_EQ(utc.day, 1); + EXPECT_EQ(utc.hour, 12); } TEST(Time, UtcToCRoundtrip) { - UTC utc(2026, 3, 14, 9, 26, 53, 589); - auto c = utc.to_c(); - auto utc2 = UTC::from_c(c); - EXPECT_EQ(utc2.year, 2026); - EXPECT_EQ(utc2.month, 3); - EXPECT_EQ(utc2.day, 14); - EXPECT_EQ(utc2.hour, 9); - EXPECT_EQ(utc2.minute, 26); - EXPECT_EQ(utc2.second, 53); - EXPECT_EQ(utc2.nanosecond, 589u); + UTC utc(2026, 3, 14, 9, 26, 53, 589); + auto c = utc.to_c(); + auto utc2 = UTC::from_c(c); + EXPECT_EQ(utc2.year, 2026); + EXPECT_EQ(utc2.month, 3); + EXPECT_EQ(utc2.day, 14); + EXPECT_EQ(utc2.hour, 9); + EXPECT_EQ(utc2.minute, 26); + EXPECT_EQ(utc2.second, 53); + EXPECT_EQ(utc2.nanosecond, 589u); }