diff --git a/google/cloud/spanner/interval.cc b/google/cloud/spanner/interval.cc index 5d91b99615b8a..926b1b4e88b0e 100644 --- a/google/cloud/spanner/interval.cc +++ b/google/cloud/spanner/interval.cc @@ -15,11 +15,13 @@ #include "google/cloud/spanner/interval.h" #include "google/cloud/internal/absl_str_cat_quiet.h" #include "google/cloud/internal/make_status.h" +#include "google/cloud/internal/time_utils.h" #include "absl/strings/ascii.h" #include "absl/strings/match.h" #include "absl/strings/numbers.h" #include "absl/strings/string_view.h" #include "absl/strings/strip.h" +#include "absl/time/time.h" #include #include #include @@ -335,6 +337,15 @@ StatusOr ParseInterval(std::string const& str) { return intvl; } +StatusOr LoadTimeZone(absl::string_view name) { + absl::TimeZone tz; + if (!absl::LoadTimeZone(name, &tz)) { + return internal::InvalidArgumentError( + absl::StrCat(name, ": Invalid time zone"), GCP_ERROR_INFO()); + } + return tz; +} + } // namespace bool operator==(Interval const& a, Interval const& b) { @@ -388,6 +399,35 @@ StatusOr MakeInterval(absl::string_view s) { return ParseInterval(absl::AsciiStrToLower(s)); } +StatusOr Add(Timestamp const& ts, Interval const& intvl, + absl::string_view time_zone) { + auto tz = LoadTimeZone(time_zone); + if (!tz) return tz.status(); + auto ci = tz->At(*ts.get()); + auto offset = absl::FromChrono(intvl.offset_); + auto seconds = absl::IDivDuration(offset, absl::Seconds(1), &offset); + auto cs = absl::CivilSecond( // add the civil-time parts + ci.cs.year(), ci.cs.month() + intvl.months_, ci.cs.day() + intvl.days_, + ci.cs.hour(), ci.cs.minute(), ci.cs.second() + seconds); + return *MakeTimestamp(internal::ToProtoTimestamp( // overflow saturates + absl::FromCivil(cs, *tz) + ci.subsecond + offset)); +} + +StatusOr Diff(Timestamp const& ts1, Timestamp const& ts2, + absl::string_view time_zone) { + auto tz = LoadTimeZone(time_zone); + if (!tz) return tz.status(); + auto ci1 = tz->At(*ts1.get()); + auto ci2 = tz->At(*ts2.get()); + auto days = absl::CivilDay(ci1.cs) - absl::CivilDay(ci2.cs); + auto offset = absl::Hours(ci1.cs.hour() - ci2.cs.hour()) + + absl::Minutes(ci1.cs.minute() - ci2.cs.minute()) + + absl::Seconds(ci1.cs.second() - ci2.cs.second()) + + (ci1.subsecond - ci2.subsecond); + return Interval(0, 0, static_cast(days), + absl::ToChronoNanoseconds(offset)); +} + GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace spanner } // namespace cloud diff --git a/google/cloud/spanner/interval.h b/google/cloud/spanner/interval.h index 629ffa3f45395..a6eb21ec760d7 100644 --- a/google/cloud/spanner/interval.h +++ b/google/cloud/spanner/interval.h @@ -15,6 +15,7 @@ #ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_INTERVAL_H #define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SPANNER_INTERVAL_H +#include "google/cloud/spanner/timestamp.h" #include "google/cloud/spanner/version.h" #include "google/cloud/status_or.h" #include "absl/strings/string_view.h" @@ -109,6 +110,9 @@ class Interval { std::chrono::nanoseconds offset) : months_(months), days_(days), offset_(offset) {} + friend StatusOr Add(Timestamp const&, Interval const&, + absl::string_view); + std::int32_t months_; std::int32_t days_; std::chrono::nanoseconds offset_; @@ -129,6 +133,23 @@ inline Interval operator/(Interval lhs, double rhs) { return lhs /= rhs; } */ StatusOr MakeInterval(absl::string_view); +/** + * Add the Interval to the Timestamp in the civil-time space defined by + * the time zone. Saturates the Timestamp result upon overflow. Returns + * an error status if the time zone cannot be loaded. + */ +StatusOr Add(Timestamp const&, Interval const&, + absl::string_view time_zone); + +/** + * Intervals constructed by subtracting two timestamps are partially + * justified, returning a whole number of days plus any remainder. A + * year/month value will never be present. Undefined on days overflow. + * Returns an error status if the time zone cannot be loaded. + */ +StatusOr Diff(Timestamp const&, Timestamp const&, + absl::string_view time_zone); + GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace spanner } // namespace cloud diff --git a/google/cloud/spanner/interval_test.cc b/google/cloud/spanner/interval_test.cc index 221ba4d9c230b..690fe464f6e18 100644 --- a/google/cloud/spanner/interval_test.cc +++ b/google/cloud/spanner/interval_test.cc @@ -13,6 +13,7 @@ // limitations under the License. #include "google/cloud/spanner/interval.h" +#include "google/cloud/spanner/timestamp.h" #include "google/cloud/testing_util/status_matchers.h" #include #include @@ -27,6 +28,7 @@ namespace spanner { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN namespace { +using ::google::cloud::testing_util::IsOkAndHolds; using ::google::cloud::testing_util::StatusIs; using std::chrono::duration; @@ -38,6 +40,10 @@ using std::chrono::minutes; using std::chrono::nanoseconds; using std::chrono::seconds; +Timestamp MakeTimestamp(std::string const& s) { + return spanner_internal::TimestampFromRFC3339(s).value(); +} + TEST(Interval, RegularSemantics) { Interval const intvl(0, 1, 2, hours(3)); @@ -247,6 +253,63 @@ TEST(Interval, OutputStreaming) { EXPECT_EQ("1 year 2 months 3 days 04:05:06.123456789", os.str()); } +TEST(Interval, TimestampOperations) { + char const* utc = "UTC"; + char const* nyc = "America/New_York"; + char const* bad = "Hyperborea/Gorinium"; + EXPECT_THAT(Add(Timestamp(), Interval(), bad), + StatusIs(StatusCode::kInvalidArgument)); + if (!Add(Timestamp(), Interval(), nyc)) GTEST_SKIP(); // probably Windows + + // Some simple cases of zero-length intervals. + EXPECT_THAT(Add(Timestamp(), Interval(), utc), IsOkAndHolds(Timestamp())); + EXPECT_THAT(Add(Timestamp(), Interval(), nyc), IsOkAndHolds(Timestamp())); + EXPECT_THAT(Diff(Timestamp(), Timestamp(), utc), IsOkAndHolds(Interval())); + EXPECT_THAT(Diff(Timestamp(), Timestamp(), nyc), IsOkAndHolds(Interval())); + + auto hms = [](int h, int m, int s) { + return hours(h) + minutes(m) + seconds(s); + }; + + // Over continuous civil-time segments, Timestamp/Interval operations + // behave in obvious ways. + EXPECT_THAT( // + Add(MakeTimestamp("2021-02-03T04:05:06.123456789Z"), + Interval(1, 2, 3, hms(4, 5, 6) + nanoseconds(999)), utc), + IsOkAndHolds(MakeTimestamp("2022-04-06T08:10:12.123457788Z"))); + EXPECT_THAT( + Diff(MakeTimestamp("2022-04-06T08:10:12.123457788Z"), + MakeTimestamp("2021-02-03T04:05:06.123456789Z"), utc), + IsOkAndHolds(Interval(0, 0, 427, hms(4, 5, 6) + nanoseconds(999)))); + + // If we cross a Feb 29 there is an extra day. + EXPECT_THAT( // + Add(MakeTimestamp("2020-02-03T04:05:06.123456789Z"), + Interval(1, 2, 3, hms(4, 5, 6) + nanoseconds(999)), utc), + IsOkAndHolds(MakeTimestamp("2021-04-06T08:10:12.123457788Z"))); + EXPECT_THAT( + Diff(MakeTimestamp("2021-04-06T08:10:12.123457788Z"), + MakeTimestamp("2020-02-03T04:05:06.123456789Z"), utc), + IsOkAndHolds(Interval(0, 0, 428, hms(4, 5, 6) + nanoseconds(999)))); + + // Over civil-time discontinuities, two civil hours is either one absolute + // hour (skipped) or three absolute hours (repeated). + EXPECT_THAT(Add(MakeTimestamp("2023-03-12T01:02:03.456789-05:00"), + Interval(0, 0, 0, hours(2)), nyc), + IsOkAndHolds(MakeTimestamp("2023-03-12T03:02:03.456789-04:00"))); + auto ts1 = MakeTimestamp("2023-03-12T03:02:03.456789-04:00"); + auto ts2 = MakeTimestamp("2023-03-12T01:02:03.456789-05:00"); + EXPECT_EQ(*ts1.get() - *ts2.get(), absl::Hours(1)); + EXPECT_THAT(Diff(ts1, ts2, nyc), IsOkAndHolds(Interval(hours(2)))); + EXPECT_THAT(Add(MakeTimestamp("2023-11-05T01:02:03.456789-04:00"), + Interval(0, 0, 0, hours(2)), nyc), + IsOkAndHolds(MakeTimestamp("2023-11-05T03:02:03.456789-05:00"))); + ts1 = MakeTimestamp("2023-11-05T03:02:03.456789-05:00"); + ts2 = MakeTimestamp("2023-11-05T01:02:03.456789-04:00"); + EXPECT_EQ(*ts1.get() - *ts2.get(), absl::Hours(3)); + EXPECT_THAT(Diff(ts1, ts2, nyc), IsOkAndHolds(Interval(hours(2)))); +} + } // namespace GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace spanner