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) {