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 &parameters);
+
+  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 &parameters);
-
-  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}));
+}