diff --git a/libc/src/time/CMakeLists.txt b/libc/src/time/CMakeLists.txt --- a/libc/src/time/CMakeLists.txt +++ b/libc/src/time/CMakeLists.txt @@ -8,6 +8,23 @@ libc.include.errno libc.include.time libc.src.errno.errno + .time_zone_posix +) + +add_object_library( + time_zone_posix + CXX_STANDARD 20 + SRCS + time_zone_posix.cpp + HDRS + time_zone_posix.h + DEPENDS + libc.src.ctype.isdigit + libc.src.stdlib.strtol + libc.src.string.memory_utils.memset_implementation + libc.src.string.strchr + libc.src.__support.CPP.optional + libc.src.__support.str_to_integer ) add_entrypoint_object( diff --git a/libc/src/time/time_zone_posix.h b/libc/src/time/time_zone_posix.h new file mode 100644 --- /dev/null +++ b/libc/src/time/time_zone_posix.h @@ -0,0 +1,129 @@ +//===-- Collection of utils for TZ pasrsing ---------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_SRC_TIME_TIME_ZONE_POSIX_H +#define LLVM_LIBC_SRC_TIME_TIME_ZONE_POSIX_H + +#include // int8_t, int16_t and int32_t + +#include "src/__support/CPP/Optional.h" +#include "src/__support/CPP/StringView.h" + +#include // int8_t, int16_t and int32_t + +namespace __llvm_libc { +namespace time_zone_posix { + +// The TZ environment variable is specified in +// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html + +// The date/time of the transition. The date is specified as either: +// (J) the Nth day of the year (1 <= N <= 365), excluding leap days, or +// (N) the Nth day of the year (0 <= N <= 365), including leap days, or +// (M) the Nth weekday of a month (e.g., the 2nd Sunday in March). +// The time, specified as a day offset, identifies the particular moment +// of the transition, and may be negative or >= 24h, and in which case +// it would take us to another day, and perhaps week, or even month. + +class PosixTransition { +public: + enum DateFormat : int { J, N, M }; + + struct Date { + struct NonLeapDay { + int16_t day; // day of non-leap year [1:365] + }; + struct Day { + int16_t day; // day of year [0:365] + }; + struct MonthWeekWeekday { + int8_t month; // month of year [1:12] + int8_t week; // week of month [1:5] (5==last) + int8_t weekday; // 0==Sun, ..., 6=Sat + }; + + DateFormat fmt; + + union { + NonLeapDay j; + Day n; + MonthWeekWeekday m; + }; + }; + + struct Time { + int32_t offset; // seconds before/after 00:00:00 + }; + + Date date; + Time time; + + PosixTransition() { + date.fmt = N; + date.j.day = 0; + time.offset = 0; + } +}; + +// The entirety of a POSIX-string specified time-zone rule. The standard +// abbreviation and offset are always given. If the time zone includes +// daylight saving, then the daylight abbreviation is non-empty and the +// remaining fields are also valid. Note that the start/end transitions +// are not ordered---in the southern hemisphere the transition to end +// daylight time occurs first in any particular year. +class PosixTimeZone { +public: + // Default constructor for testing. + PosixTimeZone() + : m_spec(""), m_std_abbr("UTC"), m_std_offset(0), m_dst_abbr(""), + m_dst_offset(0) {} + + explicit PosixTimeZone(const cpp::StringView &spec) + : m_spec(spec), m_std_abbr("UTC"), m_std_offset(0), m_dst_abbr(""), + m_dst_offset(0) {} + + // Breaks down a POSIX time-zone specification into its constituent pieces, + // filling in any missing values (DST offset, or start/end transition times) + // with the standard-defined defaults. Returns false if the specification + // could not be parsed (although some fields of *res may have been altered). + static cpp::Optional + ParsePosixSpec(const cpp::StringView spec); + + cpp::StringView m_spec; + + cpp::StringView m_std_abbr; + int32_t m_std_offset; + + cpp::StringView m_dst_abbr; + int32_t m_dst_offset; + + PosixTransition m_dst_start; + PosixTransition m_dst_end; + +private: + bool UpdateStdAbbr(); + bool UpdateDstAbbr(); + bool UpdateStdOffset(); + bool UpdateDstOffset(); + bool UpdateDstStart(); + bool UpdateDstEnd(); + bool SpecHasData(); + + cpp::Optional ParseInt(int min, int max); + cpp::Optional ParseAbbr(); + cpp::Optional ParseOffset(int min_hour, int max_hour, int sign); + cpp::Optional ParseMonthWeekWeekday(); + cpp::Optional ParseNonLeapDay(); + cpp::Optional ParseLeapDay(); + cpp::Optional ParseDateTime(); +}; + +} // namespace time_zone_posix +} // namespace __llvm_libc + +#endif // LLVM_LIBC_SRC_TIME_TIME_ZONE_POSIX_H diff --git a/libc/src/time/time_zone_posix.cpp b/libc/src/time/time_zone_posix.cpp new file mode 100644 --- /dev/null +++ b/libc/src/time/time_zone_posix.cpp @@ -0,0 +1,304 @@ +#include // size_t + +#include "src/ctype/isdigit.h" +#include "src/stdlib/strtol.h" +#include "src/string/strchr.h" +#include "src/time/time_zone_posix.h" + +namespace __llvm_libc { +namespace time_zone_posix { + +// https://datatracker.ietf.org/doc/html/rfc8536 +// The hours part of the transition times may be signed and range +// from -167 through 167 (-167 <= hh <= 167) instead of the POSIX- +// required unsigned values from 0 through 24. +static constexpr int MAX_HOURS_IN_TRANSITION_TIMES = 167; + +cpp::Optional PosixTimeZone::ParseInt(int min, int max) { + if (m_spec.size() == 0) + return cpp::Nullopt; + + char *pEnd; + int value = strtol(m_spec.data(), &pEnd, 10); // NOLINT(runtime/deprecated_fn) + if (value < min || value > max) + return cpp::Nullopt; + + // Delete the number from the input string. + size_t len = pEnd - m_spec.data(); + m_spec.remove_prefix(len); + return value; +} + +// abbr = <.*?> | [^-+,\d]{3,} +cpp::Optional PosixTimeZone::ParseAbbr() { + if (m_spec.size() == 0) + return cpp::Nullopt; + + // Handle special zoneinfo <...> form. + cpp::Optional abbr; + if (m_spec.starts_with('<')) { + m_spec.remove_prefix(1); + const auto pos = m_spec.find_first_of('>'); + if (pos == cpp::StringView::npos) + return cpp::Nullopt; + abbr = m_spec.substr(0, pos); + // Delete the data up to and including '>' character. + m_spec.remove_prefix(pos + 1); + return abbr; + } + + size_t len = 0; + // Handle [^-+,\d]{3,} + for (const auto &p : m_spec) { + if (strchr("-+,", p)) + break; + if (isdigit(p)) + break; + len++; + } + if (len < 3) + return cpp::Nullopt; + abbr = m_spec.substr(0, len); + m_spec.remove_prefix(len); + return abbr; +} + +// offset = [+|-]hh[:mm[:ss]] (aggregated into single seconds value) +cpp::Optional PosixTimeZone::ParseOffset(int min_hour, int max_hour, + int sign) { + if (m_spec.size() == 0) + return cpp::Nullopt; + + // Handle [+|-]. + if (m_spec.starts_with('+') || m_spec.starts_with('-')) { + if (m_spec.starts_with('-')) + sign = -sign; + m_spec.remove_prefix(1); + } + + int hours = 0; + int minutes = 0; + int seconds = 0; + + // Parse hours - hh + cpp::Optional result = ParseInt(min_hour, max_hour); + if (!result.has_value()) + return cpp::Nullopt; + hours = result.value(); + + // Check for optional minutes. + // Parse minutes and if there is no data, then default to 0 - hh[:mm] + if (m_spec.starts_with(':')) { + m_spec.remove_prefix(1); + + // Parse minutes. + result = ParseInt(0, 59); + if (!result.has_value()) + return cpp::Nullopt; + minutes = result.value(); + + // Check for optional seconds. + // Parse seconds and if there is no data, then default to 0 - hh[:mm[:ss]] + if (m_spec.starts_with(':')) { + m_spec.remove_prefix(1); + + // Parse seconds. + result = ParseInt(0, 59); + if (!result.has_value()) + return cpp::Nullopt; + seconds = result.value(); + } + } + int32_t offset = sign * ((((hours * 60) + minutes) * 60) + seconds); + return offset; +} + +// (M) the Nth weekday of a month (e.g., the 2nd Sunday in March). +// Mm.w.d +cpp::Optional PosixTimeZone::ParseMonthWeekWeekday() { + // Parse month. + cpp::Optional result = ParseInt(1, 12); + if (!result.has_value()) + return cpp::Nullopt; + int month = result.value(); + + // Error if there is no dot. + if (!m_spec.starts_with('.')) + return cpp::Nullopt; + m_spec.remove_prefix(1); + + // Error if there is no week. + if (m_spec.size() == 0) + return cpp::Nullopt; + + // Parse week. + result = ParseInt(1, 5); + if (!result.has_value()) + return cpp::Nullopt; + int week = result.value(); + + // Error if there is no dot. + if (!m_spec.starts_with('.')) + return cpp::Nullopt; + m_spec.remove_prefix(1); + + // Error if there is no weekday. + if (m_spec.size() == 0) + return cpp::Nullopt; + + // Parse Weekday. + result = ParseInt(0, 6); + if (!result.has_value()) + return cpp::Nullopt; + int weekday = result.value(); + + PosixTransition posixTransition; + posixTransition.date.fmt = PosixTransition::M; + posixTransition.date.m.month = static_cast(month); + posixTransition.date.m.week = static_cast(week); + posixTransition.date.m.weekday = static_cast(weekday); + return posixTransition; +} + +// Parse Jn +// (J) the Nth day of the year (1 <= N <= 365), excluding leap days. +cpp::Optional PosixTimeZone::ParseNonLeapDay() { + const cpp::Optional result = ParseInt(1, 365); + if (!result.has_value()) + return cpp::Nullopt; + PosixTransition posixTransition; + posixTransition.date.fmt = PosixTransition::J; + posixTransition.date.j.day = static_cast(result.value()); + return posixTransition; +} + +// Parse n +// (N) the Nth day of the year (0 <= N <= 365), including leap days +cpp::Optional PosixTimeZone::ParseLeapDay() { + const cpp::Optional result = ParseInt(0, 365); + if (!result.has_value()) + return cpp::Nullopt; + PosixTransition posixTransition; + posixTransition.date.fmt = PosixTransition::N; + posixTransition.date.n.day = static_cast(result.value()); + return posixTransition; +} + +// datetime = ( Jn | n | Mm.w.d ) [ / offset ] +cpp::Optional PosixTimeZone::ParseDateTime() { + PosixTransition posixTransition; + if (m_spec.starts_with(',')) { + m_spec.remove_prefix(1); + + cpp::Optional optionalPosixTransition; + if (m_spec.starts_with('M')) { + m_spec.remove_prefix(1); + optionalPosixTransition = ParseMonthWeekWeekday(); // Mm.w.d + } else if (m_spec.starts_with('J')) { + m_spec.remove_prefix(1); + optionalPosixTransition = ParseNonLeapDay(); // Jn + } else { + optionalPosixTransition = ParseLeapDay(); // n + } + if (optionalPosixTransition.has_value()) + posixTransition = optionalPosixTransition.value(); + } + + // Parse time offset: / offset + posixTransition.time.offset = 2 * 60 * 60; // default offset is 02:00:00 + if (m_spec.starts_with('/')) { + m_spec.remove_prefix(1); + const cpp::Optional offset = ParseOffset( + -MAX_HOURS_IN_TRANSITION_TIMES, MAX_HOURS_IN_TRANSITION_TIMES, 1); + if (!offset.has_value()) + return cpp::Nullopt; + posixTransition.time.offset = offset.value(); + } + return posixTransition; +} + +bool PosixTimeZone::UpdateStdAbbr() { + const auto abbr_result = ParseAbbr(); + if (!abbr_result.has_value()) + return false; + m_std_abbr = *abbr_result; + return true; +} + +bool PosixTimeZone::UpdateDstAbbr() { + const auto abbr_result = ParseAbbr(); + if (!abbr_result.has_value()) + return false; + m_dst_abbr = *abbr_result; + return true; +} + +bool PosixTimeZone::UpdateStdOffset() { + const cpp::Optional offset = ParseOffset(0, 24, -1); + if (!offset.has_value()) + return false; + m_std_offset = offset.value(); + return true; +} + +bool PosixTimeZone::UpdateDstOffset() { + m_dst_offset = m_std_offset + (60 * 60); // default + if (!m_spec.starts_with(',')) { + const cpp::Optional offset = ParseOffset(0, 24, -1); + if (!offset.has_value()) + return false; + m_dst_offset = offset.value(); + } + return true; +} + +bool PosixTimeZone::UpdateDstStart() { + const auto date_time_result = ParseDateTime(); + if (!date_time_result.has_value()) + return false; + m_dst_start = *date_time_result; + return true; +} + +bool PosixTimeZone::UpdateDstEnd() { + const auto date_time_result = ParseDateTime(); + if (!date_time_result.has_value()) + return false; + m_dst_end = *date_time_result; + return true; +} + +bool PosixTimeZone::SpecHasData() { + if (m_spec.size() == 0) + return false; + return true; +} + +// spec = std offset [ dst [ offset ] , datetime , datetime ] +cpp::Optional +PosixTimeZone::ParsePosixSpec(const cpp::StringView spec) { + if (spec.starts_with(':')) + return cpp::Nullopt; + + PosixTimeZone res(spec); + if (!res.UpdateStdAbbr()) + return cpp::Nullopt; + if (!res.UpdateStdOffset()) + return cpp::Nullopt; + if (res.m_spec.size() == 0) + return res; + if (!res.UpdateDstAbbr()) + return cpp::Nullopt; + if (!res.UpdateDstOffset()) + return cpp::Nullopt; + if (!res.UpdateDstStart()) + return cpp::Nullopt; + if (!res.UpdateDstEnd()) + return cpp::Nullopt; + if (res.m_spec.size() != 0) + return cpp::Nullopt; + return res; +} + +} // namespace time_zone_posix +} // namespace __llvm_libc diff --git a/libc/test/src/time/CMakeLists.txt b/libc/test/src/time/CMakeLists.txt --- a/libc/test/src/time/CMakeLists.txt +++ b/libc/test/src/time/CMakeLists.txt @@ -68,3 +68,16 @@ DEPENDS libc.src.time.mktime ) + +add_libc_unittest( + time_zone_posix + SUITE + libc_time_unittests + SRCS + time_zone_posix_test.cpp + HDRS + TZHelper.h + TZMatcher.h + DEPENDS + libc.src.time.time_zone_posix +) diff --git a/libc/test/src/time/TZHelper.h b/libc/test/src/time/TZHelper.h new file mode 100644 --- /dev/null +++ b/libc/test/src/time/TZHelper.h @@ -0,0 +1,69 @@ +//===---- TzHelper.h --------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_TEST_SRC_TIME_TZ_HELPER_H +#define LLVM_LIBC_TEST_SRC_TIME_TZ_HELPER_H + +#include "src/time/time_zone_posix.h" + +namespace __llvm_libc { +namespace tzhelper { +namespace testing { + +// A helper function to initialize tz data structure. +static inline __llvm_libc::time_zone_posix::PosixTimeZone +initialize_posix_time_zone_data( + __llvm_libc::cpp::StringView spec, __llvm_libc::cpp::StringView std_abbr, + int32_t std_offset, __llvm_libc::cpp::StringView dst_abbr, + int32_t dst_offset, __llvm_libc::time_zone_posix::PosixTransition dst_start, + __llvm_libc::time_zone_posix::PosixTransition dst_end) { + __llvm_libc::time_zone_posix::PosixTimeZone res(spec); + res.m_std_abbr = std_abbr; + res.m_std_offset = std_offset; + res.m_dst_abbr = dst_abbr; + res.m_dst_offset = dst_offset; + res.m_dst_start = dst_start; + res.m_dst_end = dst_end; + return res; +} + +static inline __llvm_libc::time_zone_posix::PosixTransition +initialize_posix_transition_M_data(int8_t month, int8_t week, int8_t weekday, + int32_t offset) { + __llvm_libc::time_zone_posix::PosixTransition res; + res.date.fmt = __llvm_libc::time_zone_posix::PosixTransition::M; + res.date.m.month = month; + res.date.m.week = week; + res.date.m.weekday = weekday; + res.time.offset = offset; + return res; +} + +static inline __llvm_libc::time_zone_posix::PosixTransition +initialize_posix_transition_J_data(int16_t day, int32_t offset) { + __llvm_libc::time_zone_posix::PosixTransition res; + res.date.fmt = __llvm_libc::time_zone_posix::PosixTransition::J; + res.date.j.day = day; + res.time.offset = offset; + return res; +} + +static inline __llvm_libc::time_zone_posix::PosixTransition +initialize_posix_transition_N_data(int16_t day, int32_t offset) { + __llvm_libc::time_zone_posix::PosixTransition res; + res.date.fmt = __llvm_libc::time_zone_posix::PosixTransition::J; + res.date.n.day = day; + res.time.offset = offset; + return res; +} + +} // namespace testing +} // namespace tzhelper +} // namespace __llvm_libc + +#endif // LLVM_LIBC_TEST_SRC_TIME_TZ_HELPER_H diff --git a/libc/test/src/time/TZMatcher.h b/libc/test/src/time/TZMatcher.h new file mode 100644 --- /dev/null +++ b/libc/test/src/time/TZMatcher.h @@ -0,0 +1,115 @@ +//===---- TzMatchers.h ------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_TEST_SRC_TIME_TZ_MATCHER_H +#define LLVM_LIBC_TEST_SRC_TIME_TZ_MATCHER_H + +#include "src/time/time_zone_posix.h" + +#include "utils/UnitTest/Test.h" + +namespace __llvm_libc { +namespace tzmatcher { +namespace testing { + +class PosixTimeZoneMatcher + : public __llvm_libc::testing::Matcher { + time_zone_posix::PosixTimeZone expected; + time_zone_posix::PosixTimeZone actual; + +public: + PosixTimeZoneMatcher(time_zone_posix::PosixTimeZone expectedValue) + : expected(expectedValue) {} + + bool posix_transition_equals(time_zone_posix::PosixTransition expectedValue, + time_zone_posix::PosixTransition actualValue) { + switch (expectedValue.date.fmt) { + case time_zone_posix::PosixTransition::J: + return (expectedValue.date.fmt == actualValue.date.fmt && + expectedValue.time.offset == actualValue.time.offset && + expectedValue.date.j.day == actualValue.date.j.day); + case time_zone_posix::PosixTransition::N: + return (expectedValue.date.fmt == actualValue.date.fmt && + expectedValue.time.offset == actualValue.time.offset && + expectedValue.date.n.day == actualValue.date.n.day); + case time_zone_posix::PosixTransition::M: + return (expectedValue.date.fmt == actualValue.date.fmt && + expectedValue.time.offset == actualValue.time.offset && + expectedValue.date.m.month == actualValue.date.m.month && + expectedValue.date.m.week == actualValue.date.m.week && + expectedValue.date.m.weekday == actualValue.date.m.weekday); + } + } + + void posix_transition_describe_value( + const char *label, time_zone_posix::PosixTransition value, + __llvm_libc::testutils::StreamWrapper &stream) { + switch (value.date.fmt) { + case time_zone_posix::PosixTransition::J: + stream << label << ".date.fmt: J "; + stream << label << ".date.j.day: " << value.date.j.day; + break; + case time_zone_posix::PosixTransition::N: + stream << label << ".date.fmt: N "; + stream << label << ".date.n.day: " << value.date.n.day; + break; + case time_zone_posix::PosixTransition::M: + stream << label << ".date.fmt: M "; + stream << label + << ".date.m.month: " << static_cast(value.date.m.month); + stream << label + << ".date.m.week: " << static_cast(value.date.m.week); + stream << label + << ".date.m.weekday: " << static_cast(value.date.m.weekday); + break; + } + stream << label << ".time.offset: " << value.time.offset; + } + + bool match(time_zone_posix::PosixTimeZone actualValue) { + actual = actualValue; + return (actual.m_spec == expected.m_spec && + actual.m_std_abbr == expected.m_std_abbr && + actual.m_std_offset == expected.m_std_offset && + actual.m_dst_abbr == expected.m_dst_abbr && + actual.m_dst_offset == expected.m_dst_offset && + posix_transition_equals(expected.m_dst_start, actual.m_dst_start) && + posix_transition_equals(expected.m_dst_end, actual.m_dst_end)); + } + + void describeValue(const char *label, time_zone_posix::PosixTimeZone value, + __llvm_libc::testutils::StreamWrapper &stream) { + stream << label; + if (value.m_spec.data()) + stream << " m_spec: " << value.m_spec.data(); + else + stream << " m_spec: null"; + stream << " m_std_abbr: " << value.m_std_abbr.data(); + stream << " m_std_offset: " << value.m_std_offset; + stream << " m_dst_abbr: " << value.m_dst_abbr.data(); + stream << " m_dst_offset: " << value.m_dst_offset; + posix_transition_describe_value(" m_dst_start", value.m_dst_start, stream); + posix_transition_describe_value(" m_dst_end", value.m_dst_end, stream); + stream << '\n'; + } + + void explainError(__llvm_libc::testutils::StreamWrapper &stream) override { + describeValue("Expected PosixTimeZone value: ", expected, stream); + describeValue(" Actual PosixTimeZone value: ", actual, stream); + } +}; + +} // namespace testing +} // namespace tzmatcher +} // namespace __llvm_libc + +#define EXPECT_POSIX_TIME_ZONE_EQ(expected, actual) \ + EXPECT_THAT((actual), __llvm_libc::tzmatcher::testing::PosixTimeZoneMatcher( \ + (expected))) + +#endif // LLVM_LIBC_TEST_SRC_TIME_TZ_MATCHER_H diff --git a/libc/test/src/time/time_zone_posix_test.cpp b/libc/test/src/time/time_zone_posix_test.cpp new file mode 100644 --- /dev/null +++ b/libc/test/src/time/time_zone_posix_test.cpp @@ -0,0 +1,273 @@ +//===-- Unittests for asctime ---------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "src/__support/CPP/StringView.h" +#include "src/time/time_zone_posix.h" +#include "test/ErrnoSetterMatcher.h" +#include "test/src/time/TZHelper.h" +#include "test/src/time/TZMatcher.h" +#include "utils/UnitTest/Test.h" + +#include +#include + +TEST(LlvmLibcParsePosixSpec, InvalidTest) { + const char *bad_timezones[] = { + "", + ":", + " ", + // + // Test for [+|-]hh[:mm[:ss]] in STD and DST offsets. + // + // Test missing hours in STD offset. + "EST", + "EST+", + "EST-", + // Test for missing minutes in STD offset + "EST5:", + "EST+5:", + "EST-5:", + // Test for missing seconds in STD offset + "EST5:0:", + "EST+5:0:", + "EST-5:0:", + // Test invalid hours data in STD Offset. + "EST25", + "EST+25", + // Test invalid minutes data in STD offset. + "EST5:-1:0", + "EST5:60:0", + "EST+5:-1:0", + "EST+5:60:0", + "EST-5:-1:0", + "EST-5:60:0", + // Test invalid seconds data in STD offset. + "EST5:0:-1", + "EST5:0:60", + "EST+5:0:-1", + "EST+5:0:60", + "EST-5:0:-1", + "EST-5:0:60", + + // Test missing hours in DST offset. + "EST5EDT", + "EST5EDT+", + "EST5EDT-", + // Test for missing minutes in DST offset + "EST5EDT6:", + "EST5EDT+6:", + "EST5EDT-6:", + // Test for missing seconds in DST offset + "EST5EDT6:0:", + "EST5EDT+6:0:", + "EST5EDT-6:0:", + // Test invalid hours data in DST Offset. + "EST5EDT25", + "EST5EDT+25", + // Test invalid minutes data in DST offset. + "EST5EDT6:-1:0", + "EST5EDT6:60:0", + "EST5EDT+6:-1:0", + "EST5EDT+6:60:0", + "EST5EDT-6:-1:0", + "EST5EDT-6:60:0", + // Test invalid seconds data in DST offset. + "EST5EDT6:0:-1", + "EST5EDT6:0:60", + "EST5EDT+6:0:-1", + "EST5EDT+6:0:60", + "EST5EDT-6:0:-1", + "EST5EDT-6:0:60", + + // Test extra unnecessary data at the end of DST offset + // (start_date and end_date or optional) + "EST5EDT6AAA", + "EST5EDT+6AAA", + "EST5EDT-6AAA", + "EST5EDT6:59BBB", + "EST5EDT+6:59BBB", + "EST5EDT-6:59BBB", + "EST5EDT6:59:59CCC", + "EST5EDT+6:59:59CCC", + "EST5EDT-6:59:59CCC", + + // Test invalid time offset in start_date + "PST8PDT,M3.2.0/", + "PST8PDT,M3.2.0/24:", + "PST8PDT,M3.2.0/24:59:", + "PST8PDT,M3.2.0/168", + "PST8PDT,M3.2.0/+168", + "PST8PDT,M3.2.0/-168", + "PST8PDT,M3.2.0/24:-1:59", + "PST8PDT,M3.2.0/24:60:59", + "PST8PDT,M3.2.0/24:0:-1", + "PST8PDT,M3.2.0/24:0:60", + // Test invalid time offset in end_date + "PST8PDT,M3.2.0,M11.1.0/", + "PST8PDT,M3.2.0,M11.1.0/24:", + "PST8PDT,M3.2.0,M11.1.0/24:59:", + "PST8PDT,M3.2.0,M11.1.0/168", + "PST8PDT,M3.2.0,M11.1.0/+168", + "PST8PDT,M3.2.0,M11.1.0/-168", + "PST8PDT,M3.2.0,M11.1.0/24:-1:59", + "PST8PDT,M3.2.0,M11.1.0/24:60:59", + "PST8PDT,M3.2.0,M11.1.0/24:0:-1", + "PST8PDT,M3.2.0,M11.1.0/24:0:60", + + // Test invalid data in MonthWeekWeekday + "PST8PDT,M0.2.0,M11.1.0", // Test 0 as month in start_date + "PST8PDT,M13.2.0,M11.1.0", // Test 13 as month in start_date + "PST8PDT,M1.0.0,M11.1.0", // Test 0 as week in start_date + "PST8PDT,M1.6.0,M11.1.0", // Test 6 as week in start_date + "PST8PDT,M1.2.-1,M11.1.0", // Test -1 as weekday in start_date + "PST8PDT,M1.2.7,M11.1.0", // Test 7 as weekday in start_date + "PST8PDT,M0.2.0,M0.1.0", // Test 0 as month in end_date + "PST8PDT,M13.2.0,M13.1.0", // Test 13 as month in end_date + "PST8PDT,M1.0.0,M11.0.0", // Test 0 as week in end_date + "PST8PDT,M1.6.0,M11.6.0", // Test 6 as week in end_date + "PST8PDT,M1.2.-1,M11.1.-1", // Test -1 as weekday in end_date + "PST8PDT,M1.2.7,M11.1.7", // Test 7 as weekday in end_date + "PST8PDT,J0", // Test 0 as nonLeapDay in start_date + "PST8PDT,J366", // Test 366 as nonLeapDay in start_date + "PST8PDT,J1,J0", // Test 0 as nonLeapDay in end_date + "PST8PDT,J1,J366", // Test 366 as nonLeapDay in end_date + "PST8PDT,-1", // Test -1 as LeapDay in start_date + "PST8PDT,366", // Test 366 as LeapDay in start_date + "PST8PDT,1,-1", // Test -1 as LeapDay in end_date + "PST8PDT,1,366", // Test 366 as LeapDay in end_date + + // Test extra unnecessary data at the end. + "PST8PDT,M3.2.0,M11.1.0AA", + "PST8PDT,M3.2.0BB", + "PST8PDT,J59CC", + "PST8PDT,J59,J58DD", + "PST8PDT,59EE", + "PST8PDT,59,58FF", + }; + + for (const auto &timezone : bad_timezones) { + __llvm_libc::cpp::StringView timezone_spec(timezone); + const auto posix_result = + __llvm_libc::time_zone_posix::PosixTimeZone::ParsePosixSpec( + timezone_spec); + ASSERT_FALSE(posix_result.has_value()); + } +} + +TEST(LlvmLibcParsePosixSpec, ValidTest) { + const char *good_timezones[] = { + "EST5", + "EST5:0", + "EST5:59", + "EST5:0:0", + "EST5:0:59", + "EST+5", + "EST+5:0", + "EST+5:59", + "EST+5:0:0", + "EST+5:0:59", + "EST-5", + "EST-5:0", + "EST-5:59", + "EST-5:0:0", + "EST-5:0:59", + "EST5EDT6", + "EST5EDT6/1:2:3", + "EST5EDT6:0", + "EST5EDT6:0/1:2:3", + "EST5EDT6:59", + "EST5EDT6:59/1:2:3", + "EST5EDT6:0:0", + "EST5EDT6:0:0/1:2:3", + "EST5EDT6:0:59", + "EST5EDT6:0:59/1:2:3", + "EST5EDT+6", + "EST5EDT+6/1:2:3", + "EST5EDT+6:0", + "EST5EDT+6:0/1:2:3", + "EST5EDT+6:59", + "EST5EDT+6:59/1:2:3", + "EST5EDT+6:0:0", + "EST5EDT+6:0:0/1:2:3", + "EST5EDT+6:0:59", + "EST5EDT+6:0:59/1:2:3", + "EST5EDT-6", + "EST5EDT-6/1:2:3", + "EST5EDT-6:0", + "EST5EDT-6:0/1:2:3", + "EST5EDT-6:59", + "EST5EDT-6:59/1:2:3", + "EST5EDT-6:0:0", + "EST5EDT-6:0:0/1:2:3", + "EST5EDT-6:0:59", + "EST5EDT-6:0:59/1:2:3", + "PST8PDT,", + "PST8PDT,M3", + "PST8PDT,M3.", + "PST8PDT,M3.2", + "PST8PDT,M3.2.", + "PST8PDT,M3.2.0", + "PST8PDT,M3.2.0,", + "PST8PDT,M3.2.0/1:2:3", + "PST8PDT,M3.2.0,M11", + "PST8PDT,M3.2.0,M11.", + "PST8PDT,M3.2.0,M11.1", + "PST8PDT,M3.2.0,M11.1.", + "PST8PDT,M3.2.0,M11.1.0", + "PST8PDT,M3.2.0,M11.1.0/1:2:3", + "PST8PDT,J59", + "PST8PDT,J59/1:2:3", + "PST8PDT,J59,J58", + "PST8PDT,J59,J58/1:2:3", + "PST8PDT,59", + "PST8PDT,59/1:2:3", + "PST8PDT,59,58", + "PST8PDT,59,58/1:2:3", + }; + + for (const auto &timezone : good_timezones) { + __llvm_libc::cpp::StringView timezone_spec(timezone); + const auto posix_result = + __llvm_libc::time_zone_posix::PosixTimeZone::ParsePosixSpec( + timezone_spec); + // TODO(rtenneti): Use a special matcher to verify the data. + if (!posix_result.has_value()) { + __builtin_printf( + "Testing failed for: %s - expected to succeed but failed\n", + timezone); + } + } +} + +TEST(LlvmLibcParsePosixSpec, ValidTestAndVerify) { + __llvm_libc::cpp::StringView timezone_spec("PST8PDT,M3.2.0,M11.1.0"); + // TODO(rtenneti): Use a special matcher to verify the data. + __llvm_libc::cpp::Optional<__llvm_libc::time_zone_posix::PosixTimeZone> + posix_result = + __llvm_libc::time_zone_posix::PosixTimeZone::ParsePosixSpec( + timezone_spec); + ASSERT_TRUE(posix_result.has_value()); + __llvm_libc::time_zone_posix::PosixTimeZone posix = posix_result.value(); + + __llvm_libc::time_zone_posix::PosixTransition dst_start = + __llvm_libc::tzhelper::testing::initialize_posix_transition_M_data( + static_cast(3), static_cast(2), + static_cast(0), static_cast(7200)); + __llvm_libc::time_zone_posix::PosixTransition dst_end = + __llvm_libc::tzhelper::testing::initialize_posix_transition_M_data( + static_cast(11), static_cast(1), + static_cast(0), static_cast(7200)); + + __llvm_libc::time_zone_posix::PosixTimeZone expected_posix = + __llvm_libc::tzhelper::testing::initialize_posix_time_zone_data( + __llvm_libc::cpp::StringView(""), __llvm_libc::cpp::StringView("PST"), + static_cast(-28800), __llvm_libc::cpp::StringView("PDT"), + static_cast(-25200), dst_start, dst_end); + + EXPECT_POSIX_TIME_ZONE_EQ(expected_posix, posix); +}