From 086fc4776994906258e25e70ce1f9faefebf3fcf Mon Sep 17 00:00:00 2001 From: Kostas Kyrimis <kostaskyrim@gmail.com> Date: Fri, 20 Aug 2021 14:14:45 +0300 Subject: [PATCH] Define subtraction and addition for temporal types (#209) --- src/utils/temporal.cpp | 17 +++ src/utils/temporal.hpp | 218 +++++++++++++++++++++++++--------- tests/unit/utils_temporal.cpp | 203 +++++++++++++++++++++++++++++++ 3 files changed, 384 insertions(+), 54 deletions(-) diff --git a/src/utils/temporal.cpp b/src/utils/temporal.cpp index 6b2981f30..33d1904ac 100644 --- a/src/utils/temporal.cpp +++ b/src/utils/temporal.cpp @@ -3,6 +3,7 @@ #include <charconv> #include <chrono> #include <ctime> +#include <limits> #include <string_view> #include "utils/exceptions.hpp" @@ -717,6 +718,11 @@ int64_t Duration::Months() const { return std::chrono::duration_cast<std::chrono::months>(ms).count(); } +int64_t Duration::Days() const { + std::chrono::microseconds ms(microseconds); + return std::chrono::duration_cast<std::chrono::days>(ms).count(); +} + int64_t Duration::SubMonthsAsDays() const { namespace chrono = std::chrono; const auto months = chrono::months(Months()); @@ -732,6 +738,14 @@ int64_t Duration::SubDaysAsSeconds() const { return chrono::duration_cast<chrono::seconds>(micros - months - days).count(); } +int64_t Duration::SubDaysAsMicroseconds() const { + namespace chrono = std::chrono; + const auto months = chrono::months(Months()); + const auto days = chrono::days(SubMonthsAsDays()); + const auto micros = chrono::microseconds(microseconds); + return (micros - months - days).count(); +} + int64_t Duration::SubSecondsAsNanoseconds() const { namespace chrono = std::chrono; const auto months = chrono::months(Months()); @@ -742,6 +756,9 @@ int64_t Duration::SubSecondsAsNanoseconds() const { } Duration Duration::operator-() const { + if (microseconds == std::numeric_limits<decltype(microseconds)>::min()) [[unlikely]] { + throw utils::BasicException("Duration arithmetic overflows"); + } Duration result{-microseconds}; return result; } diff --git a/src/utils/temporal.hpp b/src/utils/temporal.hpp index fea3ee3cd..10329ca8a 100644 --- a/src/utils/temporal.hpp +++ b/src/utils/temporal.hpp @@ -1,9 +1,9 @@ #pragma once -#include <cstdint> - #include <chrono> +#include <cstdint> #include <iostream> +#include <limits> #include "fmt/format.h" #include "utils/exceptions.hpp" @@ -24,6 +24,88 @@ constexpr auto GetAndSubtractDuration(TSecond &base_duration) { return duration.count(); } +template <typename TType> +bool Overflows(const TType &lhs, const TType &rhs) { + if (lhs > 0 && rhs > 0 && lhs > (std::numeric_limits<TType>::max() - rhs)) [[unlikely]] { + return true; + } + return false; +} + +template <typename TType> +bool Underflows(const TType &lhs, const TType &rhs) { + if (lhs < 0 && rhs < 0 && lhs < (std::numeric_limits<TType>::min() - rhs)) [[unlikely]] { + return true; + } + return false; +} + +struct DurationParameters { + double years{0}; + double months{0}; + double days{0}; + double hours{0}; + double minutes{0}; + double seconds{0}; + double milliseconds{0}; + double microseconds{0}; +}; + +DurationParameters ParseDurationParameters(std::string_view string); + +struct Date; +struct LocalTime; +struct LocalDateTime; + +struct Duration { + explicit Duration(int64_t microseconds); + explicit Duration(const DurationParameters ¶meters); + + auto operator<=>(const Duration &) const = default; + + int64_t Months() const; + int64_t Days() const; + int64_t SubMonthsAsDays() const; + int64_t SubDaysAsSeconds() const; + int64_t SubDaysAsMicroseconds() const; + int64_t SubSecondsAsNanoseconds() const; + + friend std::ostream &operator<<(std::ostream &os, const Duration &dur) { + // ISO 8601 extended format: P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]. + namespace chrono = std::chrono; + auto micros = chrono::microseconds(dur.microseconds); + const auto y = GetAndSubtractDuration<chrono::years>(micros); + const auto mo = GetAndSubtractDuration<chrono::months>(micros); + const auto dd = GetAndSubtractDuration<chrono::days>(micros); + const auto h = GetAndSubtractDuration<chrono::hours>(micros); + const auto m = GetAndSubtractDuration<chrono::minutes>(micros); + const auto s = GetAndSubtractDuration<chrono::seconds>(micros); + return os << fmt::format("P{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}.{:0>6}", y, mo, dd, h, m, s, micros.count()); + } + + Duration operator-() const; + + friend Duration operator+(const Duration &lhs, const Duration rhs) { + if (Overflows(lhs.microseconds, rhs.microseconds)) { + throw utils::BasicException("Duration arithmetic overflows"); + } + + if (Underflows(lhs.microseconds, rhs.microseconds)) { + throw utils::BasicException("Duration arithmetic underflows"); + } + + return Duration(lhs.microseconds + rhs.microseconds); + } + + friend Duration operator-(const Duration &lhs, const Duration rhs) { return lhs + (-rhs); } + + int64_t microseconds; +}; + +struct DurationHash { + size_t operator()(const Duration &duration) const; +}; + struct DateParameters { int64_t years{0}; int64_t months{1}; @@ -35,6 +117,19 @@ struct DateParameters { // boolean indicates whether the parsed string was in extended format std::pair<DateParameters, bool> ParseDateParameters(std::string_view date_string); +constexpr std::chrono::year_month_day ToChronoYMD(uint16_t years, uint8_t months, uint8_t days) { + namespace chrono = std::chrono; + return chrono::year_month_day(chrono::year(years), chrono::month(months), chrono::day(days)); +} + +constexpr std::chrono::sys_days ToChronoSysDaysYMD(uint16_t years, uint8_t months, uint8_t days) { + return std::chrono::sys_days(ToChronoYMD(years, months, days)); +} + +constexpr std::chrono::days DaysSinceEpoch(uint16_t years, uint8_t months, uint8_t days) { + return ToChronoSysDaysYMD(years, months, days).time_since_epoch(); +} + struct Date { explicit Date() : Date{DateParameters{}} {} // we assume we accepted date in microseconds which was normilized using the epoch time point @@ -49,6 +144,26 @@ struct Date { int64_t MicrosecondsSinceEpoch() const; int64_t DaysSinceEpoch() const; + friend Date operator+(const Date &date, const Duration &dur) { + namespace chrono = std::chrono; + const auto date_as_duration = Duration(date.MicrosecondsSinceEpoch()); + const auto result = date_as_duration + dur; + const auto ymd = chrono::year_month_day(chrono::sys_days(chrono::days(result.Days()))); + return Date({static_cast<int>(ymd.year()), static_cast<unsigned>(ymd.month()), static_cast<unsigned>(ymd.day())}); + } + + friend Date operator+(const Duration &dur, const Date &date) { return date + dur; } + + friend Date operator-(const Date &date, const Duration &dur) { return date + (-dur); } + + friend Duration operator-(const Date &lhs, const Date &rhs) { + namespace chrono = std::chrono; + const auto lhs_days = utils::DaysSinceEpoch(lhs.years, lhs.months, lhs.days); + const auto rhs_days = utils::DaysSinceEpoch(rhs.years, rhs.months, rhs.days); + const auto days_elapsed = lhs_days - rhs_days; + return Duration(chrono::duration_cast<chrono::microseconds>(days_elapsed).count()); + } + auto operator<=>(const Date &) const = default; uint16_t years; @@ -95,6 +210,34 @@ struct LocalTime { static_cast<int>(lt.seconds), subseconds.count()); } + friend LocalTime operator+(const LocalTime &local_time, const Duration &dur) { + namespace chrono = std::chrono; + auto rhs = dur.SubDaysAsMicroseconds(); + auto abs = [](auto value) { return (value >= 0) ? value : -value; }; + const auto lhs = local_time.MicrosecondsSinceEpoch(); + if (rhs < 0 && lhs < abs(rhs)) { + constexpr int64_t one_day_in_microseconds = 24LL * 60 * 60 * 1000 * 1000; + rhs = one_day_in_microseconds + rhs; + } + auto result = chrono::microseconds(lhs + rhs); + const auto h = GetAndSubtractDuration<chrono::hours>(result) % 24; + const auto m = GetAndSubtractDuration<chrono::minutes>(result); + const auto s = GetAndSubtractDuration<chrono::seconds>(result); + const auto milli = GetAndSubtractDuration<chrono::milliseconds>(result); + const auto micro = result.count(); + return LocalTime(LocalTimeParameters{h, m, s, milli, micro}); + } + + friend LocalTime operator+(const Duration &dur, const LocalTime &local_time) { return local_time + dur; } + + friend LocalTime operator-(const LocalTime &local_time, const Duration &duration) { return local_time + (-duration); } + + friend Duration operator-(const LocalTime &lhs, const LocalTime &rhs) { + Duration lhs_dur(lhs.MicrosecondsSinceEpoch()); + Duration rhs_dur(rhs.MicrosecondsSinceEpoch()); + return lhs_dur - rhs_dur; + } + uint8_t hours; uint8_t minutes; uint8_t seconds; @@ -124,6 +267,25 @@ struct LocalDateTime { return os; } + friend LocalDateTime operator+(const LocalDateTime &dt, const Duration &dur) { + const auto local_date_time_as_duration = Duration(dt.MicrosecondsSinceEpoch()); + const auto result = local_date_time_as_duration + dur; + namespace chrono = std::chrono; + const auto ymd = chrono::year_month_day(chrono::sys_days(chrono::days(result.Days()))); + const auto date_part = + Date({static_cast<int>(ymd.year()), static_cast<unsigned>(ymd.month()), static_cast<unsigned>(ymd.day())}); + const auto local_time_part = LocalTime(result.SubDaysAsMicroseconds()); + return LocalDateTime(date_part, local_time_part); + } + + friend LocalDateTime operator+(const Duration &dur, const LocalDateTime &dt) { return dt + dur; } + + friend LocalDateTime operator-(const LocalDateTime &dt, const Duration &dur) { return dt + (-dur); } + + friend Duration operator-(const LocalDateTime &lhs, const LocalDateTime &rhs) { + return Duration(lhs.MicrosecondsSinceEpoch()) - Duration(rhs.MicrosecondsSinceEpoch()); + } + Date date; LocalTime local_time; }; @@ -132,58 +294,6 @@ struct LocalDateTimeHash { size_t operator()(const LocalDateTime &local_date_time) const; }; -struct DurationParameters { - double years{0}; - double months{0}; - double days{0}; - double hours{0}; - double minutes{0}; - double seconds{0}; - double milliseconds{0}; - double microseconds{0}; -}; - -DurationParameters ParseDurationParameters(std::string_view string); - -struct Duration { - explicit Duration(int64_t microseconds); - explicit Duration(const DurationParameters ¶meters); - - auto operator<=>(const Duration &) const = default; - - int64_t Months() const; - int64_t SubMonthsAsDays() const; - int64_t SubDaysAsSeconds() const; - int64_t SubSecondsAsNanoseconds() const; - - friend std::ostream &operator<<(std::ostream &os, const Duration &dur) { - // ISO 8601 extended format: P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]. - namespace chrono = std::chrono; - auto micros = chrono::microseconds(dur.microseconds); - const auto y = GetAndSubtractDuration<chrono::years>(micros); - const auto mo = GetAndSubtractDuration<chrono::months>(micros); - const auto dd = GetAndSubtractDuration<chrono::days>(micros); - const auto h = GetAndSubtractDuration<chrono::hours>(micros); - const auto m = GetAndSubtractDuration<chrono::minutes>(micros); - const auto s = GetAndSubtractDuration<chrono::seconds>(micros); - return os << fmt::format("P{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}.{:0>6}", y, mo, dd, h, m, s, micros.count()); - } - - Duration operator-() const; - - int64_t microseconds; -}; - -struct DurationHash { - size_t operator()(const Duration &duration) const; -}; - -constexpr std::chrono::days DaysSinceEpoch(uint16_t years, uint8_t months, uint8_t days) { - namespace chrono = std::chrono; - const auto ymd = chrono::year_month_day(chrono::year(years), chrono::month(months), chrono::day(days)); - return chrono::sys_days{ymd}.time_since_epoch(); -} - Date UtcToday(); LocalTime UtcLocalTime(); LocalDateTime UtcLocalDateTime(); diff --git a/tests/unit/utils_temporal.cpp b/tests/unit/utils_temporal.cpp index d1cbf174b..8d53ed879 100644 --- a/tests/unit/utils_temporal.cpp +++ b/tests/unit/utils_temporal.cpp @@ -1,5 +1,6 @@ #include <chrono> #include <iostream> +#include <limits> #include <optional> #include <sstream> @@ -343,3 +344,205 @@ TEST(TemporalTest, PrintLocalDateTime) { ASSERT_TRUE(stream); ASSERT_EQ(stream.view(), "1970-01-01T13:02:40.100050"); } + +TEST(TemporalTest, DurationAddition) { + // a >= 0 && b >= 0 + const auto zero = utils::Duration(0); + const auto one = utils::Duration(1); + const auto two = one + one; + const auto four = two + two; + ASSERT_EQ(two.microseconds, 2); + ASSERT_EQ(four.microseconds, 4); + const auto max = utils::Duration(std::numeric_limits<int64_t>::max()); + ASSERT_THROW(max + one, utils::BasicException); + ASSERT_EQ(zero + zero, zero); + ASSERT_EQ(max + zero, max); + + // a < 0 && b < 0 + const auto neg_one = -one; + const auto neg_two = neg_one + neg_one; + const auto neg_four = neg_two + neg_two; + ASSERT_EQ(neg_two.microseconds, -2); + ASSERT_EQ(neg_four.microseconds, -4); + const auto min = utils::Duration(std::numeric_limits<int64_t>::min()); + ASSERT_THROW(min + neg_one, utils::BasicException); + ASSERT_EQ(min + zero, min); + + // a < 0, b > 0 && a > 0 , b < 0 + ASSERT_EQ(neg_one + one, zero); + ASSERT_EQ(neg_two + one, neg_one); + ASSERT_EQ(neg_two + four, two); + ASSERT_EQ(four + neg_two, two); + + // a {min, max} && b {min, max} + ASSERT_EQ(min + max, neg_one); + ASSERT_EQ(max + min, neg_one); + ASSERT_THROW(min + min, utils::BasicException); + ASSERT_THROW(max + max, utils::BasicException); +} + +TEST(TemporalTest, DurationSubtraction) { + // a >= 0 && b >= 0 + const auto one = utils::Duration(1); + const auto two = one + one; + const auto zero = one - one; + ASSERT_EQ(zero.microseconds, 0); + const auto neg_one = zero - one; + const auto neg_two = zero - two; + ASSERT_EQ(neg_one.microseconds, -1); + ASSERT_EQ(neg_two.microseconds, -2); + const auto max = utils::Duration(std::numeric_limits<int64_t>::max()); + const auto min_minus_one = utils::Duration(std::numeric_limits<int64_t>::min() + 1); + ASSERT_EQ(max - zero, max); + ASSERT_EQ(zero - max, min_minus_one); + + // a < 0 && b < 0 + ASSERT_EQ(neg_two - neg_two, zero); + const auto min = utils::Duration(std::numeric_limits<int64_t>::min()); + ASSERT_THROW(min - one, utils::BasicException); + ASSERT_EQ(min - zero, min); + + // a < 0, b > 0 && a > 0 , b < 0 + ASSERT_EQ(neg_one - one, neg_two); + ASSERT_EQ(one - neg_one, two); + const auto neg_three = utils::Duration(-3); + const auto three = -neg_three; + ASSERT_EQ(neg_two - one, neg_three); + ASSERT_EQ(one - neg_two, three); + + // a {min, max} && b {min, max} + ASSERT_THROW(min - max, utils::BasicException); + ASSERT_THROW(max - min, utils::BasicException); + // There is no positive representation of min + ASSERT_THROW(min - min, utils::BasicException); + ASSERT_EQ(max - max, zero); +} + +TEST(TemporalTest, LocalTimeAndDurationAddition) { + const auto half_past_one = utils::LocalTime({1, 30, 10}); + const auto three = half_past_one + utils::Duration({1994, 2, 10, 1, 30, 2, 22, 45}); + const auto three_symmetrical = utils::Duration({1994, 2, 10, 1, 30, 2, 22, 45}) + half_past_one; + ASSERT_EQ(three, utils::LocalTime({3, 0, 12, 22, 45})); + ASSERT_EQ(three_symmetrical, utils::LocalTime({3, 0, 12, 22, 45})); + + const auto half_an_hour_before_midnight = utils::LocalTime({23, 30, 10}); + { + const auto half_past_midnight = half_an_hour_before_midnight + utils::Duration({1994, 1, 10, 1}); + ASSERT_EQ(half_past_midnight, utils::LocalTime({.minutes = 30, .seconds = 10})); + } + const auto identity = half_an_hour_before_midnight + utils::Duration({.days = 1}); + ASSERT_EQ(identity, half_an_hour_before_midnight); + ASSERT_EQ(identity, half_an_hour_before_midnight + utils::Duration({.days = 1, .hours = 24})); + const auto an_hour_and_a_half_before_midnight = utils::LocalTime({22, 30, 10}); + ASSERT_EQ(half_an_hour_before_midnight + utils::Duration({.hours = 23}), an_hour_and_a_half_before_midnight); + + const auto minus_one_hour = utils::Duration({-1994, -2, -10, -1, 0, 0, -20, -20}); + const auto minus_one_hour_exact = utils::Duration({-1994, -2, -10, -1}); + { + const auto half_past_midnight = half_past_one + minus_one_hour; + ASSERT_EQ(half_past_midnight, utils::LocalTime({0, 30, 9, 979, 980})); + ASSERT_EQ(half_past_midnight + minus_one_hour_exact, utils::LocalTime({23, 30, 9, 979, 980})); + + const auto minus_two_hours_thirty_mins = utils::Duration({-1994, -2, -10, -2, -30, -9}); + ASSERT_EQ(half_past_midnight + minus_two_hours_thirty_mins, utils::LocalTime({22, 0, 0, 979, 980})); + + ASSERT_NO_THROW(half_past_midnight + (utils::Duration(std::numeric_limits<int64_t>::max()))); + ASSERT_EQ(half_past_midnight + (utils::Duration(std::numeric_limits<int64_t>::max())), + utils::LocalTime({0, 22, 40, 755, 787})); + ASSERT_NO_THROW(half_past_midnight + (utils::Duration(std::numeric_limits<int64_t>::min()))); + ASSERT_EQ(half_past_midnight + (utils::Duration(std::numeric_limits<int64_t>::min())), + utils::LocalTime({0, 37, 39, 204, 172})); + } +} + +TEST(TemporalTest, LocalTimeAndDurationSubtraction) { + const auto half_past_one = utils::LocalTime({1, 30, 10}); + const auto midnight = half_past_one - utils::Duration({1994, 2, 10, 1, 30, 10}); + ASSERT_EQ(midnight, utils::LocalTime()); + ASSERT_EQ(midnight - utils::Duration({-1994, -2, -10, -1, -30, -10}), utils::LocalTime({1, 30, 10})); + + const auto almost_an_hour_and_a_half_before_midnight = midnight - utils::Duration({1994, 2, 10, 1, 30, 1, 20, 20}); + ASSERT_EQ(almost_an_hour_and_a_half_before_midnight, utils::LocalTime({22, 29, 58, 979, 980})); + + ASSERT_NO_THROW(midnight - (utils::Duration(std::numeric_limits<int64_t>::max()))); + ASSERT_EQ(midnight - (utils::Duration(std::numeric_limits<int64_t>::max())), utils::LocalTime({0, 7, 29, 224, 193})); + ASSERT_THROW(midnight - utils::Duration(std::numeric_limits<int64_t>::min()), utils::BasicException); +} + +TEST(TemporalTest, LocalTimeDeltaDuration) { + const auto half_past_one = utils::LocalTime({1, 30, 10}); + const auto half_past_two = utils::LocalTime({2, 30, 10}); + const auto an_hour_negative = half_past_one - half_past_two; + ASSERT_EQ(an_hour_negative, utils::Duration({.hours = -1})); + const auto an_hour = half_past_two - half_past_one; + ASSERT_EQ(an_hour, utils::Duration({.hours = 1})); +} + +TEST(TemporalTest, DateAddition) { + const auto unix_epoch = utils::Date({1970, 1, 1}); + const auto one_day_after_unix_epoch = unix_epoch + utils::Duration({.days = 1}); + const auto one_day_after_unix_epoch_symmetrical = utils::Duration({.days = 1}) + unix_epoch; + ASSERT_EQ(one_day_after_unix_epoch, utils::Date({1970, 1, 2})); + ASSERT_EQ(one_day_after_unix_epoch_symmetrical, one_day_after_unix_epoch); + + const auto one_month_after_unix_epoch = unix_epoch + utils::Duration({.days = 31}); + ASSERT_EQ(one_month_after_unix_epoch, utils::Date({1970, 2, 1})); + + const auto one_year_after_unix_epoch = unix_epoch + utils::Duration({.days = 365}); + ASSERT_EQ(one_year_after_unix_epoch, utils::Date({1971, 1, 1})); + + const auto last_day_of_unix_epoch = one_year_after_unix_epoch + utils::Duration({.days = -1}); + ASSERT_EQ(last_day_of_unix_epoch, utils::Date({1970, 12, 31})); + + const auto one_day_before_unix_epoch = unix_epoch + utils::Duration({.days = -1}); + ASSERT_EQ(one_day_before_unix_epoch, utils::Date({1969, 12, 31})); + + ASSERT_EQ(last_day_of_unix_epoch + utils::Duration({.days = -31}), utils::Date({1970, 11, 30})); + ASSERT_THROW(unix_epoch + utils::Duration(std::numeric_limits<int64_t>::max()), utils::BasicException); + ASSERT_THROW(unix_epoch + utils::Duration(std::numeric_limits<int64_t>::min()), utils::BasicException); +} + +TEST(TemporalTest, DateSubstraction) { + const auto day_after_unix_epoch = utils::Date({1970, 1, 2}); + const auto unix_epoch = day_after_unix_epoch - utils::Duration({.days = 1}); + ASSERT_EQ(unix_epoch, utils::Date({1970, 1, 1})); + ASSERT_EQ(utils::Date({1971, 1, 1}) - utils::Duration({.days = 1}), utils::Date({1970, 12, 31})); + ASSERT_EQ(utils::Date({1971, 1, 1}) - utils::Duration({.days = -1}), utils::Date({1971, 1, 2})); + ASSERT_THROW(unix_epoch - utils::Duration(std::numeric_limits<int64_t>::max()), utils::BasicException); + ASSERT_THROW(unix_epoch - utils::Duration(std::numeric_limits<int64_t>::min()), utils::BasicException); +} + +TEST(TemporalTest, DateDelta) { + const auto unix_epoch = utils::Date({1970, 1, 1}); + const auto one_year_after_unix_epoch = utils::Date({1971, 1, 1}); + ASSERT_EQ(one_year_after_unix_epoch - unix_epoch, utils::Duration({.days = 365})); + ASSERT_EQ(unix_epoch - one_year_after_unix_epoch, utils::Duration({.days = -365})); +} + +TEST(TemporalTest, LocalDateTimeAdditionSubtraction) { + const auto unix_epoch = utils::LocalDateTime({1970, 1, 1}, {.hours = 12}); + auto one_day_after_unix_epoch = unix_epoch + utils::Duration({.hours = 24}); + auto one_day_after_unix_epoch_symmetrical = utils::Duration({.hours = 24}) + unix_epoch; + ASSERT_EQ(one_day_after_unix_epoch, utils::LocalDateTime({1970, 1, 2}, {.hours = 12})); + ASSERT_EQ(one_day_after_unix_epoch_symmetrical, one_day_after_unix_epoch); + + one_day_after_unix_epoch = unix_epoch + utils::Duration({.days = 1}); + ASSERT_EQ(one_day_after_unix_epoch, utils::LocalDateTime({1970, 1, 2}, {.hours = 12})); + + ASSERT_EQ(one_day_after_unix_epoch + utils::Duration({.days = -1}), unix_epoch); + ASSERT_EQ(one_day_after_unix_epoch - utils::Duration({.days = 1}), unix_epoch); + ASSERT_THROW(one_day_after_unix_epoch + utils::Duration(std::numeric_limits<int64_t>::max()), utils::BasicException); + ASSERT_THROW(one_day_after_unix_epoch + utils::Duration(std::numeric_limits<int64_t>::min()), utils::BasicException); + ASSERT_THROW(one_day_after_unix_epoch - utils::Duration(std::numeric_limits<int64_t>::max()), utils::BasicException); + ASSERT_THROW(one_day_after_unix_epoch - utils::Duration(std::numeric_limits<int64_t>::min()), utils::BasicException); +} + +TEST(TemporalTest, LocalDateTimeDelta) { + const auto unix_epoch = utils::LocalDateTime({1970, 1, 1}, {1, 1, 1}); + const auto one_year_after_unix_epoch = utils::LocalDateTime({1971, 2, 1}, {12, 1, 1}); + const auto two_years_after_unix_epoch = utils::LocalDateTime({1972, 2, 1}, {1, 1, 1, 20, 34}); + ASSERT_EQ(one_year_after_unix_epoch - unix_epoch, utils::Duration({.days = 396, .hours = 11})); + ASSERT_EQ(unix_epoch - one_year_after_unix_epoch, utils::Duration({.days = -396, .hours = -11})); + ASSERT_EQ(two_years_after_unix_epoch - unix_epoch, + utils::Duration({.days = 761, .milliseconds = 20, .microseconds = 34})); +}