From 41222d8d8798ef371e3ec776656043c74db412ae Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Fri, 29 Jan 2021 22:37:46 -0800 Subject: [PATCH 1/8] Add TimeDelta --- datetime/v1/CMakeLists.txt | 87 +++ datetime/v1/caesar/datetime.hpp | 3 + datetime/v1/caesar/datetime/timedelta.cpp | 1 + datetime/v1/caesar/datetime/timedelta.hpp | 577 +++++++++++++++++++ datetime/v1/cmake/CaesarParseFile.cmake | 28 + datetime/v1/sources.txt | 1 + datetime/v1/test/datetime/timedelta_test.cpp | 524 +++++++++++++++++ datetime/v1/test/main.cpp | 2 + datetime/v1/tests.txt | 2 + datetime/v1/warnings.txt | 45 ++ 10 files changed, 1270 insertions(+) create mode 100644 datetime/v1/CMakeLists.txt create mode 100644 datetime/v1/caesar/datetime.hpp create mode 100644 datetime/v1/caesar/datetime/timedelta.cpp create mode 100644 datetime/v1/caesar/datetime/timedelta.hpp create mode 100644 datetime/v1/cmake/CaesarParseFile.cmake create mode 100644 datetime/v1/sources.txt create mode 100644 datetime/v1/test/datetime/timedelta_test.cpp create mode 100644 datetime/v1/test/main.cpp create mode 100644 datetime/v1/tests.txt create mode 100644 datetime/v1/warnings.txt diff --git a/datetime/v1/CMakeLists.txt b/datetime/v1/CMakeLists.txt new file mode 100644 index 0000000..0505cff --- /dev/null +++ b/datetime/v1/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.19) + +project(datetime LANGUAGES CXX) + +# Add custom CMake modules to module include path. +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake) + +# Check if this is the top-level CMake project or was included as a sub-project. +if(PROJECT_NAME STREQUAL CMAKE_PROJECT_NAME) + set(CAESAR_MAIN_PROJECT ON) +else() + set(CAESAR_MAIN_PROJECT OFF) +endif() + +# Project configuration options. +option(CAESAR_TEST "Enable test suite" ${CAESAR_MAIN_PROJECT}) +option(CAESAR_WERROR "Treat compiler warnings as errors" ON) + +# Import third-party dependencies. +find_package(absl REQUIRED CONFIG COMPONENTS numeric) +#find_package(date REQUIRED CONFIG COMPONENTS date date-tz) +find_package(doctest REQUIRED CONFIG) +find_package(fmt REQUIRED CONFIG) + +# Enable compiler diagnostic flags. +include(CaesarParseFile) +caesar_parse_file(warnings.txt warnings) + +if(CAESAR_WERROR) + list(APPEND warnings -Werror) +endif() + +include(CheckCompilerFlag) +foreach(warning ${warnings}) + check_compiler_flag(CXX ${warning} cxx_${warning}_supported) + if(cxx_${warning}_supported) + add_compile_options($<$:${warning}>) + endif() +endforeach() + +add_library(datetime SHARED) +add_library(caesar::datetime ALIAS datetime) + +# Require C++17. +target_compile_features(datetime PUBLIC cxx_std_17) + +# Add sources. +include(CaesarParseFile) +caesar_parse_file(sources.txt sources) +target_sources(datetime PRIVATE ${sources}) + +# Add include dirs. +target_include_directories( + datetime + PUBLIC + $ + ) + +# Link to imported targets. +target_link_libraries( + datetime + PUBLIC + absl::numeric +# date::date +# date::date-tz + PRIVATE + fmt::fmt + ) + +if(CAESAR_TEST) + enable_testing() + + # Add test sources. + caesar_parse_file(tests.txt tests) + add_executable(datetime-test ${tests}) + + target_link_libraries( + datetime-test + PRIVATE + caesar::datetime + doctest::doctest + ) + + # Register tests with CTest. + include(doctest) + doctest_discover_tests(datetime-test) +endif() diff --git a/datetime/v1/caesar/datetime.hpp b/datetime/v1/caesar/datetime.hpp new file mode 100644 index 0000000..6259c65 --- /dev/null +++ b/datetime/v1/caesar/datetime.hpp @@ -0,0 +1,3 @@ +#pragma once + +#include "datetime/timedelta.hpp" diff --git a/datetime/v1/caesar/datetime/timedelta.cpp b/datetime/v1/caesar/datetime/timedelta.cpp new file mode 100644 index 0000000..6dc36a8 --- /dev/null +++ b/datetime/v1/caesar/datetime/timedelta.cpp @@ -0,0 +1 @@ +#include "timedelta.hpp" diff --git a/datetime/v1/caesar/datetime/timedelta.hpp b/datetime/v1/caesar/datetime/timedelta.hpp new file mode 100644 index 0000000..07518f1 --- /dev/null +++ b/datetime/v1/caesar/datetime/timedelta.hpp @@ -0,0 +1,577 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace caesar { + +/** + * Represents a signed duration, the difference between two timepoints, with + * picosecond resolution + * + * Internally, TimeDelta stores a 128-bit integer tick count of picoseconds, + * allowing it to represent spans of trillions of years without loss of + * precision. + * + * TimeDelta objects can be constructed from or converted to instances of + * std::chrono::duration. In addition, the factory functions `days()`, + * `hours()`, `minutes()`, `seconds()`, `milliseconds()`, `microseconds()`, + * `nanoseconds()`, and `picoseconds()` can be used to create TimeDelta objects. + * + * \see GPSTime + * \see UTCTime + */ +class TimeDelta { + /** + * \private + * Checks whether `T` is a (built-in or user-defined) arithmetic type. + * + * Formally, this checks whether `std::numeric_limits` is specialized for + * `T`. `std::numeric_limits` is specialized for all built-in arithmetic + * types and, unlike `std::is_arithmetic`, may be specialized for + * user-defined types. + */ + template + constexpr static bool is_arithmetic = + std::numeric_limits::is_specialized; + +public: + /** A signed arithmetic type representing the number of ticks */ + using Rep = absl::int128; + + /** + * A `std::ratio` representing the tick period (i.e. the number of ticks per + * second). + */ + using Period = std::pico; + + /** Construct a new TimeDelta object representing a zero-length duration. */ + TimeDelta() = default; + + /** + * Construct a new TimeDelta object from a std::chrono::duration. + * + * If the input has sub-picosecond resolution, it will be truncated to an + * integer multiple of `TimeDelta::resolution`. + * + * ## Notes + * + * Casting from a floating-point duration is subject to undefined behavior + * when the floating-point value is NaN, infinity, or too large to be + * representable by `TimeDelta::Rep`. Otherwise, casting is subject to + * truncation as with any `static_cast` to `TimeDelta::Rep`. + */ + template + explicit constexpr TimeDelta( + const std::chrono::duration& d); + + /** + * Convert to std::chrono::duration. + * + * If the output has wider resolution than `TimeDelta::resolution`, the + * conversion is subject to truncation. + * + * ## Notes + * + * Casting is subject to truncation as with any `static_cast` to `ToRep`. + */ + template + explicit constexpr operator std::chrono::duration() const; + + /** Return the smallest representable TimeDelta. */ + static constexpr TimeDelta + min() noexcept + { + return TimeDelta(std::chrono::duration::min()); + } + + /** Return the largest representable TimeDelta. */ + static constexpr TimeDelta + max() noexcept + { + return TimeDelta(std::chrono::duration::max()); + } + + /** + * Return the smallest possible difference between non-equal TimeDelta + * objects. + */ + static constexpr TimeDelta + resolution() noexcept + { + return TimeDelta(std::chrono::duration(1)); + } + + /** + * Return a TimeDelta object representing the specified number of days. + * + * \tparam T an arithmetic type representing the number of days + * + * ## Notes + * + * Casting from a floating-point value is subject to undefined behavior + * when the value is NaN, infinity, or too large to be representable by + * `TimeDelta::Rep`. Otherwise, casting is subject to truncation as with any + * `static_cast` to `TimeDelta::Rep`. + */ + template + static constexpr TimeDelta + days(T d) + { + static_assert(is_arithmetic); + using Days = std::chrono::duration>; + return TimeDelta(Days(d)); + } + + /** + * Return a TimeDelta object representing the specified number of hours. + * + * \tparam T an arithmetic type representing the number of hours + * + * ## Notes + * + * Casting from a floating-point value is subject to undefined behavior + * when the value is NaN, infinity, or too large to be representable by + * `TimeDelta::Rep`. Otherwise, casting is subject to truncation as with any + * `static_cast` to `TimeDelta::Rep`. + */ + template + static constexpr TimeDelta + hours(T h) + { + static_assert(is_arithmetic); + using Hours = std::chrono::duration>; + return TimeDelta(Hours(h)); + } + + /** + * Return a TimeDelta object representing the specified number of minutes. + * + * \tparam T an arithmetic type representing the number of minutes + * + * ## Notes + * + * Casting from a floating-point value is subject to undefined behavior + * when the value is NaN, infinity, or too large to be representable by + * `TimeDelta::Rep`. Otherwise, casting is subject to truncation as with any + * `static_cast` to `TimeDelta::Rep`. + */ + template + static constexpr TimeDelta + minutes(T m) + { + static_assert(is_arithmetic); + using Minutes = std::chrono::duration>; + return TimeDelta(Minutes(m)); + } + + /** + * Return a TimeDelta object representing the specified number of seconds. + * + * \tparam T an arithmetic type representing the number of seconds + * + * ## Notes + * + * Casting from a floating-point value is subject to undefined behavior + * when the value is NaN, infinity, or too large to be representable by + * `TimeDelta::Rep`. Otherwise, casting is subject to truncation as with any + * `static_cast` to `TimeDelta::Rep`. + */ + template + static constexpr TimeDelta + seconds(T s) + { + static_assert(is_arithmetic); + using Seconds = std::chrono::duration; + return TimeDelta(Seconds(s)); + } + + /** + * Return a TimeDelta object representing the specified number of + * milliseconds. + * + * \tparam T an arithmetic type representing the number of milliseconds + * + * ## Notes + * + * Casting from a floating-point value is subject to undefined behavior + * when the value is NaN, infinity, or too large to be representable by + * `TimeDelta::Rep`. Otherwise, casting is subject to truncation as with any + * `static_cast` to `TimeDelta::Rep`. + */ + template + static constexpr TimeDelta + milliseconds(T ms) + { + static_assert(is_arithmetic); + using Millisecs = std::chrono::duration; + return TimeDelta(Millisecs(ms)); + } + + /** + * Return a TimeDelta object representing the specified number of + * microseconds. + * + * \tparam T an arithmetic type representing the number of microseconds + * + * ## Notes + * + * Casting from a floating-point value is subject to undefined behavior + * when the value is NaN, infinity, or too large to be representable by + * `TimeDelta::Rep`. Otherwise, casting is subject to truncation as with any + * `static_cast` to `TimeDelta::Rep`. + */ + template + static constexpr TimeDelta + microseconds(T us) + { + static_assert(is_arithmetic); + using Microsecs = std::chrono::duration; + return TimeDelta(Microsecs(us)); + } + + /** + * Return a TimeDelta object representing the specified number of + * nanoseconds. + * + * \tparam T an arithmetic type representing the number of nanoseconds + * + * ## Notes + * + * Casting from a floating-point value is subject to undefined behavior + * when the value is NaN, infinity, or too large to be representable by + * `TimeDelta::Rep`. Otherwise, casting is subject to truncation as with any + * `static_cast` to `TimeDelta::Rep`. + */ + template + static constexpr TimeDelta + nanoseconds(T ns) + { + static_assert(is_arithmetic); + using Nanosecs = std::chrono::duration; + return TimeDelta(Nanosecs(ns)); + } + + /** + * Return a TimeDelta object representing the specified number of + * picoseconds. + * + * \tparam T an arithmetic type representing the number of picoseconds + * + * ## Notes + * + * Casting from a floating-point value is subject to undefined behavior + * when the value is NaN, infinity, or too large to be representable by + * `TimeDelta::Rep`. Otherwise, casting is subject to truncation as with any + * `static_cast` to `TimeDelta::Rep`. + */ + template + static constexpr TimeDelta + picoseconds(T ps) + { + static_assert(is_arithmetic); + using Picosecs = std::chrono::duration; + return TimeDelta(Picosecs(ps)); + } + + /** Return the tick count. */ + constexpr Rep + count() const noexcept + { + return duration_.count(); + } + + /** Return the total number of seconds in the duration. */ + double + total_seconds() const + { + using Seconds = std::chrono::duration; + return Seconds(*this).count(); + } + + /** Returns a copy of the TimeDelta object. */ + constexpr TimeDelta + operator+() const noexcept + { + return *this; + } + + /** Returns the negation of the TimeDelta object. */ + constexpr TimeDelta + operator-() const + { + return TimeDelta(-duration_); + } + + /** Increment the tick count. */ + constexpr TimeDelta& + operator++() + { + ++duration_; + return *this; + } + + /** \copydoc TimeDelta::operator++() */ + constexpr TimeDelta + operator++(int) + { + return TimeDelta(duration_++); + } + + /** Decrement the tick count. */ + constexpr TimeDelta& + operator--() + { + --duration_; + return *this; + } + + /** \copydoc TimeDelta::operator--() */ + constexpr TimeDelta + operator--(int) + { + return TimeDelta(duration_--); + } + + /** Perform addition in-place and return the modified result. */ + constexpr TimeDelta& + operator+=(const TimeDelta& other) + { + duration_ += other.duration_; + return *this; + } + + /** Perform subtraction in-place and return the modified result. */ + constexpr TimeDelta& + operator-=(const TimeDelta& other) + { + duration_ -= other.duration_; + return *this; + } + + /** Perform multiplication in-place and return the modified result. */ + constexpr TimeDelta& + operator*=(Rep mul) + { + duration_ *= mul; + return *this; + } + + /** + * Perform division in-place, truncating toward zero, and return the + * modified result. + * + * ## Notes + * + * Division by zero has undefined behavior. + */ + constexpr TimeDelta& + operator/=(Rep div) + { + duration_ /= div; + return *this; + } + + /** + * Perform modulo operation in-place and return the modified result. + * + * ## Notes + * + * The behavior is undefined if the modulus is zero. + */ + constexpr TimeDelta& + operator%=(Rep mod) + { + duration_ %= mod; + return *this; + } + + /** + * \copydoc TimeDelta::operator%=(Rep) + * + * Equivalent to `*this %= other.count()`. + */ + constexpr TimeDelta& + operator%=(const TimeDelta& other) + { + duration_ %= other.duration_; + return *this; + } + + /** Add two TimeDelta objects. */ + friend constexpr TimeDelta + operator+(const TimeDelta& lhs, const TimeDelta& rhs) + { + auto out = lhs; + out += rhs; + return out; + } + + /** Subtract a TimeDelta object from another. */ + friend constexpr TimeDelta + operator-(const TimeDelta& lhs, const TimeDelta& rhs) + { + auto out = lhs; + out -= rhs; + return out; + } + + /** Multiply a TimeDelta object by a scalar. */ + friend constexpr TimeDelta + operator*(const TimeDelta& lhs, Rep rhs) + { + auto out = lhs; + out *= rhs; + return out; + } + + /** \copydoc operator*(const TimeDelta&, Rep) */ + friend constexpr TimeDelta + operator*(Rep lhs, const TimeDelta& rhs) + { + auto out = rhs; + out *= lhs; + return out; + } + + /** + * Divide a TimeDelta object by a scalar, truncating toward zero. + * + * ## Notes + * + * Division by zero has undefined behavior. + */ + friend constexpr TimeDelta + operator/(const TimeDelta& lhs, Rep rhs) + { + auto out = lhs; + out /= rhs; + return out; + } + + /** + * Compute the remainder after division. + * + * ## Notes + * + * The behavior is undefined if the modulus is zero. + */ + friend constexpr TimeDelta + operator%(const TimeDelta& lhs, Rep rhs) + { + auto out = lhs; + out %= rhs; + return out; + } + + /** + * \copydoc operator%(const TimeDelta&, Rep) + * + * Equivalent to `lhs % rhs.count()`. + */ + friend constexpr TimeDelta + operator%(const TimeDelta& lhs, const TimeDelta& rhs) + { + return lhs % rhs.count(); + } + + /** Compare two TimeDelta objects. */ + friend constexpr bool + operator==(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return lhs.duration_ == rhs.duration_; + } + + /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ + friend constexpr bool + operator!=(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return not(lhs == rhs); + } + + /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ + friend constexpr bool + operator<(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return lhs.duration_ < rhs.duration_; + } + + /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ + friend constexpr bool + operator>(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return lhs.duration_ > rhs.duration_; + } + + /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ + friend constexpr bool + operator<=(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return not(lhs > rhs); + } + + /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ + friend constexpr bool + operator>=(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return not(lhs < rhs); + } + +private: + std::chrono::duration duration_ = {}; +}; + +/** Return the absolute value of the input TimeDelta. */ +constexpr TimeDelta +abs(const TimeDelta& dt) +{ + using Duration = std::chrono::duration; + const auto d = Duration(dt); + return TimeDelta(std::chrono::abs(d)); +} + +template +constexpr TimeDelta::TimeDelta( + const std::chrono::duration& d) + : duration_([=]() { + static_assert(is_arithmetic); + using ToDuration = std::chrono::duration; + + if constexpr (std::is_floating_point_v) { + // Convert from input tick period to floating-point picoseconds. + using PicosecsFP = std::chrono::duration; + const auto p = std::chrono::duration_cast(d); + + // Cast floating-point to int128. + const auto r = static_cast(p.count()); + return ToDuration(r); + } else { + // For integral durations, we can just use duration_cast. + return std::chrono::duration_cast(d); + } + }()) +{} + +template +constexpr TimeDelta::operator std::chrono::duration() const +{ + static_assert(is_arithmetic); + using ToDuration = std::chrono::duration; + + if constexpr (std::is_floating_point_v) { + // Cast duration to floating-point picoseconds. + const auto r = static_cast(count()); + const auto p = std::chrono::duration(r); + + // Convert to output tick period. + return std::chrono::duration_cast(p); + } else { + // For integral durations, we can just use duration_cast. + return std::chrono::duration_cast(duration_); + } +} + +} // namespace caesar diff --git a/datetime/v1/cmake/CaesarParseFile.cmake b/datetime/v1/cmake/CaesarParseFile.cmake new file mode 100644 index 0000000..8beb6d3 --- /dev/null +++ b/datetime/v1/cmake/CaesarParseFile.cmake @@ -0,0 +1,28 @@ +include_guard() + +#[[ +Parse the contents of the file specified by and store them in +. + +The input file is treated as a dependency of the CMake project. Modifications +to this file will trigger re-configuration in the next build. + +If the input is a relative path, it is treated with respect to the value of +*CMAKE_CURRENT_SOURCE_DIR*. + +`` +caesar_parse_file( ) +`` +#]] +function(caesar_parse_file filename result) + # Read file contents to variable. + file(STRINGS ${filename} contents) + + # Add *filename* as a dependency of the CMake build. CMake inspects the + # timestamp of the file and will re-configure if the file has changed since + # the last cmake run. + configure_file(${filename} ${filename} COPYONLY) + + # Set output variable. + set(${result} "${contents}" PARENT_SCOPE) +endfunction() diff --git a/datetime/v1/sources.txt b/datetime/v1/sources.txt new file mode 100644 index 0000000..a348b63 --- /dev/null +++ b/datetime/v1/sources.txt @@ -0,0 +1 @@ +caesar/datetime/timedelta.cpp diff --git a/datetime/v1/test/datetime/timedelta_test.cpp b/datetime/v1/test/datetime/timedelta_test.cpp new file mode 100644 index 0000000..d8d0a3a --- /dev/null +++ b/datetime/v1/test/datetime/timedelta_test.cpp @@ -0,0 +1,524 @@ +#include + +#include + +namespace cs = caesar; + +struct TimeDeltaTest { + template + using Days = std::chrono::duration>; + + template + using Hours = std::chrono::duration>; + + template + using Minutes = std::chrono::duration>; + + template + using Seconds = std::chrono::duration; + + template + using Milliseconds = std::chrono::duration; + + template + using Microseconds = std::chrono::duration; + + template + using Nanoseconds = std::chrono::duration; + + template + using Picoseconds = std::chrono::duration; + + template + using Femtoseconds = std::chrono::duration; + + // Approximately one trillion years, in seconds + constexpr static double trillion_years_sec = 1e12 * 365 * 24 * 60 * 60; + + // TimeDelta representing one picosecond + constexpr static auto one_picosec = cs::TimeDelta::picoseconds(1); +}; + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.default_construct") +{ + const auto dt = cs::TimeDelta(); + CHECK_EQ(dt.count(), 0); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.from_chrono_duration") +{ + SUBCASE("integer_no_truncation") + { + const auto us = Microseconds(123456); + const auto dt = cs::TimeDelta(us); + + CHECK_EQ(dt.count(), us.count() * 1'000'000); + } + + SUBCASE("integer_with_truncation") + { + const auto fs = Femtoseconds(999); + + const auto dt1 = cs::TimeDelta(fs); + CHECK_EQ(dt1.count(), 0); + + const auto dt2 = cs::TimeDelta(-fs); + CHECK_EQ(dt2.count(), 0); + } + + SUBCASE("floating_point") + { + const auto s = Seconds(123.456); + const auto dt = cs::TimeDelta(s); + CHECK_EQ(dt.total_seconds(), doctest::Approx(s.count())); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.to_chrono_duration") +{ + SUBCASE("integer") + { + const auto us = Microseconds(123456); + const auto dt = cs::TimeDelta(us); + CHECK_EQ(Microseconds(dt), us); + } + + SUBCASE("floating_point") + { + const auto s = Seconds(123.456); + const auto dt = cs::TimeDelta(s); + CHECK_EQ(Seconds(dt).count(), doctest::Approx(s.count())); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.min") +{ + const auto dt = cs::TimeDelta::min(); + CHECK_LT(dt.total_seconds(), -trillion_years_sec); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.max") +{ + const auto dt = cs::TimeDelta::max(); + CHECK_GT(dt.total_seconds(), trillion_years_sec); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.resolution") +{ + const auto dt = cs::TimeDelta::resolution(); + CHECK_EQ(dt, one_picosec); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.days") +{ + SUBCASE("integer") + { + const auto d = Days(123); + const auto dt1 = cs::TimeDelta(d); + const auto dt2 = cs::TimeDelta::days(123); + + CHECK_EQ(dt1, dt2); + } + + SUBCASE("floating_point") + { + const auto d = Days(123.456); + const auto dt1 = cs::TimeDelta(d); + const auto dt2 = cs::TimeDelta::days(123.456); + + CHECK_EQ(dt2.total_seconds(), doctest::Approx(dt1.total_seconds())); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.hours") +{ + SUBCASE("integer") + { + const auto h = Hours(123); + const auto dt1 = cs::TimeDelta(h); + const auto dt2 = cs::TimeDelta::hours(123); + + CHECK_EQ(dt1, dt2); + } + + SUBCASE("floating_point") + { + const auto h = Hours(123.456); + const auto dt1 = cs::TimeDelta(h); + const auto dt2 = cs::TimeDelta::hours(123.456); + + CHECK_EQ(dt2.total_seconds(), doctest::Approx(dt1.total_seconds())); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.minutes") +{ + SUBCASE("integer") + { + const auto m = Minutes(123); + const auto dt1 = cs::TimeDelta(m); + const auto dt2 = cs::TimeDelta::minutes(123); + + CHECK_EQ(dt1, dt2); + } + + SUBCASE("floating_point") + { + const auto m = Minutes(123.456); + const auto dt1 = cs::TimeDelta(m); + const auto dt2 = cs::TimeDelta::minutes(123.456); + + CHECK_EQ(dt2.total_seconds(), doctest::Approx(dt1.total_seconds())); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.seconds") +{ + SUBCASE("integer") + { + const auto s = Seconds(123); + const auto dt1 = cs::TimeDelta(s); + const auto dt2 = cs::TimeDelta::seconds(123); + + CHECK_EQ(dt1, dt2); + } + + SUBCASE("floating_point") + { + const auto s = Seconds(123.456); + const auto dt1 = cs::TimeDelta(s); + const auto dt2 = cs::TimeDelta::seconds(123.456); + + CHECK_EQ(dt2.total_seconds(), doctest::Approx(dt1.total_seconds())); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.milliseconds") +{ + SUBCASE("integer") + { + const auto ms = Milliseconds(123); + const auto dt1 = cs::TimeDelta(ms); + const auto dt2 = cs::TimeDelta::milliseconds(123); + + CHECK_EQ(dt1, dt2); + } + + SUBCASE("floating_point") + { + const auto ms = Milliseconds(123.456); + const auto dt1 = cs::TimeDelta(ms); + const auto dt2 = cs::TimeDelta::milliseconds(123.456); + + CHECK_EQ(dt2.total_seconds(), doctest::Approx(dt1.total_seconds())); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.microseconds") +{ + SUBCASE("integer") + { + const auto us = Microseconds(123); + const auto dt1 = cs::TimeDelta(us); + const auto dt2 = cs::TimeDelta::microseconds(123); + + CHECK_EQ(dt1, dt2); + } + + SUBCASE("floating_point") + { + const auto us = Microseconds(123.456); + const auto dt1 = cs::TimeDelta(us); + const auto dt2 = cs::TimeDelta::microseconds(123.456); + + CHECK_EQ(dt2.total_seconds(), doctest::Approx(dt1.total_seconds())); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.nanoseconds") +{ + SUBCASE("integer") + { + const auto ns = Nanoseconds(123); + const auto dt1 = cs::TimeDelta(ns); + const auto dt2 = cs::TimeDelta::nanoseconds(123); + + CHECK_EQ(dt1, dt2); + } + + SUBCASE("floating_point") + { + const auto ns = Nanoseconds(123.456); + const auto dt1 = cs::TimeDelta(ns); + const auto dt2 = cs::TimeDelta::nanoseconds(123.456); + + CHECK_EQ(dt2.total_seconds(), doctest::Approx(dt1.total_seconds())); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.picoseconds") +{ + SUBCASE("integer") + { + const auto ps = Picoseconds(123); + const auto dt1 = cs::TimeDelta(ps); + const auto dt2 = cs::TimeDelta::picoseconds(123); + + CHECK_EQ(dt1, dt2); + } + + SUBCASE("floating_point") + { + const auto dt = cs::TimeDelta::picoseconds(123.456); + CHECK_EQ(dt.count(), 123); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.unary_plus") +{ + const auto ms = Milliseconds(123456); + + const auto dt1 = cs::TimeDelta(ms); + const auto dt2 = cs::TimeDelta(-ms); + + CHECK_EQ(+dt1, dt1); + CHECK_EQ(+dt2, dt2); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.unary_minus") +{ + const auto ms = Milliseconds(123456); + + const auto dt1 = cs::TimeDelta(ms); + const auto dt2 = cs::TimeDelta(-ms); + + CHECK_EQ(-dt1, dt2); + CHECK_EQ(-dt2, dt1); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.increment") +{ + cs::TimeDelta dt; + + SUBCASE("prefix") + { + CHECK_EQ(++dt, one_picosec); + CHECK_EQ(dt, one_picosec); + } + + SUBCASE("postfix") + { + CHECK_EQ(dt++, cs::TimeDelta()); + CHECK_EQ(dt, one_picosec); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.decrement") +{ + cs::TimeDelta dt; + + SUBCASE("prefix") + { + CHECK_EQ(--dt, -one_picosec); + CHECK_EQ(dt, -one_picosec); + } + + SUBCASE("postfix") + { + CHECK_EQ(dt--, cs::TimeDelta()); + CHECK_EQ(dt, -one_picosec); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.add") +{ + const auto a = Milliseconds(123456789); + const auto b = Days(12) + Minutes(34) + + Seconds(56) + Microseconds(78) + + Picoseconds(90); + + const auto dt1 = cs::TimeDelta(a); + const auto dt2 = cs::TimeDelta(b); + const auto sum = cs::TimeDelta(a + b); + + // Compound assignment + auto tmp = dt1; + tmp += dt2; + CHECK_EQ(tmp, sum); + + // Binary operator is commutative. + CHECK_EQ(dt1 + dt2, sum); + CHECK_EQ(dt2 + dt1, sum); + + // Adding zero returns identity. + CHECK_EQ(dt1 + cs::TimeDelta(), dt1); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.subtract") +{ + const auto a = Milliseconds(123456789); + const auto b = Days(12) + Minutes(34) + + Seconds(56) + Microseconds(78) + + Picoseconds(90); + + const auto dt1 = cs::TimeDelta(a); + const auto dt2 = cs::TimeDelta(b); + const auto diff = cs::TimeDelta(b - a); + + // Compound assignment + auto tmp = dt2; + tmp -= dt1; + CHECK_EQ(tmp, diff); + + // Binary operator + CHECK_EQ(dt2 - dt1, diff); + + // Adding zero returns identity. + CHECK_EQ(dt1 - cs::TimeDelta(), dt1); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.multiply") +{ + const auto a = Days(12) + Minutes(34) + + Seconds(56) + Microseconds(78) + + Picoseconds(90); + + const auto dt = cs::TimeDelta(a); + const int k = 4; + const auto prod = cs::TimeDelta(k * a); + + // Compound assignment + auto tmp = dt; + tmp *= k; + CHECK_EQ(tmp, prod); + + // Binary operator is commutative. + CHECK_EQ(k * dt, prod); + CHECK_EQ(dt * k, prod); + + // Multiplying by one returns identity. + CHECK_EQ(1 * dt, dt); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.divide") +{ + const auto a = Days(12) + Minutes(34) + + Seconds(56) + Microseconds(78) + + Picoseconds(90); + + const int k = 3; + const auto dt = cs::TimeDelta(k * a); + const auto quotient = cs::TimeDelta(a); + + // Compound assignment + auto tmp = dt; + tmp /= k; + CHECK_EQ(tmp, quotient); + + // Binary operator + CHECK_EQ(dt / k, quotient); + + // Dividing by one returns identity. + CHECK_EQ(dt / 1, dt); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.modulo") +{ + const auto a = Milliseconds(123456789); + const auto b = Days(12) + Minutes(34) + + Seconds(56) + Microseconds(78) + + Picoseconds(90); + + const auto dt1 = cs::TimeDelta(a); + const auto dt2 = cs::TimeDelta(b); + const auto remainder = cs::TimeDelta(b % a); + + // Compound assignment + auto tmp1 = dt2; + tmp1 %= dt1; + CHECK_EQ(tmp1, remainder); + + auto tmp2 = dt2; + tmp2 %= dt1.count(); + CHECK_EQ(tmp2, remainder); + + // Binary operator + CHECK_EQ(dt2 % dt1, remainder); + CHECK_EQ(dt2 % dt1.count(), remainder); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.compare_eq") +{ + const auto dt1 = cs::TimeDelta(); + const auto dt2 = cs::TimeDelta(); + const auto dt3 = one_picosec; + + CHECK(dt1 == dt1); + CHECK(dt1 == dt2); + CHECK_FALSE(dt1 == dt3); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.compare_ne") +{ + const auto dt1 = cs::TimeDelta(); + const auto dt2 = one_picosec; + const auto dt3 = one_picosec; + + CHECK(dt1 != dt2); + CHECK_FALSE(dt2 != dt3); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.compare_lt") +{ + const auto dt1 = cs::TimeDelta(); + const auto dt2 = -one_picosec; + const auto dt3 = one_picosec; + + CHECK(dt2 < dt1); + CHECK(dt1 < dt3); + CHECK_FALSE(dt3 < dt2); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.compare_gt") +{ + const auto dt1 = cs::TimeDelta(); + const auto dt2 = -one_picosec; + const auto dt3 = one_picosec; + + CHECK(dt1 > dt2); + CHECK(dt3 > dt1); + CHECK_FALSE(dt2 > dt3); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.compare_le") +{ + const auto dt1 = cs::TimeDelta(); + const auto dt2 = cs::TimeDelta(); + const auto dt3 = one_picosec; + + CHECK(dt1 <= dt2); + CHECK(dt1 <= dt3); + CHECK_FALSE(dt3 <= dt2); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.compare_ge") +{ + const auto dt1 = cs::TimeDelta(); + const auto dt2 = one_picosec; + const auto dt3 = one_picosec; + + CHECK(dt2 >= dt1); + CHECK(dt3 >= dt2); + CHECK_FALSE(dt1 >= dt3); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.abs") +{ + const auto ms = Milliseconds(123456); + + const auto dt1 = cs::TimeDelta(ms); + const auto dt2 = cs::TimeDelta(-ms); + + CHECK_EQ(cs::abs(dt1), dt1); + CHECK_EQ(cs::abs(dt2), dt1); +} diff --git a/datetime/v1/test/main.cpp b/datetime/v1/test/main.cpp new file mode 100644 index 0000000..0a3f254 --- /dev/null +++ b/datetime/v1/test/main.cpp @@ -0,0 +1,2 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include diff --git a/datetime/v1/tests.txt b/datetime/v1/tests.txt new file mode 100644 index 0000000..7cb088a --- /dev/null +++ b/datetime/v1/tests.txt @@ -0,0 +1,2 @@ +test/datetime/timedelta_test.cpp +test/main.cpp diff --git a/datetime/v1/warnings.txt b/datetime/v1/warnings.txt new file mode 100644 index 0000000..86f1906 --- /dev/null +++ b/datetime/v1/warnings.txt @@ -0,0 +1,45 @@ +-Wall +-Walloc-zero +-Wcast-align +-Wcast-qual +-Wconversion +-Wcuda-compat +-Wdeprecated +-Wdouble-promotion +-Wduplicated-branches +-Wduplicated-cond +-Wextra +-Wformat=2 +-Wimplicit-fallthrough +-Winconsistent-missing-destructor-override +-Wlogical-op +-Wloop-analysis +-Wmisleading-indentation +-Wmissing-noreturn +-Wnon-virtual-dtor +-Wnull-dereference +-Wold-style-cast +-Woverloaded-virtual +-Wpacked +-Wpedantic +-Wredundant-parens +-Wshadow +-Wsign-conversion +-Wstrict-aliasing +-Wstrict-overflow +-Wtautological-compare +-Wthread-safety +-Wundef +-Wundefined-func-template +-Wundefined-reinterpret-cast +-Wuninitialized +-Wunknown-pragmas +-Wunneeded-internal-declaration +-Wunreachable-code-aggressive +-Wunused +-Wunused-member-function +-Wvector-conversion +-Wvla +-Wweak-template-vtables +-Wweak-vtables +-Wzero-as-null-pointer-constant From 5c0064d9a6df81edbac8ebfe6fe809148aa8c31d Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Thu, 25 Feb 2021 23:36:18 -0800 Subject: [PATCH 2/8] Add GPSTime --- datetime/v1/caesar/datetime.hpp | 1 + datetime/v1/caesar/datetime/gpstime.cpp | 77 +++++++ datetime/v1/caesar/datetime/gpstime.hpp | 290 ++++++++++++++++++++++++ datetime/v1/sources.txt | 1 + 4 files changed, 369 insertions(+) create mode 100644 datetime/v1/caesar/datetime/gpstime.cpp create mode 100644 datetime/v1/caesar/datetime/gpstime.hpp diff --git a/datetime/v1/caesar/datetime.hpp b/datetime/v1/caesar/datetime.hpp index 6259c65..3900aa8 100644 --- a/datetime/v1/caesar/datetime.hpp +++ b/datetime/v1/caesar/datetime.hpp @@ -1,3 +1,4 @@ #pragma once +#include "datetime/gpstime.hpp" #include "datetime/timedelta.hpp" diff --git a/datetime/v1/caesar/datetime/gpstime.cpp b/datetime/v1/caesar/datetime/gpstime.cpp new file mode 100644 index 0000000..86750fd --- /dev/null +++ b/datetime/v1/caesar/datetime/gpstime.cpp @@ -0,0 +1,77 @@ +#include "gpstime.hpp" + +#include +#include +#include +#include + +namespace caesar { + +GPSTime::GPSTime(std::string_view datetime_string) + : GPSTime([=]() { + const auto pattern = std::regex( + R"((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(\.(\d{1,12}))?)"); + + std::match_results match; + if (not std::regex_match(datetime_string.data(), pattern)) { + throw std::invalid_argument("bad datetime string"); + } + + // Parse date components. + int y = 0, mo = 0, d = 0; + std::sscanf(match[1].data(), "%4d-%2d-%2d", &y, &mo, &d); + + // Parse hour, minute, and second components. + int h = 0, mi = 0, s = 0; + std::sscanf(match[2].first, "%2d:%2d:%2d", &h, &mi, &s); + + // Append zeros to the input string up to the specified length. + const auto zeropad = [](const auto& str, size_t length) { + std::string out = str; + out.resize(length, '0'); + return out; + }; + + // Parse sub-seconds components. + int us = 0, ps = 0; + if (match[4].length() > 0) { + const std::string subseconds_string = zeropad(match[4], 12); + std::sscanf(subseconds_string.data(), "%6d%6d", &us, &ps); + } + + return GPSTime(y, mo, d, h, mi, s, us, ps); + }()) +{} + +GPSTime::operator std::string() const +{ + // Format datetime to string, excluding sub-second components. + const std::string ymdhms_string = + fmt::format("{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}", year(), + month(), day(), hour(), minute(), second()); + + const int us = microsecond(); + const int ps = picosecond(); + + // Early exit if sub-second components are zero. + if (us == 0 and ps == 0) { + return ymdhms_string; + } + + // Format sub-second components to string. + const std::string subseconds_string = fmt::format("{:06d}{:06d}", us, ps); + + // Strip trailing zeros from string. + const auto trim = [](const auto& str) { + const auto n = str.find_last_not_of('0'); + if (n == std::string::npos) { + return str; + } else { + return str.substr(0, n + 1); + } + }; + + return fmt::format("{}.{}", ymdhms_string, trim(subseconds_string)); +} + +} // namespace caesar diff --git a/datetime/v1/caesar/datetime/gpstime.hpp b/datetime/v1/caesar/datetime/gpstime.hpp new file mode 100644 index 0000000..cff8b93 --- /dev/null +++ b/datetime/v1/caesar/datetime/gpstime.hpp @@ -0,0 +1,290 @@ +#pragma once + +#include "timedelta.hpp" + +#include +#include +#include +#include +#include +#include + +namespace caesar { + +/** + * A date/time point in Global Positioning System (GPS) time with picosecond + * resolution + * + * GPS time is an atomic time scale implemented by GPS satellites and ground + * stations. Unlike UTC time, GPS time is a continuous time scale. Leap seconds + * are not inserted. Therefore, the offset between GPS and UTC time is not fixed + * but rather changes each time a leap second adjustment is made to UTC. + * + * The date components follow the proleptic Gregorian calendar, which allows the + * representation of dates prior to the calendar's introduction in 1582. + * XXX Should dates before year 0000 or after year 9999 be considered valid??? + * + * Internally, GPSTime represents time as a 128-bit integer count of picoseconds + * relative to some reference epoch. + * + * \see UTCTime + * \see TimeDelta + */ +class GPSTime { +public: + /** + * A type representing the GPS time system which meets the named + * requirements of + * [Clock](https://en.cppreference.com/w/cpp/named_req/Clock). + */ + using Clock = date::gps_clock; + + /** \see TimeDelta::Rep */ + using Rep = TimeDelta::Rep; + + /** \see TimeDelta::Period */ + using Period = TimeDelta::Period; + + /** ... */ + using Duration = std::chrono::duration; + + /** + * Construct a new GPSTime object. + * + * \param[in] year year component + * \param[in] month month component, encoded 1 through 12 + * \param[in] day day component, encoded 1 through 31 + * \param[in] hour hour component, in the range [0, 24) + * \param[in] minute minute component, in the range [0, 60) + * \param[in] second second component, in the range [0, 60) + * \param[in] microsecond microsecond component, in the range [0, 1000000) + * \param[in] picosecond picosecond component, in the range [0, 1000000) + */ + constexpr GPSTime(int year, + int month, + int day, + int hour, + int minute, + int second, + int microsecond = 0, + int picosecond = 0); + + /** Construct a new GPSTime object from a std::chrono::time_point. */ + explicit constexpr GPSTime( + const std::chrono::time_point& time_point) noexcept + : time_point_(time_point) + {} + + /** Construct a new GPSTime object from a string representation. */ + explicit GPSTime(std::string_view datetime_string); + + /** Convert to std::chrono::time_point. */ + explicit operator std::chrono::time_point() const + { + return time_point_; + } + + /** Return a string representation of the GPS time. */ + explicit operator std::string() const; + + /** Return the earliest valid GPSTime. */ + static GPSTime + min() noexcept + { + return GPSTime(1, 1, 1, 0, 0, 0); + } + + /** Return the latest valid GPSTime. */ + static GPSTime + max() noexcept + { + return GPSTime(9999, 12, 31, 23, 59, 59, 999'999, 999'999); + } + + /** + * Return the smallest possible difference between non-equal GPSTime + * objects. + */ + static constexpr TimeDelta + resolution() noexcept + { + return TimeDelta::resolution(); + } + + /** Return the current time in GPS time. */ + static GPSTime + now() + { + return GPSTime(Clock::now()); + } + + /** Return the year component. */ + int + year() const; + + /** Return the month component, encoded 1 through 12. */ + int + month() const; + + /** Return the day component, encoded 1 through 31. */ + int + day() const; + + /** Return the day of the week. */ + date::weekday + weekday() const; + + /** Return the hour component. */ + int + hour() const; + + /** Return the minute component. */ + int + minute() const; + + /** Return the second component. */ + int + second() const; + + /** Return the microsecond component. */ + int + microsecond() const; + + /** Return the picosecond component. */ + int + picosecond() const; + + /** Increment the tick count. */ + constexpr GPSTime& + operator++() + { + ++time_point_; + return *this; + } + + /** \copydoc GPSTime::operator++() */ + constexpr GPSTime + operator++(int) + { + return GPSTime(time_point_++); + } + + /** Decrement the tick count. */ + constexpr GPSTime& + operator--() + { + --time_point_; + return *this; + } + + /** \copydoc GPSTime::operator--() */ + constexpr GPSTime + operator--(int) + { + return GPSTime(time_point_--); + } + + /** Perform addition in-place and return the modified result. */ + constexpr GPSTime& + operator+=(const TimeDelta& dt) + { + time_point_ += Duration(dt); + return *this; + } + + /** Perform subtraction in-place and return the modified result. */ + constexpr GPSTime& + operator-=(const TimeDelta& dt) + { + time_point_ -= Duration(dt); + return *this; + } + + /** Add a TimeDelta to a GPSTime. */ + friend constexpr GPSTime + operator+(const GPSTime& lhs, const TimeDelta& rhs) + { + auto out = lhs; + out += rhs; + return out; + } + + /** \copydoc operator+(const GPSTime&, const TimeDelta&) */ + friend constexpr GPSTime + operator+(const TimeDelta& lhs, const GPSTime& rhs) + { + auto out = rhs; + out += lhs; + return out; + } + + /** Subtract a TimeDelta from a GPSTime. */ + friend constexpr GPSTime + operator-(const GPSTime& lhs, const TimeDelta& rhs) + { + auto out = lhs; + out -= rhs; + return out; + } + + /** Compute the difference between two GPSTime objects. */ + friend constexpr TimeDelta + operator-(const GPSTime& lhs, const GPSTime& rhs) + { + return TimeDelta(lhs.time_point_ - rhs.time_point_); + } + + /** Compare two GPSTime objects. */ + friend constexpr bool + operator==(const GPSTime& lhs, const GPSTime& rhs) noexcept + { + return lhs.time_point_ == rhs.time_point_; + } + + /** \copydoc operator==(const GPSTime&, const GPSTime&) */ + friend constexpr bool + operator!=(const GPSTime& lhs, const GPSTime& rhs) noexcept + { + return not(lhs == rhs); + } + + /** \copydoc operator==(const GPSTime&, const GPSTime&) */ + friend constexpr bool + operator<(const GPSTime& lhs, const GPSTime& rhs) noexcept + { + return lhs.time_point_ < rhs.time_point_; + } + + /** \copydoc operator==(const GPSTime&, const GPSTime&) */ + friend constexpr bool + operator>(const GPSTime& lhs, const GPSTime& rhs) noexcept + { + return lhs.time_point_ > rhs.time_point_; + } + + /** \copydoc operator==(const GPSTime&, const GPSTime&) */ + friend constexpr bool + operator<=(const GPSTime& lhs, const GPSTime& rhs) noexcept + { + return not(lhs > rhs); + } + + /** \copydoc operator==(const GPSTime&, const GPSTime&) */ + friend constexpr bool + operator>=(const GPSTime& lhs, const GPSTime& rhs) noexcept + { + return not(lhs < rhs); + } + + /** Serialize a string representation to an output stream. */ + friend std::ostream& + operator<<(std::ostream& os, const GPSTime& ts) + { + return os << std::string(ts); + } + +private: + std::chrono::time_point time_point_; +}; + +} // namespace caesar diff --git a/datetime/v1/sources.txt b/datetime/v1/sources.txt index a348b63..5f6459a 100644 --- a/datetime/v1/sources.txt +++ b/datetime/v1/sources.txt @@ -1 +1,2 @@ +caesar/datetime/gpstime.cpp caesar/datetime/timedelta.cpp From 91f39ff86f1f1126eedc1455bdebfa242d45844d Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Fri, 5 Mar 2021 21:32:11 -0800 Subject: [PATCH 3/8] Add TimeDelta rounding functions --- datetime/v1/caesar/datetime/timedelta.hpp | 104 ++++++++++++++- datetime/v1/test/datetime/timedelta_test.cpp | 125 +++++++++++++++++-- 2 files changed, 213 insertions(+), 16 deletions(-) diff --git a/datetime/v1/caesar/datetime/timedelta.hpp b/datetime/v1/caesar/datetime/timedelta.hpp index 07518f1..57e000b 100644 --- a/datetime/v1/caesar/datetime/timedelta.hpp +++ b/datetime/v1/caesar/datetime/timedelta.hpp @@ -13,11 +13,11 @@ namespace caesar { * picosecond resolution * * Internally, TimeDelta stores a 128-bit integer tick count of picoseconds, - * allowing it to represent spans of trillions of years without loss of - * precision. + * allowing it to represent an extremely wide range of values (up to several + * quintillion years!) without loss of precision. * * TimeDelta objects can be constructed from or converted to instances of - * std::chrono::duration. In addition, the factory functions `days()`, + * std::chrono::duration. In addition, the static member functions `days()`, * `hours()`, `minutes()`, `seconds()`, `milliseconds()`, `microseconds()`, * `nanoseconds()`, and `picoseconds()` can be used to create TimeDelta objects. * @@ -55,7 +55,7 @@ class TimeDelta { * Construct a new TimeDelta object from a std::chrono::duration. * * If the input has sub-picosecond resolution, it will be truncated to an - * integer multiple of `TimeDelta::resolution`. + * integer multiple of `TimeDelta::resolution()`. * * ## Notes * @@ -533,6 +533,102 @@ abs(const TimeDelta& dt) return TimeDelta(std::chrono::abs(d)); } +/** + * Truncate to a multiple of the specified period. + * + * Returns the nearest integer multiple of period not greater in magnitude than + * dt. + * + * ## Notes + * + * The behavior is undefined if period is zero. + * + * \see floor(const TimeDelta&, const TimeDelta&) + * \see ceil(const TimeDelta&, const TimeDelta&) + * \see round(const TimeDelta&, const TimeDelta&) + */ +constexpr TimeDelta +trunc(const TimeDelta& dt, const TimeDelta& period) +{ + return dt - (dt % period); +} + +/** + * Round down to a multiple of the specified period. + * + * Returns the nearest integer multiple of period that is less than or equal to + * dt. + * + * ## Notes + * + * The behavior is undefined if period is zero. + * + * \see trunc(const TimeDelta&, const TimeDelta&) + * \see ceil(const TimeDelta&, const TimeDelta&) + * \see round(const TimeDelta&, const TimeDelta&) + */ +constexpr TimeDelta +floor(const TimeDelta& dt, const TimeDelta& period) +{ + const auto t = trunc(dt, period); + return t <= dt ? t : t - abs(period); +} + +/** + * Round up to a multiple of the specified period. + * + * Returns the nearest integer multiple of period that is greater than or equal + * to dt. + * + * ## Notes + * + * The behavior is undefined if period is zero. + * + * \see trunc(const TimeDelta&, const TimeDelta&) + * \see floor(const TimeDelta&, const TimeDelta&) + * \see round(const TimeDelta&, const TimeDelta&) + */ +constexpr TimeDelta +ceil(const TimeDelta& dt, const TimeDelta& period) +{ + const auto t = trunc(dt, period); + return t >= dt ? t : t + abs(period); +} + +/** + * Round to the nearest multiple of the specified period. + * + * Returns the integer multiple of period that is closest to dt. If there are + * two such values, returns the one that is an even multiple of period. + * + * ## Notes + * + * The behavior is undefined if period is zero. + * + * \see trunc(const TimeDelta&, const TimeDelta&) + * \see floor(const TimeDelta&, const TimeDelta&) + * \see ceil(const TimeDelta&, const TimeDelta&) + */ +constexpr TimeDelta +round(const TimeDelta& dt, const TimeDelta& period) +{ + const auto lower = floor(dt, period); + const auto upper = lower + abs(period); + + const auto lower_diff = dt - lower; + const auto upper_diff = upper - dt; + + if (lower_diff < upper_diff) { + return lower; + } + if (upper_diff < lower_diff) { + return upper; + } + + // In halfway cases, return the value that's an even multiple of period. + return (lower.count() / period.count()) & 1 ? upper : lower; +} + template constexpr TimeDelta::TimeDelta( const std::chrono::duration& d) diff --git a/datetime/v1/test/datetime/timedelta_test.cpp b/datetime/v1/test/datetime/timedelta_test.cpp index d8d0a3a..37445c5 100644 --- a/datetime/v1/test/datetime/timedelta_test.cpp +++ b/datetime/v1/test/datetime/timedelta_test.cpp @@ -32,8 +32,8 @@ struct TimeDeltaTest { template using Femtoseconds = std::chrono::duration; - // Approximately one trillion years, in seconds - constexpr static double trillion_years_sec = 1e12 * 365 * 24 * 60 * 60; + // Approximately the number of seconds in one quintillion years + constexpr static double quintillion_years_sec = 1e18 * 365 * 24 * 60 * 60; // TimeDelta representing one picosecond constexpr static auto one_picosec = cs::TimeDelta::picoseconds(1); @@ -94,13 +94,13 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.to_chrono_duration") TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.min") { const auto dt = cs::TimeDelta::min(); - CHECK_LT(dt.total_seconds(), -trillion_years_sec); + CHECK_LT(dt.total_seconds(), -quintillion_years_sec); } TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.max") { const auto dt = cs::TimeDelta::max(); - CHECK_GT(dt.total_seconds(), trillion_years_sec); + CHECK_GT(dt.total_seconds(), quintillion_years_sec); } TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.resolution") @@ -109,7 +109,7 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.resolution") CHECK_EQ(dt, one_picosec); } -TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.days") +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.days_factory") { SUBCASE("integer") { @@ -130,7 +130,7 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.days") } } -TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.hours") +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.hours_factory") { SUBCASE("integer") { @@ -151,7 +151,7 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.hours") } } -TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.minutes") +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.minutes_factory") { SUBCASE("integer") { @@ -172,7 +172,7 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.minutes") } } -TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.seconds") +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.seconds_factory") { SUBCASE("integer") { @@ -193,7 +193,7 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.seconds") } } -TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.milliseconds") +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.milliseconds_factory") { SUBCASE("integer") { @@ -214,7 +214,7 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.milliseconds") } } -TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.microseconds") +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.microseconds_factory") { SUBCASE("integer") { @@ -235,7 +235,7 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.microseconds") } } -TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.nanoseconds") +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.nanoseconds_factory") { SUBCASE("integer") { @@ -256,7 +256,7 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.nanoseconds") } } -TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.picoseconds") +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.picoseconds_factory") { SUBCASE("integer") { @@ -522,3 +522,104 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.abs") CHECK_EQ(cs::abs(dt1), dt1); CHECK_EQ(cs::abs(dt2), dt1); } + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.trunc") +{ + const auto s = cs::TimeDelta::seconds(1); + const auto ms = cs::TimeDelta::milliseconds(1); + + SUBCASE("positive") + { + const auto dt1 = 2 * s + 500 * ms; + const auto dt2 = cs::trunc(dt1, s); + + CHECK_EQ(dt2, 2 * s); + } + + SUBCASE("negative") + { + const auto dt1 = -2 * s - 500 * ms; + const auto dt2 = cs::trunc(dt1, s); + + CHECK_EQ(dt2, -2 * s); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.floor") +{ + const auto s = cs::TimeDelta::seconds(1); + const auto ms = cs::TimeDelta::milliseconds(1); + + SUBCASE("positive") + { + const auto dt1 = 2 * s + 500 * ms; + const auto dt2 = cs::floor(dt1, s); + + CHECK_EQ(dt2, 2 * s); + } + + SUBCASE("negative") + { + const auto dt1 = -2 * s - 500 * ms; + const auto dt2 = cs::floor(dt1, s); + + CHECK_EQ(dt2, -3 * s); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.ceil") +{ + const auto s = cs::TimeDelta::seconds(1); + const auto ms = cs::TimeDelta::milliseconds(1); + + SUBCASE("positive") + { + const auto dt1 = 2 * s + 500 * ms; + const auto dt2 = cs::ceil(dt1, s); + + CHECK_EQ(dt2, 3 * s); + } + + SUBCASE("negative") + { + const auto dt1 = -2 * s - 500 * ms; + const auto dt2 = cs::ceil(dt1, s); + + CHECK_EQ(dt2, -2 * s); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.round") +{ + const auto s = cs::TimeDelta::seconds(1); + const auto ms = cs::TimeDelta::milliseconds(1); + + SUBCASE("round_down") + { + const auto dt1 = 2 * s + 499 * ms; + const auto dt2 = cs::round(dt1, s); + + CHECK_EQ(dt2, 2 * s); + } + + SUBCASE("round_up") + { + const auto dt1 = 2 * s + 501 * ms; + const auto dt2 = cs::round(dt1, s); + + CHECK_EQ(dt2, 3 * s); + } + + SUBCASE("round_even") + { + const auto dt1 = 2 * s + 500 * ms; + const auto dt2 = cs::round(dt1, s); + + CHECK_EQ(dt2, 2 * s); + + const auto dt3 = 3 * s + 500 * ms; + const auto dt4 = cs::round(dt3, s); + + CHECK_EQ(dt4, 4 * s); + } +} From cbfbb4f4446bed3acabc82df2ff4820112c22a95 Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Fri, 5 Mar 2021 21:40:32 -0800 Subject: [PATCH 4/8] Liberally sprinkle [[nodiscard]] --- datetime/v1/caesar/datetime/timedelta.hpp | 69 ++++++++++++----------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/datetime/v1/caesar/datetime/timedelta.hpp b/datetime/v1/caesar/datetime/timedelta.hpp index 57e000b..f8eabe6 100644 --- a/datetime/v1/caesar/datetime/timedelta.hpp +++ b/datetime/v1/caesar/datetime/timedelta.hpp @@ -79,17 +79,18 @@ class TimeDelta { * Casting is subject to truncation as with any `static_cast` to `ToRep`. */ template - explicit constexpr operator std::chrono::duration() const; + [[nodiscard]] explicit constexpr + operator std::chrono::duration() const; /** Return the smallest representable TimeDelta. */ - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta min() noexcept { return TimeDelta(std::chrono::duration::min()); } /** Return the largest representable TimeDelta. */ - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta max() noexcept { return TimeDelta(std::chrono::duration::max()); @@ -99,7 +100,7 @@ class TimeDelta { * Return the smallest possible difference between non-equal TimeDelta * objects. */ - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta resolution() noexcept { return TimeDelta(std::chrono::duration(1)); @@ -118,7 +119,7 @@ class TimeDelta { * `static_cast` to `TimeDelta::Rep`. */ template - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta days(T d) { static_assert(is_arithmetic); @@ -139,7 +140,7 @@ class TimeDelta { * `static_cast` to `TimeDelta::Rep`. */ template - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta hours(T h) { static_assert(is_arithmetic); @@ -160,7 +161,7 @@ class TimeDelta { * `static_cast` to `TimeDelta::Rep`. */ template - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta minutes(T m) { static_assert(is_arithmetic); @@ -181,7 +182,7 @@ class TimeDelta { * `static_cast` to `TimeDelta::Rep`. */ template - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta seconds(T s) { static_assert(is_arithmetic); @@ -203,7 +204,7 @@ class TimeDelta { * `static_cast` to `TimeDelta::Rep`. */ template - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta milliseconds(T ms) { static_assert(is_arithmetic); @@ -225,7 +226,7 @@ class TimeDelta { * `static_cast` to `TimeDelta::Rep`. */ template - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta microseconds(T us) { static_assert(is_arithmetic); @@ -247,7 +248,7 @@ class TimeDelta { * `static_cast` to `TimeDelta::Rep`. */ template - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta nanoseconds(T ns) { static_assert(is_arithmetic); @@ -269,7 +270,7 @@ class TimeDelta { * `static_cast` to `TimeDelta::Rep`. */ template - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta picoseconds(T ps) { static_assert(is_arithmetic); @@ -278,14 +279,14 @@ class TimeDelta { } /** Return the tick count. */ - constexpr Rep + [[nodiscard]] constexpr Rep count() const noexcept { return duration_.count(); } /** Return the total number of seconds in the duration. */ - double + [[nodiscard]] double total_seconds() const { using Seconds = std::chrono::duration; @@ -293,14 +294,14 @@ class TimeDelta { } /** Returns a copy of the TimeDelta object. */ - constexpr TimeDelta + [[nodiscard]] constexpr TimeDelta operator+() const noexcept { return *this; } /** Returns the negation of the TimeDelta object. */ - constexpr TimeDelta + [[nodiscard]] constexpr TimeDelta operator-() const { return TimeDelta(-duration_); @@ -402,7 +403,7 @@ class TimeDelta { } /** Add two TimeDelta objects. */ - friend constexpr TimeDelta + [[nodiscard]] friend constexpr TimeDelta operator+(const TimeDelta& lhs, const TimeDelta& rhs) { auto out = lhs; @@ -411,7 +412,7 @@ class TimeDelta { } /** Subtract a TimeDelta object from another. */ - friend constexpr TimeDelta + [[nodiscard]] friend constexpr TimeDelta operator-(const TimeDelta& lhs, const TimeDelta& rhs) { auto out = lhs; @@ -420,7 +421,7 @@ class TimeDelta { } /** Multiply a TimeDelta object by a scalar. */ - friend constexpr TimeDelta + [[nodiscard]] friend constexpr TimeDelta operator*(const TimeDelta& lhs, Rep rhs) { auto out = lhs; @@ -429,7 +430,7 @@ class TimeDelta { } /** \copydoc operator*(const TimeDelta&, Rep) */ - friend constexpr TimeDelta + [[nodiscard]] friend constexpr TimeDelta operator*(Rep lhs, const TimeDelta& rhs) { auto out = rhs; @@ -444,7 +445,7 @@ class TimeDelta { * * Division by zero has undefined behavior. */ - friend constexpr TimeDelta + [[nodiscard]] friend constexpr TimeDelta operator/(const TimeDelta& lhs, Rep rhs) { auto out = lhs; @@ -459,7 +460,7 @@ class TimeDelta { * * The behavior is undefined if the modulus is zero. */ - friend constexpr TimeDelta + [[nodiscard]] friend constexpr TimeDelta operator%(const TimeDelta& lhs, Rep rhs) { auto out = lhs; @@ -472,49 +473,49 @@ class TimeDelta { * * Equivalent to `lhs % rhs.count()`. */ - friend constexpr TimeDelta + [[nodiscard]] friend constexpr TimeDelta operator%(const TimeDelta& lhs, const TimeDelta& rhs) { return lhs % rhs.count(); } /** Compare two TimeDelta objects. */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator==(const TimeDelta& lhs, const TimeDelta& rhs) noexcept { return lhs.duration_ == rhs.duration_; } /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator!=(const TimeDelta& lhs, const TimeDelta& rhs) noexcept { return not(lhs == rhs); } /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator<(const TimeDelta& lhs, const TimeDelta& rhs) noexcept { return lhs.duration_ < rhs.duration_; } /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator>(const TimeDelta& lhs, const TimeDelta& rhs) noexcept { return lhs.duration_ > rhs.duration_; } /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator<=(const TimeDelta& lhs, const TimeDelta& rhs) noexcept { return not(lhs > rhs); } /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator>=(const TimeDelta& lhs, const TimeDelta& rhs) noexcept { return not(lhs < rhs); @@ -525,7 +526,7 @@ class TimeDelta { }; /** Return the absolute value of the input TimeDelta. */ -constexpr TimeDelta +[[nodiscard]] constexpr TimeDelta abs(const TimeDelta& dt) { using Duration = std::chrono::duration; @@ -547,7 +548,7 @@ abs(const TimeDelta& dt) * \see ceil(const TimeDelta&, const TimeDelta&) * \see round(const TimeDelta&, const TimeDelta&) */ -constexpr TimeDelta +[[nodiscard]] constexpr TimeDelta trunc(const TimeDelta& dt, const TimeDelta& period) { return dt - (dt % period); @@ -567,7 +568,7 @@ trunc(const TimeDelta& dt, const TimeDelta& period) * \see ceil(const TimeDelta&, const TimeDelta&) * \see round(const TimeDelta&, const TimeDelta&) */ -constexpr TimeDelta +[[nodiscard]] constexpr TimeDelta floor(const TimeDelta& dt, const TimeDelta& period) { const auto t = trunc(dt, period); @@ -588,7 +589,7 @@ floor(const TimeDelta& dt, const TimeDelta& period) * \see floor(const TimeDelta&, const TimeDelta&) * \see round(const TimeDelta&, const TimeDelta&) */ -constexpr TimeDelta +[[nodiscard]] constexpr TimeDelta ceil(const TimeDelta& dt, const TimeDelta& period) { const auto t = trunc(dt, period); @@ -609,7 +610,7 @@ ceil(const TimeDelta& dt, const TimeDelta& period) * \see floor(const TimeDelta&, const TimeDelta&) * \see ceil(const TimeDelta&, const TimeDelta&) */ -constexpr TimeDelta +[[nodiscard]] constexpr TimeDelta round(const TimeDelta& dt, const TimeDelta& period) { const auto lower = floor(dt, period); From 5445b7cb929accc830314702916b2ab915aed0bf Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Sat, 6 Mar 2021 00:08:37 -0800 Subject: [PATCH 5/8] Add GPSTime constructor from datetime components --- datetime/v1/CMakeLists.txt | 7 +- datetime/v1/caesar/datetime/gpstime.cpp | 132 +++++++------- datetime/v1/caesar/datetime/gpstime.hpp | 199 +++++++++++++++------- datetime/v1/caesar/datetime/timedelta.hpp | 6 + 4 files changed, 213 insertions(+), 131 deletions(-) diff --git a/datetime/v1/CMakeLists.txt b/datetime/v1/CMakeLists.txt index 0505cff..f91e554 100644 --- a/datetime/v1/CMakeLists.txt +++ b/datetime/v1/CMakeLists.txt @@ -18,7 +18,7 @@ option(CAESAR_WERROR "Treat compiler warnings as errors" ON) # Import third-party dependencies. find_package(absl REQUIRED CONFIG COMPONENTS numeric) -#find_package(date REQUIRED CONFIG COMPONENTS date date-tz) +find_package(date REQUIRED CONFIG COMPONENTS date date-tz) find_package(doctest REQUIRED CONFIG) find_package(fmt REQUIRED CONFIG) @@ -45,7 +45,6 @@ add_library(caesar::datetime ALIAS datetime) target_compile_features(datetime PUBLIC cxx_std_17) # Add sources. -include(CaesarParseFile) caesar_parse_file(sources.txt sources) target_sources(datetime PRIVATE ${sources}) @@ -61,8 +60,8 @@ target_link_libraries( datetime PUBLIC absl::numeric -# date::date -# date::date-tz + date::date + date::date-tz PRIVATE fmt::fmt ) diff --git a/datetime/v1/caesar/datetime/gpstime.cpp b/datetime/v1/caesar/datetime/gpstime.cpp index 86750fd..f7d9401 100644 --- a/datetime/v1/caesar/datetime/gpstime.cpp +++ b/datetime/v1/caesar/datetime/gpstime.cpp @@ -7,71 +7,71 @@ namespace caesar { -GPSTime::GPSTime(std::string_view datetime_string) - : GPSTime([=]() { - const auto pattern = std::regex( - R"((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(\.(\d{1,12}))?)"); - - std::match_results match; - if (not std::regex_match(datetime_string.data(), pattern)) { - throw std::invalid_argument("bad datetime string"); - } - - // Parse date components. - int y = 0, mo = 0, d = 0; - std::sscanf(match[1].data(), "%4d-%2d-%2d", &y, &mo, &d); - - // Parse hour, minute, and second components. - int h = 0, mi = 0, s = 0; - std::sscanf(match[2].first, "%2d:%2d:%2d", &h, &mi, &s); - - // Append zeros to the input string up to the specified length. - const auto zeropad = [](const auto& str, size_t length) { - std::string out = str; - out.resize(length, '0'); - return out; - }; - - // Parse sub-seconds components. - int us = 0, ps = 0; - if (match[4].length() > 0) { - const std::string subseconds_string = zeropad(match[4], 12); - std::sscanf(subseconds_string.data(), "%6d%6d", &us, &ps); - } - - return GPSTime(y, mo, d, h, mi, s, us, ps); - }()) -{} - -GPSTime::operator std::string() const -{ - // Format datetime to string, excluding sub-second components. - const std::string ymdhms_string = - fmt::format("{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}", year(), - month(), day(), hour(), minute(), second()); - - const int us = microsecond(); - const int ps = picosecond(); - - // Early exit if sub-second components are zero. - if (us == 0 and ps == 0) { - return ymdhms_string; - } - - // Format sub-second components to string. - const std::string subseconds_string = fmt::format("{:06d}{:06d}", us, ps); - - // Strip trailing zeros from string. - const auto trim = [](const auto& str) { - const auto n = str.find_last_not_of('0'); - if (n == std::string::npos) { - return str; - } else { - return str.substr(0, n + 1); - } - }; - - return fmt::format("{}.{}", ymdhms_string, trim(subseconds_string)); -} +// GPSTime::GPSTime(std::string_view datetime_string) +// : GPSTime([=]() { +// const auto pattern = std::regex( +// R"((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(\.(\d{1,12}))?)"); +// +// std::match_results match; +// if (not std::regex_match(datetime_string.data(), pattern)) { +// throw std::invalid_argument("bad datetime string"); +// } +// +// // Parse date components. +// int y = 0, mo = 0, d = 0; +// std::sscanf(match[1].data(), "%4d-%2d-%2d", &y, &mo, &d); +// +// // Parse hour, minute, and second components. +// int h = 0, mi = 0, s = 0; +// std::sscanf(match[2].first, "%2d:%2d:%2d", &h, &mi, &s); +// +// // Append zeros to the input string up to the specified length. +// const auto zeropad = [](const auto& str, size_t length) { +// std::string out = str; +// out.resize(length, '0'); +// return out; +// }; +// +// // Parse sub-seconds components. +// int us = 0, ps = 0; +// if (match[4].length() > 0) { +// const std::string subseconds_string = zeropad(match[4], 12); +// std::sscanf(subseconds_string.data(), "%6d%6d", &us, &ps); +// } +// +// return GPSTime(y, mo, d, h, mi, s, us, ps); +// }()) +//{} +// +// GPSTime::operator std::string() const +//{ +// // Format datetime to string, excluding sub-second components. +// const std::string ymdhms_string = +// fmt::format("{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}", year(), +// month(), day(), hour(), minute(), second()); +// +// const int us = microsecond(); +// const int ps = picosecond(); +// +// // Early exit if sub-second components are zero. +// if (us == 0 and ps == 0) { +// return ymdhms_string; +// } +// +// // Format sub-second components to string. +// const std::string subseconds_string = fmt::format("{:06d}{:06d}", us, ps); +// +// // Strip trailing zeros from string. +// const auto trim = [](const std::string& str) { +// const auto n = str.find_last_not_of('0'); +// if (n == std::string::npos) { +// return str; +// } else { +// return str.substr(0, n + 1); +// } +// }; +// +// return fmt::format("{}.{}", ymdhms_string, trim(subseconds_string)); +//} } // namespace caesar diff --git a/datetime/v1/caesar/datetime/gpstime.hpp b/datetime/v1/caesar/datetime/gpstime.hpp index cff8b93..7182783 100644 --- a/datetime/v1/caesar/datetime/gpstime.hpp +++ b/datetime/v1/caesar/datetime/gpstime.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -15,14 +16,15 @@ namespace caesar { * A date/time point in Global Positioning System (GPS) time with picosecond * resolution * - * GPS time is an atomic time scale implemented by GPS satellites and ground - * stations. Unlike UTC time, GPS time is a continuous time scale. Leap seconds - * are not inserted. Therefore, the offset between GPS and UTC time is not fixed - * but rather changes each time a leap second adjustment is made to UTC. + * The GPS time system is an atomic time scale implemented by GPS satellites and + * ground stations. Unlike UTC time, GPS time is a continuous time scale -- leap + * seconds are not inserted. Therefore, the offset between GPS and UTC time is + * not fixed but rather changes each time a leap second adjustment is made to + * UTC. * * The date components follow the proleptic Gregorian calendar, which allows the * representation of dates prior to the calendar's introduction in 1582. - * XXX Should dates before year 0000 or after year 9999 be considered valid??? + * XXX Should dates before year 0001 or after year 9999 be considered valid??? * * Internally, GPSTime represents time as a 128-bit integer count of picoseconds * relative to some reference epoch. @@ -53,7 +55,7 @@ class GPSTime { * * \param[in] year year component * \param[in] month month component, encoded 1 through 12 - * \param[in] day day component, encoded 1 through 31 + * \param[in] day day component * \param[in] hour hour component, in the range [0, 24) * \param[in] minute minute component, in the range [0, 60) * \param[in] second second component, in the range [0, 60) @@ -69,6 +71,7 @@ class GPSTime { int microsecond = 0, int picosecond = 0); + // XXX check >= GPSTime::min() and <= GPSTime::max() /** Construct a new GPSTime object from a std::chrono::time_point. */ explicit constexpr GPSTime( const std::chrono::time_point& time_point) noexcept @@ -79,23 +82,24 @@ class GPSTime { explicit GPSTime(std::string_view datetime_string); /** Convert to std::chrono::time_point. */ - explicit operator std::chrono::time_point() const + [[nodiscard]] explicit + operator std::chrono::time_point() const { return time_point_; } /** Return a string representation of the GPS time. */ - explicit operator std::string() const; + [[nodiscard]] explicit operator std::string() const; /** Return the earliest valid GPSTime. */ - static GPSTime + [[nodiscard]] static GPSTime min() noexcept { return GPSTime(1, 1, 1, 0, 0, 0); } /** Return the latest valid GPSTime. */ - static GPSTime + [[nodiscard]] static GPSTime max() noexcept { return GPSTime(9999, 12, 31, 23, 59, 59, 999'999, 999'999); @@ -105,84 +109,84 @@ class GPSTime { * Return the smallest possible difference between non-equal GPSTime * objects. */ - static constexpr TimeDelta + [[nodiscard]] static constexpr TimeDelta resolution() noexcept { return TimeDelta::resolution(); } /** Return the current time in GPS time. */ - static GPSTime + [[nodiscard]] static GPSTime now() { return GPSTime(Clock::now()); } /** Return the year component. */ - int + [[nodiscard]] int year() const; /** Return the month component, encoded 1 through 12. */ - int + [[nodiscard]] int month() const; - /** Return the day component, encoded 1 through 31. */ - int + /** Return the day component. */ + [[nodiscard]] int day() const; /** Return the day of the week. */ - date::weekday + [[nodiscard]] date::weekday weekday() const; /** Return the hour component. */ - int + [[nodiscard]] int hour() const; /** Return the minute component. */ - int + [[nodiscard]] int minute() const; /** Return the second component. */ - int + [[nodiscard]] int second() const; /** Return the microsecond component. */ - int + [[nodiscard]] int microsecond() const; /** Return the picosecond component. */ - int + [[nodiscard]] int picosecond() const; - /** Increment the tick count. */ - constexpr GPSTime& - operator++() - { - ++time_point_; - return *this; - } - - /** \copydoc GPSTime::operator++() */ - constexpr GPSTime - operator++(int) - { - return GPSTime(time_point_++); - } - - /** Decrement the tick count. */ - constexpr GPSTime& - operator--() - { - --time_point_; - return *this; - } - - /** \copydoc GPSTime::operator--() */ - constexpr GPSTime - operator--(int) - { - return GPSTime(time_point_--); - } + ///** Increment the tick count. */ + // constexpr GPSTime& + // operator++() + //{ + // ++time_point_; + // return *this; + //} + + ///** \copydoc GPSTime::operator++() */ + // constexpr GPSTime + // operator++(int) + //{ + // return GPSTime(time_point_++); + //} + + ///** Decrement the tick count. */ + // constexpr GPSTime& + // operator--() + //{ + // --time_point_; + // return *this; + //} + + ///** \copydoc GPSTime::operator--() */ + // constexpr GPSTime + // operator--(int) + //{ + // return GPSTime(time_point_--); + //} /** Perform addition in-place and return the modified result. */ constexpr GPSTime& @@ -201,7 +205,7 @@ class GPSTime { } /** Add a TimeDelta to a GPSTime. */ - friend constexpr GPSTime + [[nodiscard]] friend constexpr GPSTime operator+(const GPSTime& lhs, const TimeDelta& rhs) { auto out = lhs; @@ -210,7 +214,7 @@ class GPSTime { } /** \copydoc operator+(const GPSTime&, const TimeDelta&) */ - friend constexpr GPSTime + [[nodiscard]] friend constexpr GPSTime operator+(const TimeDelta& lhs, const GPSTime& rhs) { auto out = rhs; @@ -219,7 +223,7 @@ class GPSTime { } /** Subtract a TimeDelta from a GPSTime. */ - friend constexpr GPSTime + [[nodiscard]] friend constexpr GPSTime operator-(const GPSTime& lhs, const TimeDelta& rhs) { auto out = lhs; @@ -228,49 +232,49 @@ class GPSTime { } /** Compute the difference between two GPSTime objects. */ - friend constexpr TimeDelta + [[nodiscard]] friend constexpr TimeDelta operator-(const GPSTime& lhs, const GPSTime& rhs) { return TimeDelta(lhs.time_point_ - rhs.time_point_); } /** Compare two GPSTime objects. */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator==(const GPSTime& lhs, const GPSTime& rhs) noexcept { return lhs.time_point_ == rhs.time_point_; } /** \copydoc operator==(const GPSTime&, const GPSTime&) */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator!=(const GPSTime& lhs, const GPSTime& rhs) noexcept { return not(lhs == rhs); } /** \copydoc operator==(const GPSTime&, const GPSTime&) */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator<(const GPSTime& lhs, const GPSTime& rhs) noexcept { return lhs.time_point_ < rhs.time_point_; } /** \copydoc operator==(const GPSTime&, const GPSTime&) */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator>(const GPSTime& lhs, const GPSTime& rhs) noexcept { return lhs.time_point_ > rhs.time_point_; } /** \copydoc operator==(const GPSTime&, const GPSTime&) */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator<=(const GPSTime& lhs, const GPSTime& rhs) noexcept { return not(lhs > rhs); } /** \copydoc operator==(const GPSTime&, const GPSTime&) */ - friend constexpr bool + [[nodiscard]] friend constexpr bool operator>=(const GPSTime& lhs, const GPSTime& rhs) noexcept { return not(lhs < rhs); @@ -287,4 +291,77 @@ class GPSTime { std::chrono::time_point time_point_; }; +constexpr GPSTime::GPSTime(int year, + int month, + int day, + int hour, + int minute, + int second, + int microsecond, + int picosecond) + : time_point_([=]() { + // Get the last day in the specified year/month. + const auto last_day_in_month = [](int year_, int month_) { + const auto y = date::year(year_); + const auto m = date::month(static_cast(month_)); + const auto mdl = date::month_day_last(m); + const auto ymdl = date::year_month_day_last(y, mdl); + const date::day d = ymdl.day(); + return static_cast(unsigned(d)); + }; + + if (year < 1 or year > 9999) { + throw std::invalid_argument("invalid year"); + } + if (month < 1 or month > 12) { + throw std::invalid_argument("invalid month"); + } + if (day < 1 or day > last_day_in_month(year, month)) { + throw std::invalid_argument("invalid day"); + } + if (hour < 0 or hour >= 24) { + throw std::invalid_argument("invalid hour"); + } + if (minute < 0 or minute >= 60) { + throw std::invalid_argument("invalid minute"); + } + if (second < 0 or second >= 60) { + throw std::invalid_argument("invalid second"); + } + if (microsecond < 0 or microsecond >= 1'000'000) { + throw std::invalid_argument("invalid microsecond"); + } + if (picosecond < 0 or picosecond >= 1'000'000) { + throw std::invalid_argument("invalid picosecond"); + } + + const auto y = date::year(year); + const auto m = date::month(static_cast(month)); + const auto d = date::day(static_cast(day)); + const auto ymd = date::year_month_day(y, m, d); + + // Convert calendar date to days since Unix epoch (1970-01-01). + const auto days_since_unix_epoch = date::sys_days(ymd); + + // Convert to days since GPS epoch (1980-01-06). + const auto days_since_gps_epoch = + date::clock_cast(days_since_unix_epoch); + + using Hours = std::chrono::duration>; + using Minutes = std::chrono::duration>; + using Seconds = std::chrono::duration; + using Microseconds = std::chrono::duration; + using Picoseconds = std::chrono::duration; + + // Convert time of day to duration since midnight. + const auto since_midnight = + Hours(hour) + Minutes(minute) + Seconds(second) + + Microseconds(microsecond) + Picoseconds(picosecond); + + using TimePoint = std::chrono::time_point; + + return TimePoint(days_since_gps_epoch) + since_midnight; + }()) +{} + } // namespace caesar diff --git a/datetime/v1/caesar/datetime/timedelta.hpp b/datetime/v1/caesar/datetime/timedelta.hpp index f8eabe6..96ffb11 100644 --- a/datetime/v1/caesar/datetime/timedelta.hpp +++ b/datetime/v1/caesar/datetime/timedelta.hpp @@ -109,6 +109,8 @@ class TimeDelta { /** * Return a TimeDelta object representing the specified number of days. * + * A day is assumed to contain exactly 86400 seconds. + * * \tparam T an arithmetic type representing the number of days * * ## Notes @@ -130,6 +132,8 @@ class TimeDelta { /** * Return a TimeDelta object representing the specified number of hours. * + * An hour is assumed to contain exactly 3600 seconds. + * * \tparam T an arithmetic type representing the number of hours * * ## Notes @@ -151,6 +155,8 @@ class TimeDelta { /** * Return a TimeDelta object representing the specified number of minutes. * + * A minute is assumed to contain exactly 60 seconds. + * * \tparam T an arithmetic type representing the number of minutes * * ## Notes From d3229eb89750e2e50ba1db27d781642920dd6f0b Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Sun, 7 Mar 2021 00:09:13 -0800 Subject: [PATCH 6/8] Update GPSTime; Add unit tests --- datetime/v1/caesar/datetime/gpstime.cpp | 132 ++++----- datetime/v1/caesar/datetime/gpstime.hpp | 303 ++++++++++++++------- datetime/v1/caesar/datetime/timedelta.hpp | 31 ++- datetime/v1/test/datetime/gpstime_test.cpp | 148 ++++++++++ datetime/v1/tests.txt | 1 + 5 files changed, 441 insertions(+), 174 deletions(-) create mode 100644 datetime/v1/test/datetime/gpstime_test.cpp diff --git a/datetime/v1/caesar/datetime/gpstime.cpp b/datetime/v1/caesar/datetime/gpstime.cpp index f7d9401..1e70b65 100644 --- a/datetime/v1/caesar/datetime/gpstime.cpp +++ b/datetime/v1/caesar/datetime/gpstime.cpp @@ -7,71 +7,71 @@ namespace caesar { -// GPSTime::GPSTime(std::string_view datetime_string) -// : GPSTime([=]() { -// const auto pattern = std::regex( -// R"((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(\.(\d{1,12}))?)"); -// -// std::match_results match; -// if (not std::regex_match(datetime_string.data(), pattern)) { -// throw std::invalid_argument("bad datetime string"); -// } -// -// // Parse date components. -// int y = 0, mo = 0, d = 0; -// std::sscanf(match[1].data(), "%4d-%2d-%2d", &y, &mo, &d); -// -// // Parse hour, minute, and second components. -// int h = 0, mi = 0, s = 0; -// std::sscanf(match[2].first, "%2d:%2d:%2d", &h, &mi, &s); -// -// // Append zeros to the input string up to the specified length. -// const auto zeropad = [](const auto& str, size_t length) { -// std::string out = str; -// out.resize(length, '0'); -// return out; -// }; -// -// // Parse sub-seconds components. -// int us = 0, ps = 0; -// if (match[4].length() > 0) { -// const std::string subseconds_string = zeropad(match[4], 12); -// std::sscanf(subseconds_string.data(), "%6d%6d", &us, &ps); -// } -// -// return GPSTime(y, mo, d, h, mi, s, us, ps); -// }()) -//{} -// -// GPSTime::operator std::string() const -//{ -// // Format datetime to string, excluding sub-second components. -// const std::string ymdhms_string = -// fmt::format("{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}", year(), -// month(), day(), hour(), minute(), second()); -// -// const int us = microsecond(); -// const int ps = picosecond(); -// -// // Early exit if sub-second components are zero. -// if (us == 0 and ps == 0) { -// return ymdhms_string; -// } -// -// // Format sub-second components to string. -// const std::string subseconds_string = fmt::format("{:06d}{:06d}", us, ps); -// -// // Strip trailing zeros from string. -// const auto trim = [](const std::string& str) { -// const auto n = str.find_last_not_of('0'); -// if (n == std::string::npos) { -// return str; -// } else { -// return str.substr(0, n + 1); -// } -// }; -// -// return fmt::format("{}.{}", ymdhms_string, trim(subseconds_string)); -//} +GPSTime::GPSTime(std::string_view datetime_string) + : GPSTime([=]() { + const auto pattern = std::regex( + R"((\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(\.(\d{1,12}))?)"); + + std::match_results match; + if (not std::regex_match(datetime_string.data(), match, pattern)) { + throw std::invalid_argument("bad datetime string"); + } + + // Parse date components. + int y = 0, mo = 0, d = 0; + std::sscanf(match[1].first, "%4d-%2d-%2d", &y, &mo, &d); + + // Parse hour, minute, and second components. + int h = 0, mi = 0, s = 0; + std::sscanf(match[2].first, "%2d:%2d:%2d", &h, &mi, &s); + + // Append zeros to the input string up to the specified length. + const auto zeropad = [](const auto& str, size_t length) { + std::string out = str; + out.resize(length, '0'); + return out; + }; + + // Parse sub-seconds components. + int us = 0, ps = 0; + if (match[4].length() > 0) { + const std::string subseconds_string = zeropad(match[4], 12); + std::sscanf(subseconds_string.data(), "%6d%6d", &us, &ps); + } + + return GPSTime(y, mo, d, h, mi, s, us, ps); + }()) +{} + +GPSTime::operator std::string() const +{ + // Format datetime to string, excluding sub-second components. + const std::string ymdhms_string = + fmt::format("{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}", year(), + month(), day(), hour(), minute(), second()); + + const int us = microsecond(); + const int ps = picosecond(); + + // Early exit if sub-second components are zero. + if (us == 0 and ps == 0) { + return ymdhms_string; + } + + // Format sub-second components to string. + const std::string subseconds_string = fmt::format("{:06d}{:06d}", us, ps); + + // Strip trailing zeros from string. + const auto trim = [](const std::string& str) { + const auto n = str.find_last_not_of('0'); + if (n == std::string::npos) { + return str; + } else { + return str.substr(0, n + 1); + } + }; + + return fmt::format("{}.{}", ymdhms_string, trim(subseconds_string)); +} } // namespace caesar diff --git a/datetime/v1/caesar/datetime/gpstime.hpp b/datetime/v1/caesar/datetime/gpstime.hpp index 7182783..33c81cd 100644 --- a/datetime/v1/caesar/datetime/gpstime.hpp +++ b/datetime/v1/caesar/datetime/gpstime.hpp @@ -10,6 +10,16 @@ #include #include +// Migration path to C++20 +// +// All symbols in the `date::` namespace used herein have been standardized in +// C++20 as part of the `` library. If/when support for C++17 is +// dropped, we can simply replace the namespace with `std::chrono::`. +// +// In addition, C++20 adds increment & decrement operators for +// `std::chrono::time_point`, which we could use to clean up the implementation +// of the corresponding GPSTime operators. + namespace caesar { /** @@ -17,17 +27,19 @@ namespace caesar { * resolution * * The GPS time system is an atomic time scale implemented by GPS satellites and - * ground stations. Unlike UTC time, GPS time is a continuous time scale -- leap - * seconds are not inserted. Therefore, the offset between GPS and UTC time is - * not fixed but rather changes each time a leap second adjustment is made to - * UTC. + * ground stations. Unlike UTC time, GPS time is a continuous linear time scale + * -- leap seconds are never inserted. Therefore, the offset between GPS and UTC + * time is not fixed but rather changes each time a leap second adjustment is + * made to UTC. * + * GPSTime can be broken down into individual date and time-of-day components. * The date components follow the proleptic Gregorian calendar, which allows the - * representation of dates prior to the calendar's introduction in 1582. - * XXX Should dates before year 0001 or after year 9999 be considered valid??? + * representation of dates prior to the calendar's introduction in 1582. Dates + * before year 1 or after year 9999 may not be represented by GPSTime. The time + * components describe time since midnight in a 24-hour clock system. * - * Internally, GPSTime represents time as a 128-bit integer count of picoseconds - * relative to some reference epoch. + * Internally, GPSTime stores a 128-bit integer timestamp with a picosecond tick + * interval. * * \see UTCTime * \see TimeDelta @@ -35,20 +47,30 @@ namespace caesar { class GPSTime { public: /** - * A type representing the GPS time system which meets the named - * requirements of - * [Clock](https://en.cppreference.com/w/cpp/named_req/Clock). + * A [*Clock*](https://en.cppreference.com/w/cpp/named_req/Clock) + * representing the time system on which GPSTime is measured */ using Clock = date::gps_clock; - /** \see TimeDelta::Rep */ - using Rep = TimeDelta::Rep; + /** + * A `std::chrono::duration` capable of exactly representing durations + * between GPS time points + */ + using Duration = typename TimeDelta::Duration; - /** \see TimeDelta::Period */ - using Period = TimeDelta::Period; + /** + * A signed arithmetic type representing the number of ticks of the duration + */ + using Rep = typename Duration::rep; - /** ... */ - using Duration = std::chrono::duration; + /** + * A `std::ratio` representing the tick period of the duration (i.e. the + * number of ticks per second) + */ + using Period = typename Duration::period; + + /** A `std::chrono::time_point` with the same Clock, Rep, and Period */ + using TimePoint = std::chrono::time_point; /** * Construct a new GPSTime object. @@ -61,6 +83,9 @@ class GPSTime { * \param[in] second second component, in the range [0, 60) * \param[in] microsecond microsecond component, in the range [0, 1000000) * \param[in] picosecond picosecond component, in the range [0, 1000000) + * + * \throws std::invalid_argument if any component is outside the expected + * range */ constexpr GPSTime(int year, int month, @@ -71,19 +96,31 @@ class GPSTime { int microsecond = 0, int picosecond = 0); - // XXX check >= GPSTime::min() and <= GPSTime::max() - /** Construct a new GPSTime object from a std::chrono::time_point. */ - explicit constexpr GPSTime( - const std::chrono::time_point& time_point) noexcept + /** + * Construct a new GPSTime object from a std::chrono::time_point. + * + * \throws std::out_of_range if time_point is < GPSTime::min() or > + * GPSTime::max() + */ + explicit constexpr GPSTime(const TimePoint& time_point) : time_point_(time_point) - {} + { + if (*this < GPSTime::min() or *this > GPSTime::max()) { + throw std::out_of_range( + "input time point is outside of valid GPSTime range"); + } + } - /** Construct a new GPSTime object from a string representation. */ + /** + * Construct a new GPSTime object from a string representation. + * + * \throws std::invalid_argument if the input string does not match the + * expected format + */ explicit GPSTime(std::string_view datetime_string); /** Convert to std::chrono::time_point. */ - [[nodiscard]] explicit - operator std::chrono::time_point() const + [[nodiscard]] explicit constexpr operator TimePoint() const noexcept { return time_point_; } @@ -92,14 +129,14 @@ class GPSTime { [[nodiscard]] explicit operator std::string() const; /** Return the earliest valid GPSTime. */ - [[nodiscard]] static GPSTime + [[nodiscard]] static constexpr GPSTime min() noexcept { return GPSTime(1, 1, 1, 0, 0, 0); } /** Return the latest valid GPSTime. */ - [[nodiscard]] static GPSTime + [[nodiscard]] static constexpr GPSTime max() noexcept { return GPSTime(9999, 12, 31, 23, 59, 59, 999'999, 999'999); @@ -122,71 +159,152 @@ class GPSTime { return GPSTime(Clock::now()); } + /** Return the date component. */ + [[nodiscard]] constexpr date::year_month_day + date() const + { + // Convert timestamp to Unix time (change of epoch). + using ToClock = date::local_t; + const auto unix_time = date::clock_cast(time_point_); + + // Round down to nearest day. + const auto unix_days = std::chrono::floor(unix_time); + + // Convert to calendar date. + return date::year_month_day(unix_days); + } + + /** Return the time of day. */ + [[nodiscard]] constexpr date::hh_mm_ss + time_of_day() const + { + auto d = time_point_.time_since_epoch(); + + // Subtract the number of whole days to get time since midnight. + using Days = std::chrono::duration>; + d -= std::chrono::floor(d); + + return date::hh_mm_ss(d); + } + /** Return the year component. */ - [[nodiscard]] int - year() const; + [[nodiscard]] constexpr int + year() const + { + const auto y = date().year(); + return int(y); + } /** Return the month component, encoded 1 through 12. */ - [[nodiscard]] int - month() const; + [[nodiscard]] constexpr int + month() const + { + const auto m = date().month(); + return static_cast(unsigned(m)); + } /** Return the day component. */ - [[nodiscard]] int - day() const; + [[nodiscard]] constexpr int + day() const + { + const auto d = date().day(); + return static_cast(unsigned(d)); + } /** Return the day of the week. */ - [[nodiscard]] date::weekday - weekday() const; + [[nodiscard]] constexpr date::weekday + weekday() const + { + // Convert calendar date to days since Unix epoch (1970-01-01). + const auto unix_days = date::sys_days(date()); + return date::weekday(unix_days); + } /** Return the hour component. */ - [[nodiscard]] int - hour() const; + [[nodiscard]] constexpr int + hour() const + { + const auto h = time_of_day().hours(); + return static_cast(h.count()); + } /** Return the minute component. */ - [[nodiscard]] int - minute() const; + [[nodiscard]] constexpr int + minute() const + { + const auto m = time_of_day().minutes(); + return static_cast(m.count()); + } /** Return the second component. */ - [[nodiscard]] int - second() const; + [[nodiscard]] constexpr int + second() const + { + const auto s = time_of_day().seconds(); + return static_cast(s.count()); + } /** Return the microsecond component. */ - [[nodiscard]] int - microsecond() const; + [[nodiscard]] constexpr int + microsecond() const + { + // Get subseconds component. + const Duration ss = time_of_day().subseconds(); + + // Truncate to microseconds (time of day is always nonnegative, so no + // need to use `std::chrono::floor` -- duration_cast is slightly + // more efficient). + using Microsecs = std::chrono::duration; + const auto us = std::chrono::duration_cast(ss); + + return static_cast(us.count()); + } /** Return the picosecond component. */ - [[nodiscard]] int - picosecond() const; - - ///** Increment the tick count. */ - // constexpr GPSTime& - // operator++() - //{ - // ++time_point_; - // return *this; - //} - - ///** \copydoc GPSTime::operator++() */ - // constexpr GPSTime - // operator++(int) - //{ - // return GPSTime(time_point_++); - //} - - ///** Decrement the tick count. */ - // constexpr GPSTime& - // operator--() - //{ - // --time_point_; - // return *this; - //} - - ///** \copydoc GPSTime::operator--() */ - // constexpr GPSTime - // operator--(int) - //{ - // return GPSTime(time_point_--); - //} + [[nodiscard]] constexpr int + picosecond() const + { + // Get subseconds component. + const Duration ss = time_of_day().subseconds(); + + // Subtract the number of whole microseconds to get picoseconds + // component. + using Microsecs = std::chrono::duration; + const auto us = std::chrono::duration_cast(ss); + const auto ps = ss - us; + + return static_cast(ps.count()); + } + + /** Increment the tick count. */ + constexpr GPSTime& + operator++() + { + time_point_ += Duration(1); + return *this; + } + + /** \copydoc GPSTime::operator++() */ + constexpr GPSTime + operator++(int) + { + return GPSTime(time_point_ + Duration(1)); + } + + /** Decrement the tick count. */ + constexpr GPSTime& + operator--() + { + time_point_ -= Duration(1); + return *this; + } + + /** \copydoc GPSTime::operator--() */ + constexpr GPSTime + operator--(int) + { + return GPSTime(time_point_ - Duration(1)); + } /** Perform addition in-place and return the modified result. */ constexpr GPSTime& @@ -282,13 +400,13 @@ class GPSTime { /** Serialize a string representation to an output stream. */ friend std::ostream& - operator<<(std::ostream& os, const GPSTime& ts) + operator<<(std::ostream& os, const GPSTime& t) { - return os << std::string(ts); + return os << std::string(t); } private: - std::chrono::time_point time_point_; + TimePoint time_point_; }; constexpr GPSTime::GPSTime(int year, @@ -301,11 +419,11 @@ constexpr GPSTime::GPSTime(int year, int picosecond) : time_point_([=]() { // Get the last day in the specified year/month. - const auto last_day_in_month = [](int year_, int month_) { - const auto y = date::year(year_); - const auto m = date::month(static_cast(month_)); - const auto mdl = date::month_day_last(m); - const auto ymdl = date::year_month_day_last(y, mdl); + const auto last_day_in_month = [](int y, int m) { + const auto yy = date::year(y); + const auto mm = date::month(static_cast(m)); + const auto mdl = date::month_day_last(mm); + const auto ymdl = date::year_month_day_last(yy, mdl); const date::day d = ymdl.day(); return static_cast(unsigned(d)); }; @@ -341,26 +459,23 @@ constexpr GPSTime::GPSTime(int year, const auto ymd = date::year_month_day(y, m, d); // Convert calendar date to days since Unix epoch (1970-01-01). - const auto days_since_unix_epoch = date::sys_days(ymd); + const auto unix_days = date::local_days(ymd); // Convert to days since GPS epoch (1980-01-06). - const auto days_since_gps_epoch = - date::clock_cast(days_since_unix_epoch); + const auto gps_days = date::clock_cast(unix_days); using Hours = std::chrono::duration>; using Minutes = std::chrono::duration>; using Seconds = std::chrono::duration; - using Microseconds = std::chrono::duration; - using Picoseconds = std::chrono::duration; + using Microsecs = std::chrono::duration; + using Picosecs = std::chrono::duration; // Convert time of day to duration since midnight. - const auto since_midnight = - Hours(hour) + Minutes(minute) + Seconds(second) + - Microseconds(microsecond) + Picoseconds(picosecond); - - using TimePoint = std::chrono::time_point; + const auto since_midnight = Hours(hour) + Minutes(minute) + + Seconds(second) + Microsecs(microsecond) + + Picosecs(picosecond); - return TimePoint(days_since_gps_epoch) + since_midnight; + return TimePoint(gps_days) + since_midnight; }()) {} diff --git a/datetime/v1/caesar/datetime/timedelta.hpp b/datetime/v1/caesar/datetime/timedelta.hpp index 96ffb11..8ca1482 100644 --- a/datetime/v1/caesar/datetime/timedelta.hpp +++ b/datetime/v1/caesar/datetime/timedelta.hpp @@ -44,10 +44,13 @@ class TimeDelta { /** * A `std::ratio` representing the tick period (i.e. the number of ticks per - * second). + * second) */ using Period = std::pico; + /** A `std::chrono::duration` with the same Rep and Period */ + using Duration = std::chrono::duration; + /** Construct a new TimeDelta object representing a zero-length duration. */ TimeDelta() = default; @@ -86,14 +89,14 @@ class TimeDelta { [[nodiscard]] static constexpr TimeDelta min() noexcept { - return TimeDelta(std::chrono::duration::min()); + return TimeDelta(Duration::min()); } /** Return the largest representable TimeDelta. */ [[nodiscard]] static constexpr TimeDelta max() noexcept { - return TimeDelta(std::chrono::duration::max()); + return TimeDelta(Duration::max()); } /** @@ -103,7 +106,7 @@ class TimeDelta { [[nodiscard]] static constexpr TimeDelta resolution() noexcept { - return TimeDelta(std::chrono::duration(1)); + return TimeDelta(Duration(1)); } /** @@ -528,15 +531,14 @@ class TimeDelta { } private: - std::chrono::duration duration_ = {}; + Duration duration_ = {}; }; /** Return the absolute value of the input TimeDelta. */ [[nodiscard]] constexpr TimeDelta abs(const TimeDelta& dt) { - using Duration = std::chrono::duration; - const auto d = Duration(dt); + const auto d = TimeDelta::Duration(dt); return TimeDelta(std::chrono::abs(d)); } @@ -641,19 +643,19 @@ constexpr TimeDelta::TimeDelta( const std::chrono::duration& d) : duration_([=]() { static_assert(is_arithmetic); - using ToDuration = std::chrono::duration; if constexpr (std::is_floating_point_v) { // Convert from input tick period to floating-point picoseconds. - using PicosecsFP = std::chrono::duration; - const auto p = std::chrono::duration_cast(d); + using Picosecs = std::chrono::duration; + const auto p = std::chrono::duration_cast(d); // Cast floating-point to int128. const auto r = static_cast(p.count()); - return ToDuration(r); + return Duration(r); } else { - // For integral durations, we can just use duration_cast. - return std::chrono::duration_cast(d); + // For integral durations, we can just use duration_cast to + // convert directly. + return std::chrono::duration_cast(d); } }()) {} @@ -672,7 +674,8 @@ constexpr TimeDelta::operator std::chrono::duration() const // Convert to output tick period. return std::chrono::duration_cast(p); } else { - // For integral durations, we can just use duration_cast. + // For integral durations, we can just use duration_cast to convert + // directly. return std::chrono::duration_cast(duration_); } } diff --git a/datetime/v1/test/datetime/gpstime_test.cpp b/datetime/v1/test/datetime/gpstime_test.cpp new file mode 100644 index 0000000..2e781ab --- /dev/null +++ b/datetime/v1/test/datetime/gpstime_test.cpp @@ -0,0 +1,148 @@ +#include + +#include +#include + +namespace cs = caesar; + +TEST_CASE("datetime.gpstime.from_components") +{ + SUBCASE("after_epoch") + { + int Y = 2001, M = 2, D = 3; + int h = 4, m = 5, s = 6; + int us = 7, ps = 8; + + const auto t = cs::GPSTime(Y, M, D, h, m, s, us, ps); + + CHECK_EQ(t.year(), Y); + CHECK_EQ(t.month(), M); + CHECK_EQ(t.day(), D); + CHECK_EQ(t.hour(), h); + CHECK_EQ(t.minute(), m); + CHECK_EQ(t.second(), s); + CHECK_EQ(t.microsecond(), us); + CHECK_EQ(t.picosecond(), ps); + } + + SUBCASE("before_epoch") + { + int Y = 900, M = 8, D = 7; + int h = 6, m = 5, s = 4; + int us = 3, ps = 2; + + const auto t = cs::GPSTime(Y, M, D, h, m, s, us, ps); + + CHECK_EQ(t.year(), Y); + CHECK_EQ(t.month(), M); + CHECK_EQ(t.day(), D); + CHECK_EQ(t.hour(), h); + CHECK_EQ(t.minute(), m); + CHECK_EQ(t.second(), s); + CHECK_EQ(t.microsecond(), us); + CHECK_EQ(t.picosecond(), ps); + } + + const auto check_bad_gpstime = [](int Y, int M, int D, int h, int m, int s, + int us, int ps) { + CHECK_THROWS_AS({ cs::GPSTime(Y, M, D, h, m, s, us, ps); }, + std::invalid_argument); + }; + + SUBCASE("invalid_year") + { + check_bad_gpstime(0, 1, 1, 0, 0, 0, 0, 0); + check_bad_gpstime(10000, 1, 1, 0, 0, 0, 0, 0); + } + + SUBCASE("invalid_month") + { + check_bad_gpstime(2000, 0, 1, 0, 0, 0, 0, 0); + check_bad_gpstime(2000, 13, 1, 0, 0, 0, 0, 0); + } + + SUBCASE("invalid_day") + { + check_bad_gpstime(2000, 1, 0, 0, 0, 0, 0, 0); + check_bad_gpstime(2000, 1, 32, 0, 0, 0, 0, 0); + check_bad_gpstime(2000, 2, 30, 0, 0, 0, 0, 0); + check_bad_gpstime(2001, 2, 29, 0, 0, 0, 0, 0); + check_bad_gpstime(2000, 4, 31, 0, 0, 0, 0, 0); + } + + SUBCASE("invalid_hour") + { + check_bad_gpstime(2000, 1, 1, -1, 0, 0, 0, 0); + check_bad_gpstime(2000, 1, 1, 24, 0, 0, 0, 0); + } + + SUBCASE("invalid_minute") + { + check_bad_gpstime(2000, 1, 1, 0, -1, 0, 0, 0); + check_bad_gpstime(2000, 1, 1, 0, 60, 0, 0, 0); + } + + SUBCASE("invalid_second") + { + check_bad_gpstime(2000, 1, 1, 0, 0, -1, 0, 0); + check_bad_gpstime(2000, 1, 1, 0, 0, 60, 0, 0); + } + + SUBCASE("invalid_microsecond") + { + check_bad_gpstime(2000, 1, 1, 0, 0, 0, -1, 0); + check_bad_gpstime(2000, 1, 1, 0, 0, 0, 1'000'000, 0); + } + + SUBCASE("invalid_picosecond") + { + check_bad_gpstime(2000, 1, 1, 0, 0, 0, 0, -1); + check_bad_gpstime(2000, 1, 1, 0, 0, 0, 0, 1'000'000); + } +} + +TEST_CASE("datetime.gpstime.min") +{ + const auto t = cs::GPSTime::min(); + + CHECK_EQ(t.year(), 1); + CHECK_EQ(t.month(), 1); + CHECK_EQ(t.day(), 1); + CHECK_EQ(t.hour(), 0); + CHECK_EQ(t.minute(), 0); + CHECK_EQ(t.second(), 0); + CHECK_EQ(t.microsecond(), 0); + CHECK_EQ(t.picosecond(), 0); +} + +TEST_CASE("datetime.gpstime.max") +{ + const auto t = cs::GPSTime::max(); + + CHECK_EQ(t.year(), 9999); + CHECK_EQ(t.month(), 12); + CHECK_EQ(t.day(), 31); + CHECK_EQ(t.hour(), 23); + CHECK_EQ(t.minute(), 59); + CHECK_EQ(t.second(), 59); + CHECK_EQ(t.microsecond(), 999'999); + CHECK_EQ(t.picosecond(), 999'999); +} + +TEST_CASE("datetime.gpstime.resolution") +{ + const cs::TimeDelta resolution = cs::GPSTime::resolution(); + const cs::TimeDelta ps = cs::TimeDelta::picoseconds(1); + + CHECK_EQ(resolution, ps); +} + +TEST_CASE("datetime.gpstime.now") +{ + // Not much we can test here. now() must be some time after the day this + // test was written. + const auto t = cs::GPSTime::now(); + const auto min = cs::GPSTime(2021, 3, 6, 0, 0, 0); + + CHECK_GE(t, min); +} diff --git a/datetime/v1/tests.txt b/datetime/v1/tests.txt index 7cb088a..12e05e1 100644 --- a/datetime/v1/tests.txt +++ b/datetime/v1/tests.txt @@ -1,2 +1,3 @@ +test/datetime/gpstime_test.cpp test/datetime/timedelta_test.cpp test/main.cpp From bb2185f7464f9a0b64ccc34dba49533a12545f8d Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Sun, 7 Mar 2021 21:54:33 -0800 Subject: [PATCH 7/8] Add GPSTime unit tests --- datetime/v1/caesar/datetime/gpstime.hpp | 13 +- datetime/v1/caesar/datetime/timedelta.hpp | 2 +- datetime/v1/test/datetime/gpstime_test.cpp | 293 ++++++++++++++++++- datetime/v1/test/datetime/timedelta_test.cpp | 12 +- 4 files changed, 306 insertions(+), 14 deletions(-) diff --git a/datetime/v1/caesar/datetime/gpstime.hpp b/datetime/v1/caesar/datetime/gpstime.hpp index 33c81cd..8eaa641 100644 --- a/datetime/v1/caesar/datetime/gpstime.hpp +++ b/datetime/v1/caesar/datetime/gpstime.hpp @@ -251,9 +251,8 @@ class GPSTime { // Get subseconds component. const Duration ss = time_of_day().subseconds(); - // Truncate to microseconds (time of day is always nonnegative, so no - // need to use `std::chrono::floor` -- duration_cast is slightly - // more efficient). + // Truncate to microseconds (time of day is always nonnegative, so we + // use duration_cast instead of floor, which is slightly less efficient. using Microsecs = std::chrono::duration; const auto us = std::chrono::duration_cast(ss); @@ -288,7 +287,9 @@ class GPSTime { constexpr GPSTime operator++(int) { - return GPSTime(time_point_ + Duration(1)); + auto tmp = *this; + operator++(); + return tmp; } /** Decrement the tick count. */ @@ -303,7 +304,9 @@ class GPSTime { constexpr GPSTime operator--(int) { - return GPSTime(time_point_ - Duration(1)); + auto tmp = *this; + operator--(); + return tmp; } /** Perform addition in-place and return the modified result. */ diff --git a/datetime/v1/caesar/datetime/timedelta.hpp b/datetime/v1/caesar/datetime/timedelta.hpp index 8ca1482..9b0419a 100644 --- a/datetime/v1/caesar/datetime/timedelta.hpp +++ b/datetime/v1/caesar/datetime/timedelta.hpp @@ -295,7 +295,7 @@ class TimeDelta { } /** Return the total number of seconds in the duration. */ - [[nodiscard]] double + [[nodiscard]] constexpr double total_seconds() const { using Seconds = std::chrono::duration; diff --git a/datetime/v1/test/datetime/gpstime_test.cpp b/datetime/v1/test/datetime/gpstime_test.cpp index 2e781ab..61a83ed 100644 --- a/datetime/v1/test/datetime/gpstime_test.cpp +++ b/datetime/v1/test/datetime/gpstime_test.cpp @@ -1,6 +1,7 @@ #include #include +#include #include namespace cs = caesar; @@ -101,6 +102,104 @@ TEST_CASE("datetime.gpstime.from_components") } } +TEST_CASE("datetime.gpstime.from_time_point") +{ + // A std::chrono::time_point representing one hour, two minutes, and three + // seconds after midnight on 1980-01-06 (the GPS epoch). + const auto gps_epoch = cs::GPSTime::TimePoint() + std::chrono::hours(1) + + std::chrono::minutes(2) + std::chrono::seconds(3); + + const auto t = cs::GPSTime(gps_epoch); + + CHECK_EQ(t.year(), 1980); + CHECK_EQ(t.month(), 1); + CHECK_EQ(t.day(), 6); + CHECK_EQ(t.hour(), 1); + CHECK_EQ(t.minute(), 2); + CHECK_EQ(t.second(), 3); + CHECK_EQ(t.microsecond(), 0); + CHECK_EQ(t.picosecond(), 0); +} + +TEST_CASE("datetime.gpstime.from_string") +{ + SUBCASE("from_chars") + { + const auto s = "2001-02-03T04:05:06.789"; + const auto t = cs::GPSTime(s); + + CHECK_EQ(t.year(), 2001); + CHECK_EQ(t.month(), 2); + CHECK_EQ(t.day(), 3); + CHECK_EQ(t.hour(), 4); + CHECK_EQ(t.minute(), 5); + CHECK_EQ(t.second(), 6); + CHECK_EQ(t.microsecond(), 789'000); + CHECK_EQ(t.picosecond(), 0); + } + + SUBCASE("from_string") + { + const std::string s = "2001-02-03T04:05:06.000007000008"; + const auto t = cs::GPSTime(s); + + CHECK_EQ(t.year(), 2001); + CHECK_EQ(t.month(), 2); + CHECK_EQ(t.day(), 3); + CHECK_EQ(t.hour(), 4); + CHECK_EQ(t.minute(), 5); + CHECK_EQ(t.second(), 6); + CHECK_EQ(t.microsecond(), 7); + CHECK_EQ(t.picosecond(), 8); + } + + SUBCASE("no_subseconds") + { + const auto s = "2001-02-03T04:05:06"; + const auto t = cs::GPSTime(s); + + CHECK_EQ(t.year(), 2001); + CHECK_EQ(t.month(), 2); + CHECK_EQ(t.day(), 3); + CHECK_EQ(t.hour(), 4); + CHECK_EQ(t.minute(), 5); + CHECK_EQ(t.second(), 6); + CHECK_EQ(t.microsecond(), 0); + CHECK_EQ(t.picosecond(), 0); + } + + SUBCASE("bad_format") + { + const auto s = "asdf"; + CHECK_THROWS_AS({ cs::GPSTime{s}; }, std::invalid_argument); + } + + SUBCASE("too_many_digits") + { + // Only up to 12 digits are allowed for subsecond components. + const std::string s = "2001-02-03T04:05:06.0000000000001"; + CHECK_THROWS_AS({ cs::GPSTime{s}; }, std::invalid_argument); + } +} + +TEST_CASE("datetime.gpstime.to_string") +{ + { + const auto t = cs::GPSTime(2000, 1, 2, 3, 4, 5, 6, 7); + CHECK_EQ(std::string(t), "2000-01-02T03:04:05.000006000007"); + } + + { + const auto t = cs::GPSTime(2000, 1, 2, 3, 4, 5); + CHECK_EQ(std::string(t), "2000-01-02T03:04:05"); + } + + { + const auto t = cs::GPSTime(2000, 1, 2, 3, 4, 5, 678'900); + CHECK_EQ(std::string(t), "2000-01-02T03:04:05.6789"); + } +} + TEST_CASE("datetime.gpstime.min") { const auto t = cs::GPSTime::min(); @@ -142,7 +241,197 @@ TEST_CASE("datetime.gpstime.now") // Not much we can test here. now() must be some time after the day this // test was written. const auto t = cs::GPSTime::now(); - const auto min = cs::GPSTime(2021, 3, 6, 0, 0, 0); + const auto today = cs::GPSTime(2021, 3, 6, 0, 0, 0); + + CHECK_GE(t, today); +} + +TEST_CASE("datetime.gpstime.date") +{ + const auto t = cs::GPSTime(2001, 2, 3, 4, 5, 6, 7, 8); + const date::year_month_day date = t.date(); + + CHECK_EQ(date.year(), date::year(2001)); + CHECK_EQ(date.month(), date::month(2)); + CHECK_EQ(date.day(), date::day(3)); +} + +TEST_CASE("datetime.gpstime.time_of_day") +{ + const auto t = cs::GPSTime(2001, 2, 3, 4, 5, 6, 7, 8); + const auto tod = t.time_of_day(); + + CHECK_EQ(tod.hours(), std::chrono::hours(4)); + CHECK_EQ(tod.minutes(), std::chrono::minutes(5)); + CHECK_EQ(tod.seconds(), std::chrono::seconds(6)); + + const auto subseconds = decltype(tod)::precision(7'000'008); + CHECK_EQ(tod.subseconds(), subseconds); +} + +TEST_CASE("datetime.gpstime.increment") +{ + cs::GPSTime t(2001, 1, 1, 0, 0, 0); + + SUBCASE("prefix_increment") + { + CHECK_EQ((++t).picosecond(), 1); + CHECK_EQ(t.picosecond(), 1); + } + + SUBCASE("postfix_increment") + { + CHECK_EQ((t++).picosecond(), 0); + CHECK_EQ(t.picosecond(), 1); + } +} + +TEST_CASE("datetime.gpstime.decrement") +{ + auto t = cs::GPSTime(2001, 1, 1, 0, 0, 0); + + SUBCASE("prefix_decrement") + { + CHECK_EQ((--t).picosecond(), 999'999); + CHECK_EQ(t.picosecond(), 999'999); + } + + SUBCASE("postfix_decrement") + { + CHECK_EQ((t--).picosecond(), 0); + CHECK_EQ(t.picosecond(), 999'999); + } +} + +TEST_CASE("datetime.gpstime.add_timedelta") +{ + auto t = cs::GPSTime(2000, 1, 2, 3, 4, 5, 6, 7); + const auto dt = cs::TimeDelta::days(12) + cs::TimeDelta::minutes(34) + + cs::TimeDelta::seconds(56) + + cs::TimeDelta::microseconds(78) + + cs::TimeDelta::picoseconds(90); + + const auto sum = cs::GPSTime(2000, 1, 14, 3, 39, 1, 84, 97); + + SUBCASE("compound_assignment") + { + t += dt; + CHECK_EQ(t, sum); + } + + SUBCASE("binary_operator") { CHECK_EQ(t + dt, sum); } +} + +TEST_CASE("datetime.gpstime.subtract_timedelta") +{ + auto t = cs::GPSTime(2001, 2, 3, 4, 5, 6, 7, 8); + const auto dt = cs::TimeDelta::days(12) + cs::TimeDelta::minutes(34) + + cs::TimeDelta::seconds(56) + + cs::TimeDelta::microseconds(78) + + cs::TimeDelta::picoseconds(90); + + const auto diff = cs::GPSTime(2001, 1, 22, 3, 30, 9, 999'928, 999'918); + + SUBCASE("compound_assignment") + { + t -= dt; + CHECK_EQ(t, diff); + } + + SUBCASE("binary_operator") { CHECK_EQ(t - dt, diff); } +} + +TEST_CASE("datetime.gpstime.subtract_gpstime") +{ + const auto t1 = cs::GPSTime(2001, 2, 3, 4, 5, 6, 7, 8); + const auto t2 = cs::GPSTime(2001, 1, 22, 3, 30, 9, 999'928, 999'918); - CHECK_GE(t, min); + const auto dt = cs::TimeDelta::days(12) + cs::TimeDelta::minutes(34) + + cs::TimeDelta::seconds(56) + + cs::TimeDelta::microseconds(78) + + cs::TimeDelta::picoseconds(90); + + CHECK_EQ(t2 - t1, -dt); + CHECK_EQ(t1 - t2, dt); +} + +TEST_CASE("datetime.gpstime.compare_eq") +{ + const auto t1 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t2 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t3 = cs::GPSTime(2000, 1, 1, 0, 0, 0, 0, 1); + + CHECK(t1 == t1); + CHECK(t1 == t2); + CHECK_FALSE(t1 == t3); +} + +TEST_CASE("datetime.gpstime.compare_ne") +{ + const auto t1 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t2 = cs::GPSTime(2000, 1, 1, 0, 0, 0, 0, 1); + const auto t3 = cs::GPSTime(2000, 1, 1, 0, 0, 0, 0, 1); + + CHECK(t1 != t2); + CHECK_FALSE(t2 != t3); +} + +TEST_CASE("datetime.gpstime.compare_lt") +{ + const auto t1 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t2 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t3 = cs::GPSTime(2000, 1, 1, 0, 0, 0, 0, 1); + const auto t4 = cs::GPSTime(1999, 12, 31, 23, 59, 59); + + CHECK(t1 < t3); + CHECK(t4 < t1); + CHECK_FALSE(t1 < t2); + CHECK_FALSE(t3 < t4); +} + +TEST_CASE("datetime.gpstime.compare_gt") +{ + const auto t1 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t2 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t3 = cs::GPSTime(2000, 1, 1, 0, 0, 0, 0, 1); + const auto t4 = cs::GPSTime(1999, 12, 31, 23, 59, 59); + + CHECK(t3 > t1); + CHECK(t1 > t4); + CHECK_FALSE(t1 > t2); + CHECK_FALSE(t4 > t3); +} + +TEST_CASE("datetime.gpstime.compare_le") +{ + const auto t1 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t2 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t3 = cs::GPSTime(2000, 1, 1, 0, 0, 0, 0, 1); + const auto t4 = cs::GPSTime(1999, 12, 31, 23, 59, 59); + + CHECK(t1 <= t3); + CHECK(t4 <= t1); + CHECK(t1 <= t2); + CHECK_FALSE(t3 <= t4); +} + +TEST_CASE("datetime.gpstime.compare_ge") +{ + const auto t1 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t2 = cs::GPSTime(2000, 1, 1, 0, 0, 0); + const auto t3 = cs::GPSTime(2000, 1, 1, 0, 0, 0, 0, 1); + const auto t4 = cs::GPSTime(1999, 12, 31, 23, 59, 59); + + CHECK(t3 >= t1); + CHECK(t1 >= t4); + CHECK(t1 >= t2); + CHECK_FALSE(t4 >= t3); +} + +TEST_CASE("datetime.gpstime.to_stream") +{ + const auto t = cs::GPSTime(2000, 1, 2, 3, 4, 5, 678'000); + std::ostringstream ss; + ss << t; + CHECK_EQ(ss.str(), "2000-01-02T03:04:05.678"); } diff --git a/datetime/v1/test/datetime/timedelta_test.cpp b/datetime/v1/test/datetime/timedelta_test.cpp index 37445c5..21fe1c3 100644 --- a/datetime/v1/test/datetime/timedelta_test.cpp +++ b/datetime/v1/test/datetime/timedelta_test.cpp @@ -45,7 +45,7 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.default_construct") CHECK_EQ(dt.count(), 0); } -TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.from_chrono_duration") +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.from_duration") { SUBCASE("integer_no_truncation") { @@ -74,7 +74,7 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.from_chrono_duration") } } -TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.to_chrono_duration") +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.to_duration") { SUBCASE("integer") { @@ -300,13 +300,13 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.increment") { cs::TimeDelta dt; - SUBCASE("prefix") + SUBCASE("prefix_increment") { CHECK_EQ(++dt, one_picosec); CHECK_EQ(dt, one_picosec); } - SUBCASE("postfix") + SUBCASE("postfix_increment") { CHECK_EQ(dt++, cs::TimeDelta()); CHECK_EQ(dt, one_picosec); @@ -317,13 +317,13 @@ TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.decrement") { cs::TimeDelta dt; - SUBCASE("prefix") + SUBCASE("prefix_decrement") { CHECK_EQ(--dt, -one_picosec); CHECK_EQ(dt, -one_picosec); } - SUBCASE("postfix") + SUBCASE("postfix_decrement") { CHECK_EQ(dt--, cs::TimeDelta()); CHECK_EQ(dt, -one_picosec); From d7dd563becb0a0193c7ed3a37cd4efcb12226c06 Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Thu, 11 Mar 2021 22:04:40 -0800 Subject: [PATCH 8/8] Clean up cmake --- datetime/v1/CMakeLists.txt | 86 ------------------------- datetime/v1/cmake/CaesarParseFile.cmake | 28 -------- datetime/v1/sources.txt | 2 - datetime/v1/tests.txt | 3 - datetime/v1/warnings.txt | 45 ------------- 5 files changed, 164 deletions(-) delete mode 100644 datetime/v1/CMakeLists.txt delete mode 100644 datetime/v1/cmake/CaesarParseFile.cmake delete mode 100644 datetime/v1/sources.txt delete mode 100644 datetime/v1/tests.txt delete mode 100644 datetime/v1/warnings.txt diff --git a/datetime/v1/CMakeLists.txt b/datetime/v1/CMakeLists.txt deleted file mode 100644 index f91e554..0000000 --- a/datetime/v1/CMakeLists.txt +++ /dev/null @@ -1,86 +0,0 @@ -cmake_minimum_required(VERSION 3.19) - -project(datetime LANGUAGES CXX) - -# Add custom CMake modules to module include path. -list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake) - -# Check if this is the top-level CMake project or was included as a sub-project. -if(PROJECT_NAME STREQUAL CMAKE_PROJECT_NAME) - set(CAESAR_MAIN_PROJECT ON) -else() - set(CAESAR_MAIN_PROJECT OFF) -endif() - -# Project configuration options. -option(CAESAR_TEST "Enable test suite" ${CAESAR_MAIN_PROJECT}) -option(CAESAR_WERROR "Treat compiler warnings as errors" ON) - -# Import third-party dependencies. -find_package(absl REQUIRED CONFIG COMPONENTS numeric) -find_package(date REQUIRED CONFIG COMPONENTS date date-tz) -find_package(doctest REQUIRED CONFIG) -find_package(fmt REQUIRED CONFIG) - -# Enable compiler diagnostic flags. -include(CaesarParseFile) -caesar_parse_file(warnings.txt warnings) - -if(CAESAR_WERROR) - list(APPEND warnings -Werror) -endif() - -include(CheckCompilerFlag) -foreach(warning ${warnings}) - check_compiler_flag(CXX ${warning} cxx_${warning}_supported) - if(cxx_${warning}_supported) - add_compile_options($<$:${warning}>) - endif() -endforeach() - -add_library(datetime SHARED) -add_library(caesar::datetime ALIAS datetime) - -# Require C++17. -target_compile_features(datetime PUBLIC cxx_std_17) - -# Add sources. -caesar_parse_file(sources.txt sources) -target_sources(datetime PRIVATE ${sources}) - -# Add include dirs. -target_include_directories( - datetime - PUBLIC - $ - ) - -# Link to imported targets. -target_link_libraries( - datetime - PUBLIC - absl::numeric - date::date - date::date-tz - PRIVATE - fmt::fmt - ) - -if(CAESAR_TEST) - enable_testing() - - # Add test sources. - caesar_parse_file(tests.txt tests) - add_executable(datetime-test ${tests}) - - target_link_libraries( - datetime-test - PRIVATE - caesar::datetime - doctest::doctest - ) - - # Register tests with CTest. - include(doctest) - doctest_discover_tests(datetime-test) -endif() diff --git a/datetime/v1/cmake/CaesarParseFile.cmake b/datetime/v1/cmake/CaesarParseFile.cmake deleted file mode 100644 index 8beb6d3..0000000 --- a/datetime/v1/cmake/CaesarParseFile.cmake +++ /dev/null @@ -1,28 +0,0 @@ -include_guard() - -#[[ -Parse the contents of the file specified by and store them in -. - -The input file is treated as a dependency of the CMake project. Modifications -to this file will trigger re-configuration in the next build. - -If the input is a relative path, it is treated with respect to the value of -*CMAKE_CURRENT_SOURCE_DIR*. - -`` -caesar_parse_file( ) -`` -#]] -function(caesar_parse_file filename result) - # Read file contents to variable. - file(STRINGS ${filename} contents) - - # Add *filename* as a dependency of the CMake build. CMake inspects the - # timestamp of the file and will re-configure if the file has changed since - # the last cmake run. - configure_file(${filename} ${filename} COPYONLY) - - # Set output variable. - set(${result} "${contents}" PARENT_SCOPE) -endfunction() diff --git a/datetime/v1/sources.txt b/datetime/v1/sources.txt deleted file mode 100644 index 5f6459a..0000000 --- a/datetime/v1/sources.txt +++ /dev/null @@ -1,2 +0,0 @@ -caesar/datetime/gpstime.cpp -caesar/datetime/timedelta.cpp diff --git a/datetime/v1/tests.txt b/datetime/v1/tests.txt deleted file mode 100644 index 12e05e1..0000000 --- a/datetime/v1/tests.txt +++ /dev/null @@ -1,3 +0,0 @@ -test/datetime/gpstime_test.cpp -test/datetime/timedelta_test.cpp -test/main.cpp diff --git a/datetime/v1/warnings.txt b/datetime/v1/warnings.txt deleted file mode 100644 index 86f1906..0000000 --- a/datetime/v1/warnings.txt +++ /dev/null @@ -1,45 +0,0 @@ --Wall --Walloc-zero --Wcast-align --Wcast-qual --Wconversion --Wcuda-compat --Wdeprecated --Wdouble-promotion --Wduplicated-branches --Wduplicated-cond --Wextra --Wformat=2 --Wimplicit-fallthrough --Winconsistent-missing-destructor-override --Wlogical-op --Wloop-analysis --Wmisleading-indentation --Wmissing-noreturn --Wnon-virtual-dtor --Wnull-dereference --Wold-style-cast --Woverloaded-virtual --Wpacked --Wpedantic --Wredundant-parens --Wshadow --Wsign-conversion --Wstrict-aliasing --Wstrict-overflow --Wtautological-compare --Wthread-safety --Wundef --Wundefined-func-template --Wundefined-reinterpret-cast --Wuninitialized --Wunknown-pragmas --Wunneeded-internal-declaration --Wunreachable-code-aggressive --Wunused --Wunused-member-function --Wvector-conversion --Wvla --Wweak-template-vtables --Wweak-vtables --Wzero-as-null-pointer-constant