From f7931d2b3284ba0e640e7e4c2e8aec3eb4f055f6 Mon Sep 17 00:00:00 2001 From: Kevin Wilfong Date: Fri, 20 Dec 2024 10:13:30 -0800 Subject: [PATCH] fix: Casting Varchar to Timestamp should handle offsets that are not recognized time zones (#11849) Summary: Pull Request resolved: https://github.com/facebookincubator/velox/pull/11849 When casting a Varchar to Timestamp, Presto Java allows an offset timestamp in place of a time zone. This string is of the form +/-HH:MM:SS.mmm where all units except the hour are optional, the colons are optional, the . is optional and may also be a , and this string must be separated by the date/time by a single space. This is not interpreted as a time zone but rather simply a milliseconds from UTC and is applied in addition to the time zone conversion if a session time zone is present. Note that strings that fit this pattern but are valid time zones e.g. +02:00 are still treated as time zones. Since this is not a true time zone it is not allowed when casting from Varchar to TimestampWithTimeZone or any other string to time conversions (at least AFAICT). This change updates fromTimestampWithTimezoneString to handle this case and return an offsetMillis in place of a time zone if one is present. Casting from Varchar to Timestamp has been updated to apply this offset to the result, while all other locations where this function is called have been updated to throw if offsetMillis is present. Reviewed By: kgpai Differential Revision: D67182709 fbshipit-source-id: cb2680da7244cab3104453dafb93ba516ce3a7a2 --- velox/expression/PrestoCastHooks.cpp | 36 +++-- velox/expression/tests/CastExprTest.cpp | 87 +++++++++++ velox/functions/prestosql/DateTimeFunctions.h | 3 +- .../prestosql/tests/DateTimeFunctionsTest.cpp | 3 + .../tests/TimestampWithTimeZoneCastTest.cpp | 6 + .../types/TimestampWithTimeZoneType.cpp | 10 +- velox/type/TimestampConversion.cpp | 139 ++++++++++++++++-- velox/type/TimestampConversion.h | 29 +++- velox/type/tests/TimestampConversionTest.cpp | 96 ++++++++++-- 9 files changed, 364 insertions(+), 45 deletions(-) diff --git a/velox/expression/PrestoCastHooks.cpp b/velox/expression/PrestoCastHooks.cpp index 9a2b64e5a70a..b86036e11033 100644 --- a/velox/expression/PrestoCastHooks.cpp +++ b/velox/expression/PrestoCastHooks.cpp @@ -54,17 +54,35 @@ Expected PrestoCastHooks::castStringToTimestamp( // If the parsed string has timezone information, convert the timestamp at // GMT at that time. For example, "1970-01-01 00:00:00 -00:01" is 60 seconds // at GMT. - if (result.second != nullptr) { - result.first.toGMT(*result.second); + if (result.timeZone != nullptr) { + result.timestamp.toGMT(*result.timeZone); + } else if (result.offsetMillis.has_value()) { + auto seconds = result.timestamp.getSeconds(); + // use int128_t to avoid overflow. + int128_t nanos = result.timestamp.getNanos(); + + seconds -= result.offsetMillis.value() / util::kMillisPerSecond; + nanos -= (result.offsetMillis.value() % util::kMillisPerSecond) * + util::kNanosPerMicro * util::kMicrosPerMsec; + + if (nanos < 0) { + seconds -= 1; + nanos += Timestamp::kNanosInSecond; + } else if (nanos > Timestamp::kMaxNanos) { + seconds += 1; + nanos -= Timestamp::kNanosInSecond; + } + result.timestamp = Timestamp(seconds, nanos); + } else { + // If no timezone information is available in the input string, check if we + // should understand it as being at the session timezone, and if so, convert + // to GMT. + if (options_.timeZone != nullptr) { + result.timestamp.toGMT(*options_.timeZone); + } } - // If no timezone information is available in the input string, check if we - // should understand it as being at the session timezone, and if so, convert - // to GMT. - else if (options_.timeZone != nullptr) { - result.first.toGMT(*options_.timeZone); - } - return result.first; + return result.timestamp; } Expected PrestoCastHooks::castIntToTimestamp(int64_t seconds) const { diff --git a/velox/expression/tests/CastExprTest.cpp b/velox/expression/tests/CastExprTest.cpp index 814ad750212a..198c8b34934d 100644 --- a/velox/expression/tests/CastExprTest.cpp +++ b/velox/expression/tests/CastExprTest.cpp @@ -548,8 +548,40 @@ TEST_F(CastExprTest, stringToTimestamp) { "1970-01-01 00:00:00-02:00", "1970-01-01 00:00:00 +02", "1970-01-01 00:00:00 -0101", + // Fully specified offset. + "1970-01-02 00:00:00 +01:01:01.001", + "1970-01-01 00:00:00 -01:01:01.001", + // Offset with two digit milliseconds. + "1970-01-02 00:00:00 +01:01:01.01", + "1970-01-01 00:00:00 -01:01:01.01", + // Offset with one digit milliseconds. + "1970-01-02 00:00:00 +01:01:01.1", + "1970-01-01 00:00:00 -01:01:01.1", + // Offset without milliseconds. + "1970-01-02 00:00:00 +01:01:01", + "1970-01-01 00:00:00 -01:01:01", + // Offset without seconds. + "1970-01-02 00:00:00 +23:01", + "1970-01-01 00:00:00 -23:01", + // Offset without minutes. + "1970-01-02 00:00:00 +23", + "1970-01-01 00:00:00 -23", + // Upper and lower limits of offsets. + "2000-01-01 12:13:14.123+23:59:59.999", + "2000-01-01 12:13:14.123-23:59:59.999", + // Comma instead of period for decimal in offset. + "1970-01-01 00:00:00 -01:01:01,001", + // Trailing spaces after offset. + "1970-01-02 00:00:00 +01:01:01.001 ", + // Overflow of nanoseconds in offset. + "1970-01-01 00:00:00.999 -01:01:01.002", + // Underflow of nanoseconds in offset. + "1970-01-02 00:00:00.001 +01:01:01.002", + // No optional separators + "1970-01-01 00:00:00 -010101001", std::nullopt, }; + std::vector> expected{ Timestamp(0, 0), Timestamp(10800, 0), @@ -559,6 +591,25 @@ TEST_F(CastExprTest, stringToTimestamp) { Timestamp(7200, 0), Timestamp(-7200, 0), Timestamp(3660, 0), + Timestamp(82738, 999000000), + Timestamp(3661, 1000000), + Timestamp(82738, 990000000), + Timestamp(3661, 10000000), + Timestamp(82738, 900000000), + Timestamp(3661, 100000000), + Timestamp(82739, 0), + Timestamp(3661, 0), + Timestamp(3540, 0), + Timestamp(82860, 0), + Timestamp(3600, 0), + Timestamp(82800, 0), + Timestamp(946642394, 124000000), + Timestamp(946815194, 122000000), + Timestamp(3661, 1000000), + Timestamp(82738, 999000000), + Timestamp(3662, 1000000), + Timestamp(82738, 999000000), + Timestamp(3661, 1000000), std::nullopt, }; testCast("timestamp", input, expected); @@ -571,6 +622,12 @@ TEST_F(CastExprTest, stringToTimestamp) { "1970-01-01 00:00 +01:00", "1970-01-01 00:00 America/Sao_Paulo", "2000-01-01 12:21:56Z", + "2000-01-01 12:21:56+01:01:01", + // Test going back and forth across DST boundaries. + "2024-03-10 09:59:59 -00:00:02", + "2024-03-10 10:00:01 +00:00:02", + "2024-11-03 08:59:59 -00:00:02", + "2024-11-03 09:00:01 +00:00:02", }; expected = { Timestamp(28800, 0), @@ -578,6 +635,11 @@ TEST_F(CastExprTest, stringToTimestamp) { Timestamp(-3600, 0), Timestamp(10800, 0), Timestamp(946729316, 0), + Timestamp(946725655, 0), + Timestamp(1710064801, 0), + Timestamp(1710064799, 0), + Timestamp(1730624401, 0), + Timestamp(1730624399, 0), }; testCast("timestamp", input, expected); @@ -602,6 +664,31 @@ TEST_F(CastExprTest, stringToTimestamp) { (evaluateOnce( "try_cast(c0 as timestamp)", "2045-12-31 18:00:00")), "Unable to convert timezone 'America/Los_Angeles' past 2037-11-01 09:00:00"); + // Only one white space is allowed before the offset string. + VELOX_ASSERT_THROW( + (evaluateOnce( + "cast(c0 as timestamp)", "2000-01-01 00:00:00 +01:01:01")), + "Cannot cast VARCHAR '2000-01-01 00:00:00 +01:01:01' to TIMESTAMP. Unknown timezone value: \"\""); + // Hour must be in the ragne [0, 23]. + VELOX_ASSERT_THROW( + (evaluateOnce( + "cast(c0 as timestamp)", "2000-01-01 00:00:00 +24")), + "Cannot cast VARCHAR '2000-01-01 00:00:00 +24' to TIMESTAMP. Unknown timezone value: \"+24\""); + // Minute must be in the range [0, 59]. + VELOX_ASSERT_THROW( + (evaluateOnce( + "cast(c0 as timestamp)", "2000-01-01 00:00:00 +01:60")), + "Cannot cast VARCHAR '2000-01-01 00:00:00 +01:60' to TIMESTAMP. Unknown timezone value: \"+01:60\""); + // Second must be in the range [0, 59]. + VELOX_ASSERT_THROW( + (evaluateOnce( + "cast(c0 as timestamp)", "2000-01-01 00:00:00 +01:01:60")), + "Cannot cast VARCHAR '2000-01-01 00:00:00 +01:01:60' to TIMESTAMP. Unknown timezone value: \"+01:01:60\""); + // Millisecond must be in the range [0, 999]. + VELOX_ASSERT_THROW( + (evaluateOnce( + "cast(c0 as timestamp)", "2000-01-01 00:00:00 +01:01:01.1000")), + "Cannot cast VARCHAR '2000-01-01 00:00:00 +01:01:01.1000' to TIMESTAMP. Unknown timezone value: \"+01:01:01.1000\""); setLegacyCast(true); input = { diff --git a/velox/functions/prestosql/DateTimeFunctions.h b/velox/functions/prestosql/DateTimeFunctions.h index 0d059b1a95c8..d6629b37508c 100644 --- a/velox/functions/prestosql/DateTimeFunctions.h +++ b/velox/functions/prestosql/DateTimeFunctions.h @@ -1525,7 +1525,8 @@ struct FromIso8601Timestamp { return castResult.error(); } - auto [ts, timeZone] = castResult.value(); + auto [ts, timeZone, offsetMillis] = castResult.value(); + VELOX_DCHECK(!offsetMillis.has_value()); // Input string may not contain a timezone - if so, it is interpreted in // session timezone. if (!timeZone) { diff --git a/velox/functions/prestosql/tests/DateTimeFunctionsTest.cpp b/velox/functions/prestosql/tests/DateTimeFunctionsTest.cpp index 81983edbfc0c..5ffc17c6810e 100644 --- a/velox/functions/prestosql/tests/DateTimeFunctionsTest.cpp +++ b/velox/functions/prestosql/tests/DateTimeFunctionsTest.cpp @@ -4387,6 +4387,9 @@ TEST_F(DateTimeFunctionsTest, fromIso8601Timestamp) { VELOX_ASSERT_THROW( fromIso("1970-01-02T11:38:56.123 America/New_York"), R"(Unable to parse timestamp value: "1970-01-02T11:38:56.123 America/New_York")"); + VELOX_ASSERT_THROW( + fromIso("1970-01-02T11:38:56+16:00:01"), + "Unknown timezone value: \"+16:00:01\""); VELOX_ASSERT_THROW(fromIso("T"), R"(Unable to parse timestamp value: "T")"); diff --git a/velox/functions/prestosql/tests/TimestampWithTimeZoneCastTest.cpp b/velox/functions/prestosql/tests/TimestampWithTimeZoneCastTest.cpp index 7ef536978874..d4f973e99fac 100644 --- a/velox/functions/prestosql/tests/TimestampWithTimeZoneCastTest.cpp +++ b/velox/functions/prestosql/tests/TimestampWithTimeZoneCastTest.cpp @@ -175,6 +175,9 @@ TEST_F(TimestampWithTimeZoneCastTest, fromVarcharInvalidInput) { const auto invalidStringVector6 = makeNullableFlatVector({"2012-10 America/Los_Angeles"}); + const auto invalidStringVector7 = + makeNullableFlatVector({"2012-10-01 +16:00"}); + auto millis = parseTimestamp("2012-10-31 07:00:47").toMillis(); auto timestamps = std::vector{millis}; @@ -204,6 +207,9 @@ TEST_F(TimestampWithTimeZoneCastTest, fromVarcharInvalidInput) { VELOX_ASSERT_THROW( testCast(invalidStringVector6, expected), "Unable to parse timestamp value: \"2012-10 America/Los_Angeles\""); + VELOX_ASSERT_THROW( + testCast(invalidStringVector7, expected), + "Unknown timezone value in: \"2012-10-01 +16:00\""); } TEST_F(TimestampWithTimeZoneCastTest, toTimestamp) { diff --git a/velox/functions/prestosql/types/TimestampWithTimeZoneType.cpp b/velox/functions/prestosql/types/TimestampWithTimeZoneType.cpp index de833dd77f2d..d6fbf76f6413 100644 --- a/velox/functions/prestosql/types/TimestampWithTimeZoneType.cpp +++ b/velox/functions/prestosql/types/TimestampWithTimeZoneType.cpp @@ -88,10 +88,18 @@ void castFromString( if (castResult.hasError()) { context.setStatus(row, castResult.error()); } else { - auto [ts, timeZone] = castResult.value(); + auto [ts, timeZone, millisOffset] = castResult.value(); // Input string may not contain a timezone - if so, it is interpreted in // session timezone. if (timeZone == nullptr) { + if (millisOffset.has_value()) { + context.setStatus( + row, + Status::UserError( + "Unknown timezone value in: \"{}\"", + inputVector.valueAt(row))); + return; + } const auto& config = context.execCtx()->queryCtx()->queryConfig(); timeZone = getTimeZoneFromConfig(config); } diff --git a/velox/type/TimestampConversion.cpp b/velox/type/TimestampConversion.cpp index 4cfe174a0693..e409d7d3849c 100644 --- a/velox/type/TimestampConversion.cpp +++ b/velox/type/TimestampConversion.cpp @@ -529,11 +529,118 @@ bool tryParseTimeString( return true; } -// Parses a variety of timestamp strings, depending on the value of `parseMode`. -// Consumes as much of the string as it can and sets `result` to the -// timestamp from whatever it successfully parses. `pos` is set to the position -// of first character that was not consumed. Returns true if it successfully -// parsed at least a date, `result` is only set if true is returned. +// String format is [+/-]hh:mm:ss.MMM +// * minutes, seconds, and milliseconds are optional. +// * all separators are optional. +// * . may be replaced with , +bool tryParsePrestoTimeOffsetString( + const char* buf, + size_t len, + size_t& pos, + int64_t& result) { + static constexpr int sep = ':'; + int32_t hour = 0, min = 0, sec = 0, millis = 0; + pos = 0; + result = 0; + + if (len == 0) { + return false; + } + + if (buf[pos] != '+' && buf[pos] != '-') { + return false; + } + + bool positive = buf[pos++] == '+'; + + if (pos >= len) { + return false; + } + + // Read the hours. + if (!parseDoubleDigit(buf, len, pos, hour)) { + return false; + } + if (hour < 0 || hour >= 24) { + return false; + } + + result += hour * kMillisPerHour; + + if (pos >= len || (buf[pos] != sep && !characterIsDigit(buf[pos]))) { + result *= positive ? 1 : -1; + return pos == len; + } + + // Skip the separator. + if (buf[pos] == sep) { + pos++; + } + + // Read the minutes. + if (!parseDoubleDigit(buf, len, pos, min)) { + return false; + } + if (min < 0 || min >= 60) { + return false; + } + + result += min * kMillisPerMinute; + + if (pos >= len || (buf[pos] != sep && !characterIsDigit(buf[pos]))) { + result *= positive ? 1 : -1; + return pos == len; + } + + // Skip the separator. + if (buf[pos] == sep) { + pos++; + } + + // Try to read seconds. + if (!parseDoubleDigit(buf, len, pos, sec)) { + return false; + } + if (sec < 0 || sec >= 60) { + return false; + } + + result += sec * kMillisPerSecond; + + if (pos >= len || + (buf[pos] != '.' && buf[pos] != ',' && !characterIsDigit(buf[pos]))) { + result *= positive ? 1 : -1; + return pos == len; + } + + // Skip the decimal. + if (buf[pos] == '.' || buf[pos] == ',') { + pos++; + } + + // Try to read microseconds. + if (pos >= len) { + return false; + } + + // We expect milliseconds. + int32_t mult = 100; + for (; pos < len && mult > 0 && characterIsDigit(buf[pos]); + pos++, mult /= 10) { + millis += (buf[pos] - '0') * mult; + } + + result += millis; + result *= positive ? 1 : -1; + return pos == len; +} + +// Parses a variety of timestamp strings, depending on the value of +// `parseMode`. Consumes as much of the string as it can and sets `result` to +// the timestamp from whatever it successfully parses. `pos` is set to the +// position of first character that was not consumed. Returns true if it +// successfully parsed at least a date, `result` is only set if true is +// returned. bool tryParseTimestampString( const char* buf, size_t len, @@ -583,8 +690,8 @@ bool tryParseTimestampString( if (!tryParseTimeString( buf + pos, len - pos, timePos, microsSinceMidnight, parseMode)) { // The rest of the string is not a valid time, but it could be relevant to - // the caller (e.g. it could be a time zone), return the date we parsed and - // let them decide what to do with the rest. + // the caller (e.g. it could be a time zone), return the date we parsed + // and let them decide what to do with the rest. result = fromDatetime(daysSinceEpoch, 0); return true; } @@ -857,8 +964,7 @@ fromTimestampString(const char* str, size_t len, TimestampParseMode parseMode) { return resultTimestamp; } -Expected> -fromTimestampWithTimezoneString( +Expected fromTimestampWithTimezoneString( const char* str, size_t len, TimestampParseMode parseMode) { @@ -870,6 +976,7 @@ fromTimestampWithTimezoneString( } const tz::TimeZone* timeZone = nullptr; + std::optional offset = std::nullopt; if (pos < len && parseMode != TimestampParseMode::kIso8601 && characterIsSpace(str[pos])) { @@ -894,8 +1001,16 @@ fromTimestampWithTimezoneString( std::string_view timeZoneName(str + pos, timezonePos - pos); if ((timeZone = tz::locateZone(timeZoneName, false)) == nullptr) { - return folly::makeUnexpected( - Status::UserError("Unknown timezone value: \"{}\"", timeZoneName)); + int64_t offsetMillis = 0; + size_t offsetPos = 0; + if (parseMode == TimestampParseMode::kPrestoCast && + tryParsePrestoTimeOffsetString( + str + pos, timezonePos - pos, offsetPos, offsetMillis)) { + offset = offsetMillis; + } else { + return folly::makeUnexpected( + Status::UserError("Unknown timezone value: \"{}\"", timeZoneName)); + } } // Skip any spaces at the end. @@ -908,7 +1023,7 @@ fromTimestampWithTimezoneString( return folly::makeUnexpected(parserError(str, len)); } } - return std::make_pair(resultTimestamp, timeZone); + return {{resultTimestamp, timeZone, offset}}; } int32_t toDate(const Timestamp& timestamp, const tz::TimeZone* timeZone_) { diff --git a/velox/type/TimestampConversion.h b/velox/type/TimestampConversion.h index fc43e776cf27..2919a934fa8a 100644 --- a/velox/type/TimestampConversion.h +++ b/velox/type/TimestampConversion.h @@ -31,6 +31,10 @@ constexpr const int32_t kMinsPerHour{60}; constexpr const int32_t kSecsPerMinute{60}; constexpr const int64_t kMsecsPerSec{1000}; +constexpr const int64_t kMillisPerSecond{1000}; +constexpr const int64_t kMillisPerMinute{kMillisPerSecond * kSecsPerMinute}; +constexpr const int64_t kMillisPerHour{kMillisPerMinute * kMinsPerHour}; + constexpr const int64_t kMicrosPerMsec{1000}; constexpr const int64_t kMicrosPerSec{kMicrosPerMsec * kMsecsPerSec}; constexpr const int64_t kMicrosPerMinute{kMicrosPerSec * kSecsPerMinute}; @@ -225,6 +229,18 @@ inline Expected fromTimestampString( return fromTimestampString(str.data(), str.size(), parseMode); } +struct ParsedTimestampWithTimeZone { + Timestamp timestamp; + const tz::TimeZone* timeZone; + std::optional offsetMillis; + + // For ease of testing purposes. + bool operator==(const ParsedTimestampWithTimeZone& other) const { + return timestamp == other.timestamp && timeZone == other.timeZone && + offsetMillis == other.offsetMillis; + } +}; + /// Parses a timestamp string using specified TimestampParseMode. /// /// This is a timezone-aware version of the function above @@ -237,16 +253,17 @@ inline Expected fromTimestampString( /// "America/Los_Angeles", or a timezone offset, like "+06:00" or "-09:30". The /// white space between the hour definition and timestamp is optional. /// -/// `nullptr` means no timezone information was found. Returns Unexpected with -/// UserError status in case of parsing errors. -Expected> -fromTimestampWithTimezoneString( +/// `nullptr` means the timezone was not recognized as a valid time zone or +/// was not present. In this case offsetMillis may be set with the milliseconds +/// timezone offset if an offset was found but was not a valid timezone. +/// +/// Returns Unexpected with UserError status in case of parsing errors. +Expected fromTimestampWithTimezoneString( const char* buf, size_t len, TimestampParseMode parseMode); -inline Expected> -fromTimestampWithTimezoneString( +inline Expected fromTimestampWithTimezoneString( const StringView& str, TimestampParseMode parseMode) { return fromTimestampWithTimezoneString(str.data(), str.size(), parseMode); diff --git a/velox/type/tests/TimestampConversionTest.cpp b/velox/type/tests/TimestampConversionTest.cpp index 70a4238e3884..7c4fc9aec5e8 100644 --- a/velox/type/tests/TimestampConversionTest.cpp +++ b/velox/type/tests/TimestampConversionTest.cpp @@ -40,7 +40,7 @@ int32_t parseDate(const StringView& str, ParseMode mode) { }); } -std::pair parseTimestampWithTimezone( +ParsedTimestampWithTimeZone parseTimestampWithTimezone( const StringView& str, TimestampParseMode parseMode = TimestampParseMode::kPrestoCast) { return fromTimestampWithTimezoneString(str.data(), str.size(), parseMode) @@ -345,9 +345,6 @@ TEST(DateTimeUtilTest, fromTimestampStringInvalid) { parseTimestampWithTimezone("1970-01-01 00:00:00-asd"), timezoneError); VELOX_ASSERT_THROW( parseTimestampWithTimezone("1970-01-01 00:00:00Z UTC"), parserError); - VELOX_ASSERT_THROW( - parseTimestampWithTimezone("1970-01-01 00:00:00+00:00:00"), - timezoneError); // Can't have multiple spaces. VELOX_ASSERT_THROW( @@ -357,52 +354,119 @@ TEST(DateTimeUtilTest, fromTimestampStringInvalid) { TEST(DateTimeUtilTest, fromTimestampWithTimezoneString) { // -1 means no timezone information. auto expected = - std::make_pair(Timestamp(0, 0), nullptr); + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, std::nullopt}); EXPECT_EQ(parseTimestampWithTimezone("1970-01-01 00:00:00"), expected); // Test timezone offsets. EXPECT_EQ( parseTimestampWithTimezone("1970-01-01 00:00:00 -02:00"), - std::make_pair(Timestamp(0, 0), tz::locateZone("-02:00"))); + (ParsedTimestampWithTimeZone{ + Timestamp(0, 0), tz::locateZone("-02:00"), std::nullopt})); EXPECT_EQ( parseTimestampWithTimezone("1970-01-01 00:00:00+13:36"), - std::make_pair(Timestamp(0, 0), tz::locateZone("+13:36"))); + (ParsedTimestampWithTimeZone{ + Timestamp(0, 0), tz::locateZone("+13:36"), std::nullopt})); EXPECT_EQ( parseTimestampWithTimezone("1970-01-01 00:00:00 -11"), - std::make_pair(Timestamp(0, 0), tz::locateZone("-11:00"))); + (ParsedTimestampWithTimeZone{ + Timestamp(0, 0), tz::locateZone("-11:00"), std::nullopt})); EXPECT_EQ( parseTimestampWithTimezone("1970-01-01 00:00:00 +0000"), - std::make_pair(Timestamp(0, 0), tz::locateZone("+00:00"))); + (ParsedTimestampWithTimeZone{ + Timestamp(0, 0), tz::locateZone("+00:00"), std::nullopt})); + + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 +01:01:01.001"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, 3661001})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 -01:01:01.001"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, -3661001})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 +01:01:01.01"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, 3661010})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 -01:01:01.01"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, -3661010})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 +01:01:01.1"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, 3661100})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 -01:01:01.1"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, -3661100})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 +01:01:01"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, 3661000})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 -01:01:01"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, -3661000})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 +23:01"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, 82860000})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 -23:01"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, -82860000})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 +23"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, 82800000})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 -23"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, -82800000})); + EXPECT_EQ( + parseTimestampWithTimezone("2000-01-01 12:13:14.123+23:59:59.999"), + (ParsedTimestampWithTimeZone{ + Timestamp(946728794, 123000000), nullptr, 86399999})); + EXPECT_EQ( + parseTimestampWithTimezone("2000-01-01 12:13:14.123-23:59:59.999"), + (ParsedTimestampWithTimeZone{ + Timestamp(946728794, 123000000), nullptr, -86399999})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 +01:01:01,001"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, 3661001})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 -01:01:01.001 "), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, -3661001})); + EXPECT_EQ( + parseTimestampWithTimezone("1970-01-01 00:00:00 +010101001"), + (ParsedTimestampWithTimeZone{Timestamp(0, 0), nullptr, 3661001})); EXPECT_EQ( parseTimestampWithTimezone("1970-01-01 00:00:00Z"), - std::make_pair(Timestamp(0, 0), tz::locateZone("UTC"))); + (ParsedTimestampWithTimeZone{ + Timestamp(0, 0), tz::locateZone("UTC"), std::nullopt})); EXPECT_EQ( parseTimestampWithTimezone("1970-01-01 00:01:00 UTC"), - std::make_pair(Timestamp(60, 0), tz::locateZone("UTC"))); + (ParsedTimestampWithTimeZone{ + Timestamp(60, 0), tz::locateZone("UTC"), std::nullopt})); EXPECT_EQ( parseTimestampWithTimezone("1970-01-01 00:00:01 America/Los_Angeles"), - std::make_pair(Timestamp(1, 0), tz::locateZone("America/Los_Angeles"))); + (ParsedTimestampWithTimeZone{ + Timestamp(1, 0), + tz::locateZone("America/Los_Angeles"), + std::nullopt})); EXPECT_EQ( parseTimestampWithTimezone("1970-01-01 Pacific/Fiji"), - std::make_pair(Timestamp(0, 0), tz::locateZone("Pacific/Fiji"))); + (ParsedTimestampWithTimeZone{ + Timestamp(0, 0), tz::locateZone("Pacific/Fiji"), std::nullopt})); EXPECT_EQ( parseTimestampWithTimezone( "1970-01-01T+01:00", TimestampParseMode::kIso8601), - std::make_pair(Timestamp(0, 0), tz::locateZone("+01:00"))); + (ParsedTimestampWithTimeZone{ + Timestamp(0, 0), tz::locateZone("+01:00"), std::nullopt})); EXPECT_EQ( parseTimestampWithTimezone( "1970-01T+14:00", TimestampParseMode::kIso8601), - std::make_pair(Timestamp(0, 0), tz::locateZone("+14:00"))); + (ParsedTimestampWithTimeZone{ + Timestamp(0, 0), tz::locateZone("+14:00"), std::nullopt})); EXPECT_EQ( parseTimestampWithTimezone("1970T-06:00", TimestampParseMode::kIso8601), - std::make_pair(Timestamp(0, 0), tz::locateZone("-06:00"))); + (ParsedTimestampWithTimeZone{ + Timestamp(0, 0), tz::locateZone("-06:00"), std::nullopt})); } TEST(DateTimeUtilTest, toGMT) {