Skip to content

Commit

Permalink
feat(spanner): add Interval justification (#14123)
Browse files Browse the repository at this point in the history
Add APIs to flow Interval 24-hour offsets into days, and 30-day periods
into months.  Seeing as "justification" is included in the relational
operators, to best way to verify the results is by conversion to a
string.

And now that we have `JustifyHours(intvl)`, apply it to the result of
`Diff(ts1, ts2, tz)` as required.
  • Loading branch information
devbww authored May 4, 2024
1 parent 4355156 commit a3488c4
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 6 deletions.
33 changes: 27 additions & 6 deletions google/cloud/spanner/interval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,6 @@ std::string SerializeInterval(std::int32_t months, std::int32_t days,
std::ostringstream ss;
std::int32_t years = months / 12;
months %= 12;
if (months < 0) {
months += 12;
years -= 1;
}
auto plural = [](std::int32_t v) { return std::abs(v) == 1 ? "" : "s"; };
char const* sep = "";
if (years != 0) {
Expand Down Expand Up @@ -399,6 +395,31 @@ StatusOr<Interval> MakeInterval(absl::string_view s) {
return ParseInterval(absl::AsciiStrToLower(s));
}

Interval JustifyDays(Interval intvl) {
intvl.months_ += intvl.days_ / 30;
intvl.days_ %= 30;
if (intvl.days_ < 0) {
intvl.days_ += 30;
intvl.months_ -= 1;
}
return intvl;
}

Interval JustifyHours(Interval intvl) {
auto days = intvl.offset_ / hours(24);
intvl.days_ += static_cast<std::int32_t>(days);
intvl.offset_ -= hours(days * 24);
if (intvl.offset_ < nanoseconds::zero()) {
intvl.offset_ += hours(24);
intvl.days_ -= 1;
}
return intvl;
}

Interval JustifyInterval(Interval intvl) {
return JustifyDays(JustifyHours(intvl));
}

StatusOr<Timestamp> Add(Timestamp const& ts, Interval const& intvl,
absl::string_view time_zone) {
auto tz = LoadTimeZone(time_zone);
Expand All @@ -423,8 +444,8 @@ StatusOr<Interval> Diff(Timestamp const& ts1, Timestamp const& ts2,
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<std::int32_t>(days),
absl::ToChronoNanoseconds(offset));
return JustifyHours(Interval(0, 0, static_cast<std::int32_t>(days),
absl::ToChronoNanoseconds(offset)));
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
Expand Down
20 changes: 20 additions & 0 deletions google/cloud/spanner/interval.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ class Interval {
std::chrono::nanoseconds offset)
: months_(months), days_(days), offset_(offset) {}

friend Interval JustifyDays(Interval);
friend Interval JustifyHours(Interval);
friend StatusOr<Timestamp> Add(Timestamp const&, Interval const&,
absl::string_view);

Expand All @@ -133,6 +135,24 @@ inline Interval operator/(Interval lhs, double rhs) { return lhs /= rhs; }
*/
StatusOr<Interval> MakeInterval(absl::string_view);

/**
* Adjust the interval so that 30-day periods are represented as months.
* For example, maps "35 days" to "1 month 5 days".
*/
Interval JustifyDays(Interval);

/**
* Adjust the interval so that 24-hour periods are represented as days.
* For example, maps "27 hours" to "1 day 3 hours".
*/
Interval JustifyHours(Interval);

/**
* Adjust the interval using both JustifyDays() and JustifyHours().
* For example, maps "1 month -1 hour" to "29 days 23 hours".
*/
Interval JustifyInterval(Interval);

/**
* Add the Interval to the Timestamp in the civil-time space defined by
* the time zone. Saturates the Timestamp result upon overflow. Returns
Expand Down
25 changes: 25 additions & 0 deletions google/cloud/spanner/interval_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,23 @@ TEST(Interval, OutputStreaming) {
EXPECT_EQ("1 year 2 months 3 days 04:05:06.123456789", os.str());
}

TEST(Interval, Justification) {
EXPECT_EQ(std::string(Interval(0, 0, 35)), "35 days");
EXPECT_EQ(std::string(JustifyDays(Interval(0, 0, 35))), "1 month 5 days");
EXPECT_EQ(std::string(Interval(0, 0, -35)), "-35 days");
EXPECT_EQ(std::string(JustifyDays(Interval(0, 0, -35))), "-2 months 25 days");

EXPECT_EQ(std::string(Interval(hours(27))), "27:00:00");
EXPECT_EQ(std::string(JustifyHours(Interval(hours(27)))), //
"1 day 03:00:00");
EXPECT_EQ(std::string(Interval(-hours(27))), "-27:00:00");
EXPECT_EQ(std::string(JustifyHours(Interval(-hours(27)))),
"-2 days 21:00:00");

EXPECT_EQ(std::string(JustifyInterval(Interval(0, 1, 0, -hours(1)))),
"29 days 23:00:00");
}

TEST(Interval, TimestampOperations) {
char const* utc = "UTC";
char const* nyc = "America/New_York";
Expand Down Expand Up @@ -308,6 +325,14 @@ TEST(Interval, TimestampOperations) {
ts2 = MakeTimestamp("2023-11-05T01:02:03.456789-04:00");
EXPECT_EQ(*ts1.get<absl::Time>() - *ts2.get<absl::Time>(), absl::Hours(25));
EXPECT_THAT(Diff(ts1, ts2, nyc), IsOkAndHolds(Interval(0, 0, 1)));

// Subtracting timestamps should return an interval with justified hours.
auto intvl = Diff(MakeTimestamp("2001-09-29T03:00:00Z"),
MakeTimestamp("2001-07-27T12:00:00Z"), utc);
EXPECT_STATUS_OK(intvl);
if (intvl) {
EXPECT_EQ(std::string(*intvl), "63 days 15:00:00");
}
}

// A miscellaneous bunch of Interval tests that come from the examples
Expand Down

0 comments on commit a3488c4

Please sign in to comment.