PR #2109: carry field normalization into parsed year in ParseYearAnd Imported from GitHub PR https://github.com/abseil/abseil-cpp/pull/2109 **Dropped year carry in ParseCivilTime** `ParseYearAnd` parses the year on its own and normalizes it into the ~2400 range before handing the rest of the string to `ParseTime`, then rebuilds the result from the original year plus the normalized month..second fields taken from `cs`. When one of those fields carries across the year boundary (a `:60` leap second at `23:59:60` on Dec 31, or an end-of-year rollover), that carry lives in `cs.year()` and was dropped, so `ParseCivilTime("2020-12-31T23:59:60")` produced `2020-01-01T00:00:00` while constructing `CivilSecond(2020, 12, 31, 23, 59, 60)` directly gives `2021-01-01T00:00:00`. This reapplies the same carry to the original year, which is a no-op whenever no rollover happened. Merge af31d66e1109de628f4e1ffb4f760b27ec7f39ca into 63c77e4060d6b4aefb9d85f9205c2d109d5fd9b8 Merging this change closes #2109 COPYBARA_INTEGRATE_REVIEW=https://github.com/abseil/abseil-cpp/pull/2109 from nabhan06:civil-time-year-carry af31d66e1109de628f4e1ffb4f760b27ec7f39ca PiperOrigin-RevId: 949723541 Change-Id: I015bcd78575306136677e3dfc1f478066e2dca71
diff --git a/absl/time/civil_time.cc b/absl/time/civil_time.cc index 1773366..eae7e7f 100644 --- a/absl/time/civil_time.cc +++ b/absl/time/civil_time.cc
@@ -55,13 +55,20 @@ const civil_year_t y = std::strtoll(np, &endp, 10); // NOLINT(runtime/deprecated_fn) if (endp == np || errno == ERANGE) return false; - const std::string norm = StrCat(NormalizeYear(y), endp); + const civil_year_t normalized_year = NormalizeYear(y); + const std::string norm = StrCat(normalized_year, endp); const TimeZone utc = UTCTimeZone(); Time t; if (ParseTime(StrCat("%Y", fmt), norm, utc, &t, nullptr)) { const auto cs = ToCivilSecond(t, utc); - *c = CivilT(y, cs.month(), cs.day(), cs.hour(), cs.minute(), cs.second()); + // Field normalization while parsing (e.g. a ":60" leap second or an + // end-of-year rollover) can carry into the year. The other fields are taken + // from `cs`, so the same carry must be applied to the original year; + // otherwise the reconstructed value would use the wrong (un-carried) year. + const civil_year_t year = y + (cs.year() - normalized_year); + *c = + CivilT(year, cs.month(), cs.day(), cs.hour(), cs.minute(), cs.second()); return true; }
diff --git a/absl/time/civil_time_test.cc b/absl/time/civil_time_test.cc index b1e1f39..3ad8e14 100644 --- a/absl/time/civil_time_test.cc +++ b/absl/time/civil_time_test.cc
@@ -852,6 +852,23 @@ EXPECT_EQ(absl::CivilMonth(-1, 1), m); } +TEST(CivilTime, ParseFieldNormalizationCarriesYear) { + // When a field normalizes past the end of the year (e.g. a ":60" leap + // second on the last second of December), the carry must be reflected in + // the parsed year, so parsing agrees with direct field construction. + absl::CivilSecond ss; + EXPECT_TRUE(absl::ParseCivilTime("2020-12-31T23:59:60", &ss)) << ss; + EXPECT_EQ(absl::CivilSecond(2020, 12, 31, 23, 59, 60), ss); + EXPECT_EQ(absl::CivilSecond(2021, 1, 1, 0, 0, 0), ss); + + EXPECT_TRUE(absl::ParseLenientCivilTime("2020-12-31T23:59:60", &ss)) << ss; + EXPECT_EQ(absl::CivilSecond(2021, 1, 1, 0, 0, 0), ss); + + // The carry also works for negative years crossing zero. + EXPECT_TRUE(absl::ParseCivilTime("-1-12-31T23:59:60", &ss)) << ss; + EXPECT_EQ(absl::CivilSecond(0, 1, 1, 0, 0, 0), ss); +} + TEST(CivilTime, AbslStringify) { EXPECT_EQ("2015-01-02T03:04:05", absl::StrFormat("%v", absl::CivilSecond(2015, 1, 2, 3, 4, 5)));