diff --git a/datetime/v1/caesar/datetime.hpp b/datetime/v1/caesar/datetime.hpp new file mode 100644 index 0000000..3900aa8 --- /dev/null +++ b/datetime/v1/caesar/datetime.hpp @@ -0,0 +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..1e70b65 --- /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(), 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 new file mode 100644 index 0000000..8eaa641 --- /dev/null +++ b/datetime/v1/caesar/datetime/gpstime.hpp @@ -0,0 +1,485 @@ +#pragma once + +#include "timedelta.hpp" + +#include +#include +#include +#include +#include +#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 { + +/** + * A date/time point in Global Positioning System (GPS) time with picosecond + * 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 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. 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 stores a 128-bit integer timestamp with a picosecond tick + * interval. + * + * \see UTCTime + * \see TimeDelta + */ +class GPSTime { +public: + /** + * 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; + + /** + * A `std::chrono::duration` capable of exactly representing durations + * between GPS time points + */ + using Duration = typename TimeDelta::Duration; + + /** + * A signed arithmetic type representing the number of ticks of the duration + */ + using Rep = typename Duration::rep; + + /** + * 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. + * + * \param[in] year year component + * \param[in] month month component, encoded 1 through 12 + * \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) + * \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, + 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. + * + * \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. + * + * \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 constexpr operator TimePoint() const noexcept + { + return time_point_; + } + + /** Return a string representation of the GPS time. */ + [[nodiscard]] explicit operator std::string() const; + + /** Return the earliest valid GPSTime. */ + [[nodiscard]] static constexpr GPSTime + min() noexcept + { + return GPSTime(1, 1, 1, 0, 0, 0); + } + + /** Return the latest valid GPSTime. */ + [[nodiscard]] static constexpr 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. + */ + [[nodiscard]] static constexpr TimeDelta + resolution() noexcept + { + return TimeDelta::resolution(); + } + + /** Return the current time in GPS time. */ + [[nodiscard]] static GPSTime + now() + { + 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]] constexpr int + year() const + { + const auto y = date().year(); + return int(y); + } + + /** Return the month component, encoded 1 through 12. */ + [[nodiscard]] constexpr int + month() const + { + const auto m = date().month(); + return static_cast(unsigned(m)); + } + + /** Return the day component. */ + [[nodiscard]] constexpr int + day() const + { + const auto d = date().day(); + return static_cast(unsigned(d)); + } + + /** Return the day of the week. */ + [[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]] constexpr int + hour() const + { + const auto h = time_of_day().hours(); + return static_cast(h.count()); + } + + /** Return the minute component. */ + [[nodiscard]] constexpr int + minute() const + { + const auto m = time_of_day().minutes(); + return static_cast(m.count()); + } + + /** Return the second component. */ + [[nodiscard]] constexpr int + second() const + { + const auto s = time_of_day().seconds(); + return static_cast(s.count()); + } + + /** Return the microsecond component. */ + [[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 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); + + return static_cast(us.count()); + } + + /** Return the picosecond component. */ + [[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) + { + auto tmp = *this; + operator++(); + return tmp; + } + + /** Decrement the tick count. */ + constexpr GPSTime& + operator--() + { + time_point_ -= Duration(1); + return *this; + } + + /** \copydoc GPSTime::operator--() */ + constexpr GPSTime + operator--(int) + { + auto tmp = *this; + operator--(); + return tmp; + } + + /** 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. */ + [[nodiscard]] friend constexpr GPSTime + operator+(const GPSTime& lhs, const TimeDelta& rhs) + { + auto out = lhs; + out += rhs; + return out; + } + + /** \copydoc operator+(const GPSTime&, const TimeDelta&) */ + [[nodiscard]] friend constexpr GPSTime + operator+(const TimeDelta& lhs, const GPSTime& rhs) + { + auto out = rhs; + out += lhs; + return out; + } + + /** Subtract a TimeDelta from a GPSTime. */ + [[nodiscard]] friend constexpr GPSTime + operator-(const GPSTime& lhs, const TimeDelta& rhs) + { + auto out = lhs; + out -= rhs; + return out; + } + + /** Compute the difference between two GPSTime objects. */ + [[nodiscard]] friend constexpr TimeDelta + operator-(const GPSTime& lhs, const GPSTime& rhs) + { + return TimeDelta(lhs.time_point_ - rhs.time_point_); + } + + /** Compare two GPSTime objects. */ + [[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&) */ + [[nodiscard]] friend constexpr bool + operator!=(const GPSTime& lhs, const GPSTime& rhs) noexcept + { + return not(lhs == rhs); + } + + /** \copydoc operator==(const GPSTime&, const GPSTime&) */ + [[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&) */ + [[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&) */ + [[nodiscard]] friend constexpr bool + operator<=(const GPSTime& lhs, const GPSTime& rhs) noexcept + { + return not(lhs > rhs); + } + + /** \copydoc operator==(const GPSTime&, const GPSTime&) */ + [[nodiscard]] 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& t) + { + return os << std::string(t); + } + +private: + TimePoint 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 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)); + }; + + 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 unix_days = date::local_days(ymd); + + // Convert to days since GPS epoch (1980-01-06). + 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 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) + Microsecs(microsecond) + + Picosecs(picosecond); + + return TimePoint(gps_days) + since_midnight; + }()) +{} + +} // namespace caesar 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..9b0419a --- /dev/null +++ b/datetime/v1/caesar/datetime/timedelta.hpp @@ -0,0 +1,683 @@ +#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 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 static member 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; + + /** 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; + + /** + * 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 + [[nodiscard]] explicit constexpr + operator std::chrono::duration() const; + + /** Return the smallest representable TimeDelta. */ + [[nodiscard]] static constexpr TimeDelta + min() noexcept + { + return TimeDelta(Duration::min()); + } + + /** Return the largest representable TimeDelta. */ + [[nodiscard]] static constexpr TimeDelta + max() noexcept + { + return TimeDelta(Duration::max()); + } + + /** + * Return the smallest possible difference between non-equal TimeDelta + * objects. + */ + [[nodiscard]] static constexpr TimeDelta + resolution() noexcept + { + return TimeDelta(Duration(1)); + } + + /** + * 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 + * + * 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 + [[nodiscard]] 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. + * + * An hour is assumed to contain exactly 3600 seconds. + * + * \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 + [[nodiscard]] 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. + * + * A minute is assumed to contain exactly 60 seconds. + * + * \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 + [[nodiscard]] 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 + [[nodiscard]] 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 + [[nodiscard]] 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 + [[nodiscard]] 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 + [[nodiscard]] 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 + [[nodiscard]] static constexpr TimeDelta + picoseconds(T ps) + { + static_assert(is_arithmetic); + using Picosecs = std::chrono::duration; + return TimeDelta(Picosecs(ps)); + } + + /** Return the tick count. */ + [[nodiscard]] constexpr Rep + count() const noexcept + { + return duration_.count(); + } + + /** Return the total number of seconds in the duration. */ + [[nodiscard]] constexpr double + total_seconds() const + { + using Seconds = std::chrono::duration; + return Seconds(*this).count(); + } + + /** Returns a copy of the TimeDelta object. */ + [[nodiscard]] constexpr TimeDelta + operator+() const noexcept + { + return *this; + } + + /** Returns the negation of the TimeDelta object. */ + [[nodiscard]] 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. */ + [[nodiscard]] friend constexpr TimeDelta + operator+(const TimeDelta& lhs, const TimeDelta& rhs) + { + auto out = lhs; + out += rhs; + return out; + } + + /** Subtract a TimeDelta object from another. */ + [[nodiscard]] friend constexpr TimeDelta + operator-(const TimeDelta& lhs, const TimeDelta& rhs) + { + auto out = lhs; + out -= rhs; + return out; + } + + /** Multiply a TimeDelta object by a scalar. */ + [[nodiscard]] friend constexpr TimeDelta + operator*(const TimeDelta& lhs, Rep rhs) + { + auto out = lhs; + out *= rhs; + return out; + } + + /** \copydoc operator*(const TimeDelta&, Rep) */ + [[nodiscard]] 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. + */ + [[nodiscard]] 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. + */ + [[nodiscard]] 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()`. + */ + [[nodiscard]] friend constexpr TimeDelta + operator%(const TimeDelta& lhs, const TimeDelta& rhs) + { + return lhs % rhs.count(); + } + + /** Compare two TimeDelta objects. */ + [[nodiscard]] friend constexpr bool + operator==(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return lhs.duration_ == rhs.duration_; + } + + /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ + [[nodiscard]] friend constexpr bool + operator!=(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return not(lhs == rhs); + } + + /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ + [[nodiscard]] friend constexpr bool + operator<(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return lhs.duration_ < rhs.duration_; + } + + /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ + [[nodiscard]] friend constexpr bool + operator>(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return lhs.duration_ > rhs.duration_; + } + + /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ + [[nodiscard]] friend constexpr bool + operator<=(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return not(lhs > rhs); + } + + /** \copydoc operator==(const TimeDelta&, const TimeDelta&) */ + [[nodiscard]] friend constexpr bool + operator>=(const TimeDelta& lhs, const TimeDelta& rhs) noexcept + { + return not(lhs < rhs); + } + +private: + Duration duration_ = {}; +}; + +/** Return the absolute value of the input TimeDelta. */ +[[nodiscard]] constexpr TimeDelta +abs(const TimeDelta& dt) +{ + const auto d = TimeDelta::Duration(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&) + */ +[[nodiscard]] 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&) + */ +[[nodiscard]] 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&) + */ +[[nodiscard]] 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&) + */ +[[nodiscard]] 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) + : duration_([=]() { + static_assert(is_arithmetic); + + if constexpr (std::is_floating_point_v) { + // Convert from input tick period to floating-point picoseconds. + 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 Duration(r); + } else { + // For integral durations, we can just use duration_cast to + // convert directly. + 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 to convert + // directly. + return std::chrono::duration_cast(duration_); + } +} + +} // namespace caesar diff --git a/datetime/v1/test/datetime/gpstime_test.cpp b/datetime/v1/test/datetime/gpstime_test.cpp new file mode 100644 index 0000000..61a83ed --- /dev/null +++ b/datetime/v1/test/datetime/gpstime_test.cpp @@ -0,0 +1,437 @@ +#include + +#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.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(); + + 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 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); + + 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 new file mode 100644 index 0000000..21fe1c3 --- /dev/null +++ b/datetime/v1/test/datetime/timedelta_test.cpp @@ -0,0 +1,625 @@ +#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 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); +}; + +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_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_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(), -quintillion_years_sec); +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.max") +{ + const auto dt = cs::TimeDelta::max(); + CHECK_GT(dt.total_seconds(), quintillion_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_factory") +{ + 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_factory") +{ + 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_factory") +{ + 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_factory") +{ + 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_factory") +{ + 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_factory") +{ + 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_factory") +{ + 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_factory") +{ + 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_increment") + { + CHECK_EQ(++dt, one_picosec); + CHECK_EQ(dt, one_picosec); + } + + SUBCASE("postfix_increment") + { + CHECK_EQ(dt++, cs::TimeDelta()); + CHECK_EQ(dt, one_picosec); + } +} + +TEST_CASE_FIXTURE(TimeDeltaTest, "datetime.timedelta.decrement") +{ + cs::TimeDelta dt; + + SUBCASE("prefix_decrement") + { + CHECK_EQ(--dt, -one_picosec); + CHECK_EQ(dt, -one_picosec); + } + + SUBCASE("postfix_decrement") + { + 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); +} + +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); + } +} 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