| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| #include "iceberg/util/temporal_util.h" |
| |
| #include <limits> |
| #include <string> |
| |
| #include <gtest/gtest.h> |
| |
| #include "iceberg/test/matchers.h" |
| |
| namespace iceberg { |
| |
| TEST(TemporalUtilTest, ParseTimestampNs) { |
| ICEBERG_UNWRAP_OR_FAIL( |
| auto nanos, TemporalUtils::ParseTimestampNs("2026-01-01T00:00:01.000001001")); |
| EXPECT_EQ(nanos, 1767225601000001001L); |
| |
| ICEBERG_UNWRAP_OR_FAIL(auto pre_epoch_nanos, TemporalUtils::ParseTimestampNs( |
| "1969-12-31T23:59:59.123456789")); |
| EXPECT_EQ(pre_epoch_nanos, -876543211); |
| } |
| |
| TEST(TemporalUtilTest, ParseTimestampNsChecksInt64Bounds) { |
| ICEBERG_UNWRAP_OR_FAIL( |
| auto max_nanos, TemporalUtils::ParseTimestampNs("2262-04-11T23:47:16.854775807")); |
| EXPECT_EQ(max_nanos, std::numeric_limits<int64_t>::max()); |
| |
| ICEBERG_UNWRAP_OR_FAIL( |
| auto min_nanos, TemporalUtils::ParseTimestampNs("1677-09-21T00:12:43.145224192")); |
| EXPECT_EQ(min_nanos, std::numeric_limits<int64_t>::min()); |
| |
| EXPECT_THAT(TemporalUtils::ParseTimestampNs("2262-04-11T23:47:16.854775808"), |
| IsError(ErrorKind::kInvalidArgument)); |
| EXPECT_THAT(TemporalUtils::ParseTimestampNs("1677-09-21T00:12:43.145224191"), |
| IsError(ErrorKind::kInvalidArgument)); |
| } |
| |
| TEST(TemporalUtilTest, IsUtcOffset) { |
| // UTC offsets: "Z", "+00:00" and "-00:00". |
| ICEBERG_UNWRAP_OR_FAIL(auto z, TemporalUtils::IsUtcOffset("2024-06-27T00:00:00Z")); |
| EXPECT_TRUE(z); |
| ICEBERG_UNWRAP_OR_FAIL(auto plus_zero, |
| TemporalUtils::IsUtcOffset("2024-06-27T00:00:00+00:00")); |
| EXPECT_TRUE(plus_zero); |
| ICEBERG_UNWRAP_OR_FAIL(auto minus_zero, |
| TemporalUtils::IsUtcOffset("2024-06-27T00:00:00-00:00")); |
| EXPECT_TRUE(minus_zero); |
| |
| // Non-UTC offsets. |
| ICEBERG_UNWRAP_OR_FAIL(auto plus_five, |
| TemporalUtils::IsUtcOffset("2024-06-27T05:00:00+05:00")); |
| EXPECT_FALSE(plus_five); |
| ICEBERG_UNWRAP_OR_FAIL(auto minus_eight, |
| TemporalUtils::IsUtcOffset("2024-06-27T00:00:00-08:00")); |
| EXPECT_FALSE(minus_eight); |
| |
| // A missing or unparseable timezone suffix is an error. |
| EXPECT_THAT(TemporalUtils::IsUtcOffset("2024-06-27T00:00:00"), |
| IsError(ErrorKind::kInvalidArgument)); |
| EXPECT_THAT(TemporalUtils::IsUtcOffset(""), IsError(ErrorKind::kInvalidArgument)); |
| } |
| |
| TEST(TemporalUtilTest, ParseTimestampNsRejectsMoreThanNineFractionalDigits) { |
| EXPECT_THAT(TemporalUtils::ParseTimestampNs("2026-01-01T00:00:01.0000010011"), |
| IsError(ErrorKind::kInvalidArgument)); |
| } |
| |
| TEST(TemporalUtilTest, ParseTimestampNsWithZone) { |
| ICEBERG_UNWRAP_OR_FAIL(auto nanos, TemporalUtils::ParseTimestampNsWithZone( |
| "2026-01-01T00:00:01.000001001+00:00")); |
| EXPECT_EQ(nanos, 1767225601000001001L); |
| } |
| |
| TEST(TemporalUtilTest, ParseTimestampNsWithZoneChecksInt64BoundsAfterOffset) { |
| ICEBERG_UNWRAP_OR_FAIL(auto max_nanos, TemporalUtils::ParseTimestampNsWithZone( |
| "2262-04-12T00:47:16.854775807+01:00")); |
| EXPECT_EQ(max_nanos, std::numeric_limits<int64_t>::max()); |
| |
| ICEBERG_UNWRAP_OR_FAIL(auto min_nanos, TemporalUtils::ParseTimestampNsWithZone( |
| "1677-09-20T23:12:43.145224192-01:00")); |
| EXPECT_EQ(min_nanos, std::numeric_limits<int64_t>::min()); |
| |
| EXPECT_THAT( |
| TemporalUtils::ParseTimestampNsWithZone("2262-04-11T23:47:16.854775807-00:01"), |
| IsError(ErrorKind::kInvalidArgument)); |
| EXPECT_THAT( |
| TemporalUtils::ParseTimestampNsWithZone("1677-09-21T00:12:43.145224192+00:01"), |
| IsError(ErrorKind::kInvalidArgument)); |
| } |
| |
| TEST(TemporalUtilTest, ParseTimestampNsWithZoneRejectsOffsetPastPlusMinus1800) { |
| EXPECT_THAT( |
| TemporalUtils::ParseTimestampNsWithZone("2026-01-01T00:00:01.000001001+18:01"), |
| IsError(ErrorKind::kInvalidArgument)); |
| EXPECT_THAT( |
| TemporalUtils::ParseTimestampNsWithZone("2026-01-01T00:00:01.000001001-18:30"), |
| IsError(ErrorKind::kInvalidArgument)); |
| } |
| |
| struct ParseParam { |
| std::string name; |
| std::string str; |
| int64_t value; |
| enum Kind { kDay, kTime, kTimestamp, kTimestampTz } kind; |
| }; |
| |
| class TemporalParseTest : public ::testing::TestWithParam<ParseParam> {}; |
| |
| TEST_P(TemporalParseTest, ParsesCorrectly) { |
| const auto& param = GetParam(); |
| switch (param.kind) { |
| case ParseParam::kDay: { |
| ICEBERG_UNWRAP_OR_FAIL(auto parsed, TemporalUtils::ParseDay(param.str)); |
| EXPECT_EQ(parsed, static_cast<int32_t>(param.value)); |
| break; |
| } |
| case ParseParam::kTime: { |
| ICEBERG_UNWRAP_OR_FAIL(auto parsed, TemporalUtils::ParseTime(param.str)); |
| EXPECT_EQ(parsed, param.value); |
| break; |
| } |
| case ParseParam::kTimestamp: { |
| ICEBERG_UNWRAP_OR_FAIL(auto parsed, TemporalUtils::ParseTimestamp(param.str)); |
| EXPECT_EQ(parsed, param.value); |
| break; |
| } |
| case ParseParam::kTimestampTz: { |
| ICEBERG_UNWRAP_OR_FAIL(auto parsed, |
| TemporalUtils::ParseTimestampWithZone(param.str)); |
| EXPECT_EQ(parsed, param.value); |
| break; |
| } |
| } |
| } |
| |
| struct ParseTimeErrorParam { |
| std::string name; |
| std::string str; |
| }; |
| |
| class ParseTimeErrorTest : public ::testing::TestWithParam<ParseTimeErrorParam> {}; |
| |
| TEST_P(ParseTimeErrorTest, ReturnsError) { |
| EXPECT_THAT(TemporalUtils::ParseTime(GetParam().str), |
| IsError(ErrorKind::kInvalidArgument)); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| TemporalUtilTest, TemporalParseTest, |
| ::testing::Values( |
| ParseParam{"DayEpoch", "1970-01-01", 0, ParseParam::kDay}, |
| ParseParam{"DayNext", "1970-01-02", 1, ParseParam::kDay}, |
| ParseParam{"DayBeforeEpoch", "1969-12-31", -1, ParseParam::kDay}, |
| ParseParam{"DayYear999", "0999-12-31", -354286, ParseParam::kDay}, |
| ParseParam{"DayNonLeap", "1971-01-01", 365, ParseParam::kDay}, |
| ParseParam{"DayY2K", "2000-01-01", 10957, ParseParam::kDay}, |
| ParseParam{"Day2026", "2026-01-01", 20454, ParseParam::kDay}, |
| ParseParam{"TimeMidnight", "00:00", 0, ParseParam::kTime}, |
| ParseParam{"TimeOneSec", "00:00:01", 1000000, ParseParam::kTime}, |
| ParseParam{"TimeMillis", "00:00:01.500", 1500000, ParseParam::kTime}, |
| ParseParam{"TimeOneMillis", "00:00:01.001", 1001000, ParseParam::kTime}, |
| ParseParam{"TimeMicros", "00:00:01.000001", 1000001, ParseParam::kTime}, |
| ParseParam{"TimeHourMinSec", "01:02:03", 3723000000, ParseParam::kTime}, |
| ParseParam{"TimeEndOfDay", "23:59:59", 86399000000, ParseParam::kTime}, |
| ParseParam{"TimestampEpoch", "1970-01-01T00:00:00", 0, ParseParam::kTimestamp}, |
| ParseParam{"TimestampOneSec", "1970-01-01T00:00:01", 1000000, |
| ParseParam::kTimestamp}, |
| ParseParam{"TimestampMillis", "2026-01-01T00:00:01.500", 1767225601500000L, |
| ParseParam::kTimestamp}, |
| ParseParam{"TimestampOneMillis", "2026-01-01T00:00:01.001", 1767225601001000L, |
| ParseParam::kTimestamp}, |
| ParseParam{"TimestampMicros", "2026-01-01T00:00:01.000001", 1767225601000001L, |
| ParseParam::kTimestamp}, |
| ParseParam{"TimestampTzEpoch", "1970-01-01T00:00:00+00:00", 0, |
| ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzOneSec", "1970-01-01T00:00:01+00:00", 1000000, |
| ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzMillis", "2026-01-01T00:00:01.500+00:00", |
| 1767225601500000L, ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzOneMillis", "2026-01-01T00:00:01.001+00:00", |
| 1767225601001000L, ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzMicros", "2026-01-01T00:00:01.000001+00:00", |
| 1767225601000001L, ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzSuffixZ_Epoch", "1970-01-01T00:00:00Z", 0, |
| ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzSuffixZ_Millis", "2026-01-01T00:00:01.500Z", |
| 1767225601500000L, ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzNegZero_Epoch", "1970-01-01T00:00:00-00:00", 0, |
| ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzNegZero_Millis", "2026-01-01T00:00:01.500-00:00", |
| 1767225601500000L, ParseParam::kTimestampTz}, |
| ParseParam{"TimeTruncatesNanos", "00:00:01.123456789", 1123456, |
| ParseParam::kTime}, |
| ParseParam{"1Digit", "00:00:01.5", 1500000, ParseParam::kTime}, |
| ParseParam{"2Digits", "00:00:01.50", 1500000, ParseParam::kTime}, |
| ParseParam{"2DigitsNonZero", "00:00:01.12", 1120000, ParseParam::kTime}, |
| ParseParam{"4Digits", "00:00:01.0001", 1000100, ParseParam::kTime}, |
| ParseParam{"TimestampNoSec_Zero", "1970-01-01T00:00", 0, ParseParam::kTimestamp}, |
| ParseParam{"TimestampNoSec_OneMin", "1970-01-01T00:01", 60000000, |
| ParseParam::kTimestamp}, |
| ParseParam{"TimestampTzNoSec_Offset", "1970-01-01T00:00+00:00", 0, |
| ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzNoSec_OneMin", "1970-01-01T00:01+00:00", 60000000, |
| ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzNoSec_Z", "1970-01-01T00:00Z", 0, |
| ParseParam::kTimestampTz}, |
| ParseParam{"ExtendedYearPlusEpoch", "+1970-01-01", 0, ParseParam::kDay}, |
| ParseParam{"ExtendedYearPlus2026", "+2026-01-01", 20454, ParseParam::kDay}, |
| ParseParam{"ExtendedYearMinus2026", "-2026-01-01", -1459509, ParseParam::kDay}, |
| ParseParam{"TimestampTzPositiveOffset", "1970-01-01T05:00:00+05:00", 0, |
| ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzNegativeOffset", "1970-01-01T00:00:00-05:00", 18000000000, |
| ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzOffsetWithMillis", "2026-01-01T05:30:01.500+05:30", |
| 1767225601500000L, ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzNegOffsetToEpoch", "1969-12-31T19:00:00-05:00", 0, |
| ParseParam::kTimestampTz}, |
| ParseParam{"TimestampTzNoSecWithOffset", "1970-01-01T05:30+05:30", 0, |
| ParseParam::kTimestampTz}), |
| [](const ::testing::TestParamInfo<ParseParam>& info) { return info.param.name; }); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| TemporalUtilTest, ParseTimeErrorTest, |
| ::testing::Values(ParseTimeErrorParam{"EmptyString", ""}, |
| ParseTimeErrorParam{"TooShort1Char", "1"}, |
| ParseTimeErrorParam{"TooShort2Chars", "12"}, |
| ParseTimeErrorParam{"TooShort4Chars", "12:3"}, |
| ParseTimeErrorParam{"MissingColon", "1200:00"}, |
| ParseTimeErrorParam{"OutofRangeHours", "24:00:00"}, |
| ParseTimeErrorParam{"OutofRangeMinutes", "12:60:00"}, |
| ParseTimeErrorParam{"OutofRangeSeconds", "12:30:61"}, |
| ParseTimeErrorParam{"SpaceInsteadOfColon", "12 30"}), |
| [](const ::testing::TestParamInfo<ParseTimeErrorParam>& info) { |
| return info.param.name; |
| }); |
| |
| } // namespace iceberg |