From 6ae0c0daac1542e655e2fd2147d88710a132186c Mon Sep 17 00:00:00 2001 From: Anton Kolesnyk <41349689+antkmsft@users.noreply.github.com> Date: Fri, 9 Oct 2020 19:13:48 -0700 Subject: [PATCH] Add DateTime: supporting standardized string date and time representations. (#718) --- sdk/core/azure-core/CMakeLists.txt | 8 +- .../azure-core/inc/azure/core/datetime.hpp | 174 ++++ sdk/core/azure-core/src/datetime.cpp | 792 ++++++++++++++++++ sdk/core/azure-core/test/ut/CMakeLists.txt | 6 +- sdk/core/azure-core/test/ut/datetime.cpp | 459 ++++++++++ 5 files changed, 1433 insertions(+), 6 deletions(-) create mode 100644 sdk/core/azure-core/inc/azure/core/datetime.hpp create mode 100644 sdk/core/azure-core/src/datetime.cpp create mode 100644 sdk/core/azure-core/test/ut/datetime.cpp diff --git a/sdk/core/azure-core/CMakeLists.txt b/sdk/core/azure-core/CMakeLists.txt index 38a395616..9b260c92f 100644 --- a/sdk/core/azure-core/CMakeLists.txt +++ b/sdk/core/azure-core/CMakeLists.txt @@ -26,21 +26,23 @@ add_library ( src/context.cpp src/credentials/credentials.cpp src/credentials/policy/policies.cpp + src/datetime.cpp src/http/body_stream.cpp ${CURL_TRANSPORT_ADAPTER_SRC} src/http/http.cpp src/http/logging_policy.cpp src/http/policy.cpp - src/http/request.cpp src/http/raw_response.cpp + src/http/request.cpp src/http/retry_policy.cpp - src/http/transport_policy.cpp src/http/telemetry_policy.cpp + src/http/transport_policy.cpp src/http/url.cpp ${BUILD_WIN_TRANSPORT} src/logging/logging.cpp src/strings.cpp - src/version.cpp) + src/version.cpp + ) target_include_directories (${TARGET_NAME} PUBLIC $ $) diff --git a/sdk/core/azure-core/inc/azure/core/datetime.hpp b/sdk/core/azure-core/inc/azure/core/datetime.hpp new file mode 100644 index 000000000..a9aed5479 --- /dev/null +++ b/sdk/core/azure-core/inc/azure/core/datetime.hpp @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * @brief Support for date and time standardized string formats. + */ + +#pragma once + +#include +#include + +namespace Azure { namespace Core { + /** + * @brief Manages date and time in standardized string formats. + */ + class DateTime { + public: + /// A type that represents tick spans. + typedef uint64_t IntervalType; + + private: + // Number of seconds between 01-01-1970 and 01-01-1601. + static constexpr IntervalType WindowsToPosixOffsetSeconds = 11644473600LL; + + public: + /** + * @brief Defines the supported date and time string formats. + */ + enum class DateFormat + { + /// RFC 1123. + Rfc1123, + + /// ISO 8601. + Iso8601 + }; + + /** + * @brief Get the current UTC time. + */ + static DateTime UtcNow(); + + /// An invalid UTC timestamp value. + static constexpr IntervalType UtcTimestampInvalid = static_cast(-1); + + /** + * @brief Get seconds since Unix/POSIX time epoch at `01-01-1970 00:00:00`. + * If time is before epoch, @UtcTimestampInvalid is returned. + */ + static IntervalType UtcTimestamp() + { + auto const seconds = UtcNow().ToInterval() / WindowsToPosixOffsetSeconds; + return (seconds >= WindowsToPosixOffsetSeconds) ? (seconds - WindowsToPosixOffsetSeconds) + : UtcTimestampInvalid; + } + + /** + * @brief Construct an uninitialized (!@IsInitialized()) instance of @DateTime. + */ + DateTime() : m_interval(0) {} + + /** + * @brief Create @DateTime from a string representing time in UTC in the specified format. + * + * @param timeString A string with the date and time. + * @param format A format to which /p timeString adheres to. + * + * @return @DateTime that was constructed from the \p timeString; Uninitialized + * (!@IsInitialized()) @DateTime if parsing \p timeString was not successful. + * + * @throw DateTimeException If \p format is not recognized. + */ + static DateTime FromString( + std::string const& timeString, + DateFormat format = DateFormat::Rfc1123); + + /** + * @brief Get a string representation of the @DateTime. + * + * @param format The representation format to use. + * + * @throw DateTimeException If year exceeds 9999, or if \p format is not recognized. + */ + std::string ToString(DateFormat format = DateFormat::Rfc1123) const; + + /// Get the integral time value. + IntervalType ToInterval() const { return m_interval; } + + /// Subtract an interval from @DateTime. + DateTime operator-(IntervalType value) const { return DateTime(m_interval - value); } + + /// Add an interval to @DateTime. + DateTime operator+(IntervalType value) const { return DateTime(m_interval + value); } + + /// Compare two instances of @DateTime for equality. + bool operator==(DateTime dt) const { return m_interval == dt.m_interval; } + + /// Compare two instances of @DateTime for inequality. + bool operator!=(const DateTime& dt) const { return !(*this == dt); } + + /// Compare the chronological order of two @DateTime instances. + bool operator>(const DateTime& dt) const { return this->m_interval > dt.m_interval; } + + /// Compare the chronological order of two @DateTime instances. + bool operator<(const DateTime& dt) const { return this->m_interval < dt.m_interval; } + + /// Compare the chronological order of two @DateTime instances. + bool operator>=(const DateTime& dt) const { return this->m_interval >= dt.m_interval; } + + /// Compare the chronological order of two @DateTime instances. + bool operator<=(const DateTime& dt) const { return this->m_interval <= dt.m_interval; } + + /// Create an interval from milliseconds. + static IntervalType FromMilliseconds(unsigned int milliseconds) + { + return milliseconds * TicksPerMillisecond; + } + + /// Create an interval from seconds. + static IntervalType FromSeconds(unsigned int seconds) { return seconds * TicksPerSecond; } + + /// Create an interval from minutes. + static IntervalType FromMinutes(unsigned int minutes) { return minutes * TicksPerMinute; } + + /// Create an interval from hours. + static IntervalType FromHours(unsigned int hours) { return hours * TicksPerHour; } + + /// Create an interval from days. + static IntervalType FromDays(unsigned int days) { return days * TicksPerDay; } + + /// Checks whether this instance of @DateTime is initialized. + bool IsInitialized() const { return m_interval != 0; } + + private: + friend IntervalType operator-(DateTime t1, DateTime t2); + + static constexpr IntervalType TicksPerMillisecond = static_cast(10000); + static constexpr IntervalType TicksPerSecond = 1000 * TicksPerMillisecond; + static constexpr IntervalType TicksPerMinute = 60 * TicksPerSecond; + static constexpr IntervalType TicksPerHour = 60 * TicksPerMinute; + static constexpr IntervalType TicksPerDay = 24 * TicksPerHour; + + // Private constructor. Use static methods to create an instance. + DateTime(IntervalType interval) : m_interval(interval) {} + + // Storing as hundreds of nanoseconds 10e-7, i.e. 1 here equals 100ns. + IntervalType m_interval; + }; + + inline DateTime::IntervalType operator-(DateTime t1, DateTime t2) + { + auto diff = (t1.m_interval - t2.m_interval); + + // Round it down to seconds + diff /= DateTime::TicksPerSecond; + + return static_cast(diff); + } + + /** + * @brief An exception that gets thrown when @DateTime error occurs. + */ + class DateTimeException : public std::runtime_error { + public: + /** + * @brief Construct with message string. + * + * @param msg Message string. + */ + explicit DateTimeException(std::string const& msg) : std::runtime_error(msg) {} + }; +}} // namespace Azure::Core diff --git a/sdk/core/azure-core/src/datetime.cpp b/sdk/core/azure-core/src/datetime.cpp new file mode 100644 index 000000000..5b07d3380 --- /dev/null +++ b/sdk/core/azure-core/src/datetime.cpp @@ -0,0 +1,792 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "azure/core/datetime.hpp" + +#include +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#else +#include +#endif + +using namespace Azure::Core; + +DateTime DateTime::UtcNow() +{ +#ifdef _WIN32 + FILETIME fileTime = {}; + GetSystemTimeAsFileTime(&fileTime); + + ULARGE_INTEGER largeInt = {}; + largeInt.LowPart = fileTime.dwLowDateTime; + largeInt.HighPart = fileTime.dwHighDateTime; + + return DateTime(largeInt.QuadPart); +#else + struct timeval time = {}; + if (gettimeofday(&time, nullptr) != 0) + { + return DateTime(); + } + + IntervalType result = WindowsToPosixOffsetSeconds + time.tv_sec; + result *= TicksPerSecond; // convert to 10e-7 + result += time.tv_usec * 10; // convert and add microseconds, 10e-6 to 10e-7 + + return DateTime(static_cast(result)); +#endif +} + +namespace { +struct ComputeYearResult +{ + int Year; + int SecondsLeftThisYear; +}; + +constexpr int SecondsInMinute = 60; +constexpr int SecondsInHour = SecondsInMinute * 60; +constexpr int SecondsInDay = 24 * SecondsInHour; + +constexpr int DaysInYear = 365; + +constexpr ComputeYearResult ComputeYear(int64_t secondsSince1900) +{ + constexpr int64_t SecondsFrom1601To1900 = 9435484800LL; + + constexpr int DaysIn4Years = DaysInYear * 4 + 1; + + constexpr int DaysIn100Years = DaysIn4Years * 25 - 1; + constexpr int DaysIn400Years = DaysIn100Years * 4 + 1; + + constexpr int SecondsInYear = SecondsInDay * DaysInYear; + constexpr int SecondsIn4Years = SecondsInDay * DaysIn4Years; + + constexpr int64_t SecondsIn100Years = static_cast(SecondsInDay) * DaysIn100Years; + constexpr int64_t SecondsIn400Years = static_cast(SecondsInDay) * DaysIn400Years; + + int64_t secondsLeft + = secondsSince1900 + SecondsFrom1601To1900; // shift to start of this 400 year cycle + + int year400 = static_cast(secondsLeft / SecondsIn400Years); + secondsLeft -= year400 * SecondsIn400Years; + + int year100 = static_cast(secondsLeft / SecondsIn100Years); + secondsLeft -= year100 * SecondsIn100Years; + + int year4 = static_cast(secondsLeft / SecondsIn4Years); + int secondsInt = static_cast(secondsLeft - static_cast(year4) * SecondsIn4Years); + + int year1 = secondsInt / SecondsInYear; + secondsInt -= year1 * SecondsInYear; + + // shift back to 1900 base from 1601: + return {year400 * 400 + year100 * 100 + year4 * 4 + year1 - 299, secondsInt}; +} + +constexpr bool IsLeapYear(int year) +{ + return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); +} + +// The following table assumes no leap year; leap year is added separately +constexpr unsigned short const CumulativeDaysToMonth[12] = { + 0, // Jan + 31, // Feb + 59, // Mar + 90, // Apr + 120, // May + 151, // Jun + 181, // Jul + 212, // Aug + 243, // Sep + 273, // Oct + 304, // Nov + 334 // Dec +}; + +constexpr unsigned short const CumulativeDaysToMonthLeap[12] = { + 0, // Jan + 31, // Feb + 60, // Mar + 91, // Apr + 121, // May + 152, // Jun + 182, // Jul + 213, // Aug + 244, // Sep + 274, // Oct + 305, // Nov + 335 // Dec +}; + +constexpr char const dayNames[] = "Sun\0Mon\0Tue\0Wed\0Thu\0Fri\0Sat"; +constexpr char const monthNames[] = "Jan\0Feb\0Mar\0Apr\0May\0Jun\0Jul\0Aug\0Sep\0Oct\0Nov\0Dec"; + +constexpr int64_t TicksFromWindowsEpochTo1900 = 0x014F373BFDE04000LL; +} // namespace + +std::string DateTime::ToString(DateFormat format) const +{ + if (m_interval > 2650467743990000000LL) + { + throw DateTimeException("The requested year exceeds the year 9999."); + } + + int64_t const epochAdjusted = static_cast(m_interval) - TicksFromWindowsEpochTo1900; + int64_t const secondsSince1900 = epochAdjusted / TicksPerSecond; // convert to seconds + int const fracSec = static_cast(epochAdjusted % TicksPerSecond); + + auto const yearData = ComputeYear(secondsSince1900); + int const year = yearData.Year; + int const yearDay = yearData.SecondsLeftThisYear / SecondsInDay; + + int leftover = yearData.SecondsLeftThisYear % SecondsInDay; + int const hour = leftover / SecondsInHour; + + leftover = leftover % SecondsInHour; + int const minute = leftover / SecondsInMinute; + + leftover = leftover % SecondsInMinute; + + auto const& monthTable = IsLeapYear(year) ? CumulativeDaysToMonthLeap : CumulativeDaysToMonth; + int month = 0; + while (month < 11 && monthTable[month + 1] <= yearDay) + { + ++month; + } + + auto const monthDay = yearDay - monthTable[month] + 1; + auto const weekday = static_cast((secondsSince1900 / SecondsInDay + 1) % 7); + + char outBuffer[38]{}; // Thu, 01 Jan 1970 00:00:00 GMT\0 + // 1970-01-01T00:00:00.1234567Z\0 + + char* outCursor = outBuffer; + switch (format) + { + case DateFormat::Rfc1123: +#ifdef _MSC_VER + sprintf_s( +#else + std::sprintf( +#endif + outCursor, +#ifdef _MSC_VER + 26, +#endif + "%s, %02d %s %04d %02d:%02d:%02d", + dayNames + 4ULL * static_cast(weekday), + monthDay, + monthNames + 4ULL * static_cast(month), + year + 1900, + hour, + minute, + leftover); + + outCursor += 25; + memcpy(outCursor, " GMT", 4); + outCursor += 4; + return std::string(outBuffer, outCursor); + + case DateFormat::Iso8601: +#ifdef _MSC_VER + sprintf_s( +#else + std::sprintf( +#endif + outCursor, +#ifdef _MSC_VER + 20, +#endif + "%04d-%02d-%02dT%02d:%02d:%02d", + year + 1900, + month + 1, + monthDay, + hour, + minute, + leftover); + + outCursor += 19; + if (fracSec != 0) + { + // Append fractional second, which is a 7-digit value with no trailing zeros + // This way, '1200' becomes '00012' + size_t appended = +#ifdef _MSC_VER + sprintf_s( +#else + std::sprintf( +#endif + outCursor, +#ifdef _MSC_VER + 9, +#endif + ".%07d", + fracSec); + + while (outCursor[appended - 1] == '0') + { + --appended; // trim trailing zeros + } + + outCursor += appended; + } + + *outCursor = 'Z'; + ++outCursor; + return std::string(outBuffer, outCursor); + + default: + throw DateTimeException("Unrecognized date format."); + } +} + +namespace { +constexpr bool StringStartsWith(char const* haystack, char const* needle) +{ + while (*needle) + { + if (*haystack != *needle) + { + return false; + } + + ++haystack; + ++needle; + } + + return true; +} + +template constexpr bool IsDigit(char c) +{ + return ((unsigned char)((unsigned char)(c) - '0') <= N); +} + +constexpr int StringToDoubleDigitInt(char const* str) +{ + return (static_cast(str[0]) - '0') * 10 + + (static_cast(str[1]) - '0'); +} + +constexpr bool ValidateDay(int day, int month, int year) +{ + if (day < 1) + { + return false; + } + + // Month is 0-based + switch (month) + { + case 1: // Feb + return day <= (28 + IsLeapYear(year)); + break; + case 3: // Apr + case 5: // Jun + case 8: // Sep + case 10: // Nov + return day <= 30; + break; + default: + return day <= 31; + } +} + +constexpr int GetYearDay(int month, int monthDay, int year) +{ + return CumulativeDaysToMonth[month] + monthDay + (IsLeapYear(year) && month > 1) - 1; +} + +constexpr int CountLeapYears(int yearsSince1900) +{ + int tmpYears = yearsSince1900 + 299; // shift into 1601, the first 400 year cycle including 1900 + + int const year400 = tmpYears / 400; + tmpYears -= year400 * 400; + + int result = year400 * 97; + + int const year100 = tmpYears / 100; + tmpYears -= year100 * 100; + result += year100 * 24; + + result += tmpYears / 4; + + // subtract off leap years from 1601 + result -= 72; + + return result; +} + +constexpr int64_t AdjustTimezone( + int64_t result, + unsigned char chSign, + int adjustHours, + int adjustMinutes) +{ + if (adjustHours > 23) + { + return -1; + } + + // adjustMinutes > 59 is impossible due to digit 5 check + int const tzAdjust = adjustMinutes * 60 + adjustHours * 60 * 60; + if (chSign == '-') + { + if (std::numeric_limits::max() - result < tzAdjust) + { + return -1; + } + + result += tzAdjust; + } + else + { + if (tzAdjust > result) + { + return -1; + } + + result -= tzAdjust; + } + + return result; +} +} // namespace + +/* +https://tools.ietf.org/html/rfc822 +https://tools.ietf.org/html/rfc1123 + +date-time = [ day "," ] date time ; dd mm yy + ; hh:mm:ss zzz + +day = "Mon" / "Tue" / "Wed" / "Thu" + / "Fri" / "Sat" / "Sun" + +date = 1*2DIGIT month 2DIGIT ; day month year + ; e.g. 20 Jun 82 +RFC1123 changes this to: +date = 1*2DIGIT month 2*4DIGIT ; day month year + ; e.g. 20 Jun 1982 +This implementation only accepts 4 digit years. + +month = "Jan" / "Feb" / "Mar" / "Apr" + / "May" / "Jun" / "Jul" / "Aug" + / "Sep" / "Oct" / "Nov" / "Dec" + +time = hour zone ; ANSI and Military + +hour = 2DIGIT ":" 2DIGIT [":" 2DIGIT] + ; 00:00:00 - 23:59:59 + +zone = "UT" / "GMT" ; Universal Time + ; North American : UT + / "EST" / "EDT" ; Eastern: - 5/ - 4 + / "CST" / "CDT" ; Central: - 6/ - 5 + / "MST" / "MDT" ; Mountain: - 7/ - 6 + / "PST" / "PDT" ; Pacific: - 8/ - 7 + +// military time deleted by RFC 1123 + + / ( ("+" / "-") 4DIGIT ) ; Local differential + ; hours+min. (HHMM) +*/ + +DateTime DateTime::FromString(std::string const& dateString, DateFormat format) +{ + DateTime result = {}; + + int64_t secondsSince1900 = 0; + uint64_t fracSec = 0; + + auto str = dateString.c_str(); + if (format == DateFormat::Rfc1123) + { + int parsedWeekday = 0; + for (; parsedWeekday < 7; ++parsedWeekday) + { + if (StringStartsWith(str, dayNames + static_cast(parsedWeekday) * 4ULL) + && str[3] == ',' && str[4] == ' ') + { + str += 5; // parsed day of week + break; + } + } + + int monthDay; + if (IsDigit<3>(str[0]) && IsDigit(str[1]) && str[2] == ' ') + { + monthDay = StringToDoubleDigitInt(str); // validity checked later + str += 3; // parsed day + } + else if (IsDigit(str[0]) && str[1] == ' ') + { + monthDay = str[0] - '0'; + str += 2; // parsed day + } + else + { + return result; + } + + if (monthDay == 0) + { + return result; + } + + int month = 0; + for (;;) + { + if (StringStartsWith(str, monthNames + static_cast(month) * 4ULL)) + { + break; + } + + ++month; + if (month == 12) + { + return result; + } + } + + if (str[3] != ' ') + { + return result; + } + + str += 4; // parsed month + + if (!IsDigit(str[0]) || !IsDigit(str[1]) || !IsDigit(str[2]) || !IsDigit(str[3]) + || str[4] != ' ') + { + return result; + } + + int year = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0'); + if (year < 1900) + { + return result; + } + + // days in month validity check + if (!ValidateDay(monthDay, month, year)) + { + return result; + } + + str += 5; // parsed year + int const yearDay = GetYearDay(month, monthDay, year); + + if (!IsDigit<2>(str[0]) || !IsDigit(str[1]) || str[2] != ':' || !IsDigit<5>(str[3]) + || !IsDigit(str[4])) + { + return result; + } + + int const hour = StringToDoubleDigitInt(str); + if (hour > 23) + { + return result; + } + str += 3; // parsed hour + + int const minute = StringToDoubleDigitInt(str); + str += 2; // parsed mins + + int sec = 0; + if (str[0] == ':') + { + if (!IsDigit<6>(str[1]) || !IsDigit(str[2]) || str[3] != ' ') + { + return result; + } + + sec = StringToDoubleDigitInt(str + 1); + str += 4; // parsed seconds + } + else if (str[0] == ' ') + { + str += 1; // parsed seconds + } + else + { + return result; + } + + if (sec > 60) + { // 60 to allow leap seconds + return result; + } + + year -= 1900; + int const daysSince1900 = year * DaysInYear + CountLeapYears(year) + yearDay; + + if (parsedWeekday != 7) + { + int const actualWeekday = (daysSince1900 + 1) % 7; + if (parsedWeekday != actualWeekday) + { + return result; + } + } + + secondsSince1900 = static_cast(daysSince1900) * SecondsInDay + + static_cast(hour) * SecondsInHour + + static_cast(minute) * SecondsInMinute + sec; + + fracSec = 0; + if (!StringStartsWith(str, "GMT") && !StringStartsWith(str, "UT")) + { + // some timezone adjustment necessary + auto tzCh = '-'; + int tzHours; + int tzMinutes = 0; + if (StringStartsWith(str, "EDT")) + { + tzHours = 4; + } + else if (StringStartsWith(str, "EST") || StringStartsWith(str, "CDT")) + { + tzHours = 5; + } + else if (StringStartsWith(str, "CST") || StringStartsWith(str, "MDT")) + { + tzHours = 6; + } + else if (StringStartsWith(str, "MST") || StringStartsWith(str, "PDT")) + { + tzHours = 7; + } + else if (StringStartsWith(str, "PST")) + { + tzHours = 8; + } + else if ( + (str[0] == '+' || str[0] == '-') && IsDigit<2>(str[1]) && IsDigit(str[2]) + && IsDigit<5>(str[3]) && IsDigit(str[4])) + { + tzCh = str[0]; + tzHours = StringToDoubleDigitInt(str + 1); + tzMinutes = StringToDoubleDigitInt(str + 3); + } + else + { + return result; + } + + secondsSince1900 + = AdjustTimezone(secondsSince1900, static_cast(tzCh), tzHours, tzMinutes); + + if (secondsSince1900 < 0) + { + return result; + } + } + } + else if (format == DateFormat::Iso8601) + { + // parse year + if (!IsDigit(str[0]) || !IsDigit(str[1]) || !IsDigit(str[2]) || !IsDigit(str[3])) + { + return result; + } + + int year = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0'); + if (year < 1900) + { + return result; + } + + str += 4; + if (*str == '-') + { + ++str; + } + + // parse month + if (!IsDigit<1>(str[0]) || !IsDigit(str[1])) + { + return result; + } + + int month = StringToDoubleDigitInt(str); + if (month < 1 || month > 12) + { + return result; + } + + month -= 1; + str += 2; + + if (*str == '-') + { + ++str; + } + + // parse day + if (!IsDigit<3>(str[0]) || !IsDigit(str[1])) + { + return result; + } + + int monthDay = StringToDoubleDigitInt(str); + if (!ValidateDay(monthDay, month, year)) + { + return result; + } + + int const yearDay = GetYearDay(month, monthDay, year); + + str += 2; + year -= 1900; + int daysSince1900 = year * DaysInYear + CountLeapYears(year) + yearDay; + + if (str[0] != 'T' && str[0] != 't') + { + // No time + secondsSince1900 = static_cast(daysSince1900) * SecondsInDay; + + result.m_interval = static_cast( + secondsSince1900 * TicksPerSecond + fracSec + TicksFromWindowsEpochTo1900); + + return result; + } + + ++str; // skip 'T' + + // parse hour + if (!IsDigit<2>(str[0]) || !IsDigit(str[1])) + { + return result; + } + + int const hour = StringToDoubleDigitInt(str); + str += 2; + if (hour > 23) + { + return result; + } + + if (*str == ':') + { + ++str; + } + + // parse minute + if (!IsDigit<5>(str[0]) || !IsDigit(str[1])) + { + return result; + } + + int const minute = StringToDoubleDigitInt(str); + // minute > 59 is impossible because we checked that the first digit is <= 5 in the basic format + // check above + + str += 2; + + if (*str == ':') + { + ++str; + } + + // parse seconds + if (!IsDigit<6>(str[0]) || !IsDigit(str[1])) + { + return result; + } + + int const sec = StringToDoubleDigitInt(str); + // We allow 60 to account for leap seconds + if (sec > 60) + { + return result; + } + + str += 2; + if (str[0] == '.' && IsDigit(str[1])) + { + ++str; + int digits = 7; + for (;;) + { + fracSec *= 10; + fracSec += static_cast(*str) - static_cast('0'); + + --digits; + ++str; + + if (digits == 0) + { + while (IsDigit(*str)) + { + // consume remaining fractional second digits we can't use + ++str; + } + + break; + } + + if (!IsDigit(*str)) + { + // no more digits in the input, do the remaining multiplies we need + for (; digits != 0; --digits) + { + fracSec *= 10; + } + + break; + } + } + } + + secondsSince1900 = static_cast(daysSince1900) * SecondsInDay + + static_cast(hour) * SecondsInHour + + static_cast(minute) * SecondsInMinute + sec; + + if (str[0] == 'Z' || str[0] == 'z') + { + // no adjustment needed for zulu time + } + else if (str[0] == '+' || str[0] == '-') + { + unsigned char const offsetDirection = static_cast(str[0]); + if (!IsDigit<2>(str[1]) || !IsDigit(str[2]) || str[3] != ':' || !IsDigit<5>(str[4]) + || !IsDigit(str[5])) + { + return result; + } + + secondsSince1900 = AdjustTimezone( + secondsSince1900, + offsetDirection, + StringToDoubleDigitInt(str + 1), + StringToDoubleDigitInt(str + 4)); + if (secondsSince1900 < 0) + { + return result; + } + } + else + { + // the timezone is malformed, but cpprestsdk currently accepts this as no timezone + } + } + else + { + throw DateTimeException("Unrecognized date format."); + } + + result.m_interval = static_cast( + secondsSince1900 * TicksPerSecond + fracSec + TicksFromWindowsEpochTo1900); + + return result; +} diff --git a/sdk/core/azure-core/test/ut/CMakeLists.txt b/sdk/core/azure-core/test/ut/CMakeLists.txt index 93e4a2871..a1059749b 100644 --- a/sdk/core/azure-core/test/ut/CMakeLists.txt +++ b/sdk/core/azure-core/test/ut/CMakeLists.txt @@ -22,18 +22,18 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) include(GoogleTest) add_executable ( ${TARGET_NAME} + context.cpp + datetime.cpp http.cpp - http.hpp logging.cpp main.cpp nullable.cpp string.cpp telemetry_policy.cpp transport_adapter.cpp - transport_adapter.hpp transport_adapter_file_upload.cpp uuid.cpp - context.cpp) + ) target_link_libraries(${TARGET_NAME} PRIVATE azure-core gtest) diff --git a/sdk/core/azure-core/test/ut/datetime.cpp b/sdk/core/azure-core/test/ut/datetime.cpp new file mode 100644 index 000000000..2658a3c32 --- /dev/null +++ b/sdk/core/azure-core/test/ut/datetime.cpp @@ -0,0 +1,459 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "gtest/gtest.h" + +#include + +#include + +using namespace Azure::Core; + +TEST(DateTime, ParseDateAndTimeBasic) +{ + auto dt1 = DateTime::FromString("20130517T00:00:00Z", DateTime::DateFormat::Iso8601); + EXPECT_NE(0u, dt1.ToInterval()); + + auto dt2 = DateTime::FromString("Fri, 17 May 2013 00:00:00 GMT", DateTime::DateFormat::Rfc1123); + EXPECT_NE(0u, dt2.ToInterval()); + + EXPECT_EQ(dt1.ToInterval(), dt2.ToInterval()); +} + +TEST(DateTime, ParseDateAndTimeExtended) +{ + auto dt1 = DateTime::FromString("2013-05-17T00:00:00Z", DateTime::DateFormat::Iso8601); + EXPECT_NE(0u, dt1.ToInterval()); + + auto dt2 = DateTime::FromString("Fri, 17 May 2013 00:00:00 GMT", DateTime::DateFormat::Rfc1123); + EXPECT_NE(0u, dt2.ToInterval()); + + EXPECT_EQ(dt1.ToInterval(), dt2.ToInterval()); +} + +TEST(DateTime, ParseDateBasic) +{ + { + auto dt = DateTime::FromString("20130517", DateTime::DateFormat::Iso8601); + EXPECT_NE(0u, dt.ToInterval()); + } +} + +TEST(DateTime, ParseDateExtended) +{ + { + auto dt = DateTime::FromString("2013-05-17", DateTime::DateFormat::Iso8601); + EXPECT_NE(0u, dt.ToInterval()); + } +} + +namespace { +void TestDateTimeRoundtrip(std::string const& str, std::string const& strExpected) +{ + auto dt = DateTime::FromString(str, DateTime::DateFormat::Iso8601); + auto const str2 = dt.ToString(DateTime::DateFormat::Iso8601); + EXPECT_EQ(str2, strExpected); +} + +void TestDateTimeRoundtrip(std::string const& str) { TestDateTimeRoundtrip(str, str); } +} // namespace + +TEST(DateTime, ParseTimeRoundrip1) +{ + // Preserve all 7 digits after the comma: + TestDateTimeRoundtrip("2013-11-19T14:30:59.1234567Z"); +} + +TEST(DateTime, ParseTimeRoundrip2) +{ + // lose the last '000' + TestDateTimeRoundtrip("2013-11-19T14:30:59.1234567000Z", "2013-11-19T14:30:59.1234567Z"); + + // lose the last '999' without rounding up + TestDateTimeRoundtrip("2013-11-19T14:30:59.1234567999Z", "2013-11-19T14:30:59.1234567Z"); +} + +TEST(DateTime, ParseTimeRoundrip3) +{ + // leading 0-s after the comma, tricky to parse correctly + TestDateTimeRoundtrip("2013-11-19T14:30:59.00123Z"); +} + +TEST(DateTime, ParseTimeRoundrip4) +{ + // another leading 0 test + TestDateTimeRoundtrip("2013-11-19T14:30:59.0000001Z"); +} + +TEST(DateTime, ParseTimeRoundrip5) +{ + // this is going to be truncated + TestDateTimeRoundtrip("2013-11-19T14:30:59.00000001Z", "2013-11-19T14:30:59Z"); +} + +TEST(DateTime, ParseTimeRoundrip6) +{ + // Only one digit after the dot + TestDateTimeRoundtrip("2013-11-19T14:30:59.5Z"); +} + +TEST(DateTime, ParseTimeRoundripYear1900) { TestDateTimeRoundtrip("1900-01-01T00:00:00Z"); } + +TEST(DateTime, ParseTimeRoundripYear9999) { TestDateTimeRoundtrip("9999-12-31T23:59:59Z"); } + +TEST(DateTime, EmittingTimeCorrectDay) +{ + auto const test = DateTime() + 132004507640000000LL; // 2019-04-22T23:52:44 is a Monday + auto const actual = test.ToString(DateTime::DateFormat::Rfc1123); + std::string const expected("Mon"); + EXPECT_EQ(actual.substr(0, 3), expected); +} + +namespace { +void TestRfc1123IsTimeT(char const* str, DateTime::IntervalType t) +{ + auto const dt = DateTime::FromString(str, DateTime::DateFormat::Rfc1123); + auto interval = dt.ToInterval(); + EXPECT_EQ(0, interval % 10000000); + interval /= 10000000; + interval -= 11644473600; // NT epoch adjustment + EXPECT_EQ(t, interval); +} +} // namespace + +TEST(DateTime, ParseTimeRfc1123AcceptsEachDay) +{ + TestRfc1123IsTimeT("01 Jan 1970 00:00:00 GMT", 0); + TestRfc1123IsTimeT("Fri, 02 Jan 1970 00:00:00 GMT", 86400 * 1); + TestRfc1123IsTimeT("Sat, 03 Jan 1970 00:00:00 GMT", 86400 * 2); + TestRfc1123IsTimeT("Sun, 04 Jan 1970 00:00:00 GMT", 86400 * 3); + TestRfc1123IsTimeT("Mon, 05 Jan 1970 00:00:00 GMT", 86400 * 4); + TestRfc1123IsTimeT("Tue, 06 Jan 1970 00:00:00 GMT", 86400 * 5); + TestRfc1123IsTimeT("Wed, 07 Jan 1970 00:00:00 GMT", 86400 * 6); +} + +TEST(DateTime, ParseTimeRfc1123BoundaryCases) +{ + TestRfc1123IsTimeT("01 Jan 1970 00:00:00 GMT", 0); + TestRfc1123IsTimeT("19 Jan 2038 03:14:06 GMT", std::numeric_limits::max() - 1); + TestRfc1123IsTimeT("19 Jan 2038 03:13:07 -0001", std::numeric_limits::max()); + TestRfc1123IsTimeT("19 Jan 2038 03:14:07 -0000", std::numeric_limits::max()); + TestRfc1123IsTimeT("14 Jan 2019 23:16:21 +0000", 1547507781); + TestRfc1123IsTimeT("14 Jan 2019 23:16:21 -0001", 1547507841); + TestRfc1123IsTimeT("14 Jan 2019 23:16:21 +0001", 1547507721); + TestRfc1123IsTimeT("14 Jan 2019 23:16:21 -0100", 1547511381); + TestRfc1123IsTimeT("14 Jan 2019 23:16:21 +0100", 1547504181); +} + +TEST(DateTime, ParseTimeRfc1123UseEachField) +{ + TestRfc1123IsTimeT("02 Jan 1970 00:00:00 GMT", 86400); + TestRfc1123IsTimeT("12 Jan 1970 00:00:00 GMT", 950400); + TestRfc1123IsTimeT("01 Feb 1970 00:00:00 GMT", 2678400); + TestRfc1123IsTimeT("01 Jan 2000 00:00:00 GMT", 946684800); + TestRfc1123IsTimeT("01 Jan 2100 00:00:00 GMT", 4102444800); + TestRfc1123IsTimeT("01 Jan 1990 00:00:00 GMT", 631152000); + TestRfc1123IsTimeT("01 Jan 1971 00:00:00 GMT", 31536000); + TestRfc1123IsTimeT("01 Jan 1970 10:00:00 GMT", 36000); + TestRfc1123IsTimeT("01 Jan 1970 01:00:00 GMT", 3600); + TestRfc1123IsTimeT("01 Jan 1970 00:10:00 GMT", 600); + TestRfc1123IsTimeT("01 Jan 1970 00:01:00 GMT", 60); + TestRfc1123IsTimeT("01 Jan 1970 00:00:10 GMT", 10); + TestRfc1123IsTimeT("01 Jan 1970 00:00:01 GMT", 1); + TestRfc1123IsTimeT("01 Jan 1970 10:00:00 GMT", 36000); + TestRfc1123IsTimeT("01 Jan 1970 02:00:00 PST", 36000); + TestRfc1123IsTimeT("01 Jan 1970 03:00:00 PDT", 36000); + TestRfc1123IsTimeT("01 Jan 1970 03:00:00 MST", 36000); + TestRfc1123IsTimeT("01 Jan 1970 04:00:00 MDT", 36000); + TestRfc1123IsTimeT("01 Jan 1970 04:00:00 CST", 36000); + TestRfc1123IsTimeT("01 Jan 1970 05:00:00 CDT", 36000); + TestRfc1123IsTimeT("01 Jan 1970 05:00:00 EST", 36000); + TestRfc1123IsTimeT("01 Jan 1970 06:00:00 EDT", 36000); + TestRfc1123IsTimeT("01 Jan 1970 06:00:00 -0400", 36000); + TestRfc1123IsTimeT("01 Jan 1970 05:59:00 -0401", 36000); +} + +TEST(DateTime, ParseTimeRfc1123MaxDays) +{ + TestRfc1123IsTimeT("31 Jan 1970 00:00:00 GMT", 2592000); + TestRfc1123IsTimeT("28 Feb 2019 00:00:00 GMT", 1551312000); // non leap year allows feb 28 + TestRfc1123IsTimeT("29 Feb 2020 00:00:00 GMT", 1582934400); // leap year allows feb 29 + TestRfc1123IsTimeT("31 Mar 1970 00:00:00 GMT", 7689600); + TestRfc1123IsTimeT("30 Apr 1970 00:00:00 GMT", 10281600); + TestRfc1123IsTimeT("31 May 1970 00:00:00 GMT", 12960000); + TestRfc1123IsTimeT("30 Jun 1970 00:00:00 GMT", 15552000); + TestRfc1123IsTimeT("31 Jul 1970 00:00:00 GMT", 18230400); + TestRfc1123IsTimeT("31 Aug 1970 00:00:00 GMT", 20908800); + TestRfc1123IsTimeT("30 Sep 1970 00:00:00 GMT", 23500800); + TestRfc1123IsTimeT("31 Oct 1970 00:00:00 GMT", 26179200); + TestRfc1123IsTimeT("30 Nov 1970 00:00:00 GMT", 28771200); + TestRfc1123IsTimeT("31 Dec 1970 00:00:00 GMT", 31449600); +} + +TEST(DateTime, ParseTimeRfc1123InvalidCases) +{ + std::string const badStrings[] = { + "Ahu, 01 Jan 1970 00:00:00 GMT", // bad letters in each place + "TAu, 01 Jan 1970 00:00:00 GMT", + "ThA, 01 Jan 1970 00:00:00 GMT", + "ThuA 01 Jan 1970 00:00:00 GMT", + "Thu,A01 Jan 1970 00:00:00 GMT", + "Thu, A1 Jan 1970 00:00:00 GMT", + "Thu, 0A Jan 1970 00:00:00 GMT", + "Thu, 01AJan 1970 00:00:00 GMT", + "Thu, 01 Aan 1970 00:00:00 GMT", + "Thu, 01 JAn 1970 00:00:00 GMT", + "Thu, 01 JaA 1970 00:00:00 GMT", + "Thu, 01 JanA1970 00:00:00 GMT", + "Thu, 01 Jan A970 00:00:00 GMT", + "Thu, 01 Jan 1A70 00:00:00 GMT", + "Thu, 01 Jan 19A0 00:00:00 GMT", + "Thu, 01 Jan 197A 00:00:00 GMT", + "Thu, 01 Jan 1970A00:00:00 GMT", + "Thu, 01 Jan 1970 A0:00:00 GMT", + "Thu, 01 Jan 1970 0A:00:00 GMT", + "Thu, 01 Jan 1970 00A00:00 GMT", + "Thu, 01 Jan 1970 00:A0:00 GMT", + "Thu, 01 Jan 1970 00:0A:00 GMT", + "Thu, 01 Jan 1970 00:00A00 GMT", + "Thu, 01 Jan 1970 00:00:A0 GMT", + "Thu, 01 Jan 1970 00:00:0A GMT", + "Thu, 01 Jan 1970 00:00:00AGMT", + "Thu, 01 Jan 1970 00:00:00 AMT", + "Thu, 01 Jan 1970 00:00:00 GAT", + "Thu, 01 Jan 1970 00:00:00 GMA", + "", // truncation + "T", + "Th", + "Thu", + "Thu,", + "Thu, ", + "Thu, 0", + "Thu, 01", + "Thu, 01 ", + "Thu, 01 J", + "Thu, 01 Ja", + "Thu, 01 Jan", + "Thu, 01 Jan ", + "Thu, 01 Jan 1", + "Thu, 01 Jan 19", + "Thu, 01 Jan 197", + "Thu, 01 Jan 1970", + "Thu, 01 Jan 1970 ", + "Thu, 01 Jan 1970 0", + "Thu, 01 Jan 1970 00", + "Thu, 01 Jan 1970 00:", + "Thu, 01 Jan 1970 00:0", + "Thu, 01 Jan 1970 00:00", + "Thu, 01 Jan 1970 00:00:", + "Thu, 01 Jan 1970 00:00:0", + "Thu, 01 Jan 1970 00:00:00", + "Thu, 01 Jan 1970 00:00:00 ", + "Thu, 01 Jan 1970 00:00:00 G", + "Thu, 01 Jan 1970 00:00:00 GM", + "Fri, 01 Jan 1970 00:00:00 GMT", // wrong day + "01 Jan 1899 00:00:00 GMT", // year too small + "01 Xxx 1971 00:00:00 GMT", // month bad + "00 Jan 1971 00:00:00 GMT", // day too small + "32 Jan 1971 00:00:00 GMT", // day too big + "30 Feb 1971 00:00:00 GMT", // day too big for feb + "30 Feb 1971 00:00:00 GMT", // day too big for feb (non-leap year) + "32 Mar 1971 00:00:00 GMT", // other months + "31 Apr 1971 00:00:00 GMT", + "32 May 1971 00:00:00 GMT", + "31 Jun 1971 00:00:00 GMT", + "32 Jul 1971 00:00:00 GMT", + "32 Aug 1971 00:00:00 GMT", + "31 Sep 1971 00:00:00 GMT", + "32 Oct 1971 00:00:00 GMT", + "31 Nov 1971 00:00:00 GMT", + "32 Dec 1971 00:00:00 GMT", + "01 Jan 1971 70:00:00 GMT", // hour too big + "01 Jan 1971 24:00:00 GMT", + "01 Jan 1971 00:60:00 GMT", // minute too big + "01 Jan 1971 00:00:70 GMT", // second too big + "01 Jan 1971 00:00:61 GMT", + "01 Jan 1899 00:00:00 GMT", // underflow + "01 Jan 1969 00:00:00 CEST", // bad tz + "14 Jan 2019 23:16:21 G0100", // bad tzoffsets + "01 Jan 1970 00:00:00 +2400", + "01 Jan 1970 00:00:00 -3000", + "01 Jan 1970 00:00:00 +2160", + "01 Jan 1970 00:00:00 -2400", + "01 Jan 1970 00:00:00 -2160", + "00 Jan 1971 00:00:00 GMT", // zero month day + }; + + for (auto const& str : badStrings) + { + auto const dt = DateTime::FromString(str, DateTime::DateFormat::Rfc1123); + EXPECT_EQ(0, dt.ToInterval()); + } +} + +TEST(DateTime, ParseTimeIso8601BoundaryCases) +{ + // boundary cases: + TestDateTimeRoundtrip("1970-01-01T00:00:00Z"); // epoch + TestDateTimeRoundtrip("2038-01-19T03:14:06+00:00", "2038-01-19T03:14:06Z"); // INT_MAX - 1 + TestDateTimeRoundtrip( + "2038-01-19T03:13:07-00:01", + "2038-01-19T03:14:07Z"); // INT_MAX after subtacting 1 + TestDateTimeRoundtrip("2038-01-19T03:14:07-00:00", "2038-01-19T03:14:07Z"); +} + +TEST(DateTime, ParseTimeIso8601UsesEachTimezoneDigit) +{ + TestDateTimeRoundtrip("2019-01-14T23:16:21+00:00", "2019-01-14T23:16:21Z"); + TestDateTimeRoundtrip("2019-01-14T23:16:21-00:01", "2019-01-14T23:17:21Z"); + TestDateTimeRoundtrip("2019-01-14T23:16:21+00:01", "2019-01-14T23:15:21Z"); + TestDateTimeRoundtrip("2019-01-14T23:16:21-01:00", "2019-01-15T00:16:21Z"); + TestDateTimeRoundtrip("2019-01-14T23:16:21+01:00", "2019-01-14T22:16:21Z"); +} + +TEST(DateTime, ParseTimeIso8601UsesEachDigit) +{ + TestDateTimeRoundtrip("1970-01-01T00:00:01Z"); + TestDateTimeRoundtrip("1970-01-01T00:01:00Z"); + TestDateTimeRoundtrip("1970-01-01T01:00:00Z"); + TestDateTimeRoundtrip("1970-01-02T00:00:00Z"); + TestDateTimeRoundtrip("1970-02-01T00:00:00Z"); + TestDateTimeRoundtrip("1971-01-01T00:00:00Z"); + + TestDateTimeRoundtrip("1999-01-01T00:00:00Z"); + TestDateTimeRoundtrip("1970-12-01T00:00:00Z"); + TestDateTimeRoundtrip("1970-09-01T00:00:00Z"); + TestDateTimeRoundtrip("1970-01-30T00:00:00Z"); + TestDateTimeRoundtrip("1970-01-31T00:00:00Z"); + TestDateTimeRoundtrip("1970-01-01T23:00:00Z"); + TestDateTimeRoundtrip("1970-01-01T19:00:00Z"); + TestDateTimeRoundtrip("1970-01-01T00:59:00Z"); + TestDateTimeRoundtrip("1970-01-01T00:00:59Z"); + TestDateTimeRoundtrip("1970-01-01T00:00:60Z", "1970-01-01T00:01:00Z"); // leap seconds +} + +TEST(DateTime, ParseTimeIso8601AcceptsMonthMaxDays) +{ + TestDateTimeRoundtrip("1970-01-31T00:00:00Z"); // jan + TestDateTimeRoundtrip("2019-02-28T00:00:00Z"); // non leap year allows feb 28 + TestDateTimeRoundtrip("2020-02-29T00:00:00Z"); // leap year allows feb 29 + TestDateTimeRoundtrip("1970-03-31T00:00:00Z"); // mar + TestDateTimeRoundtrip("1970-04-30T00:00:00Z"); // apr + TestDateTimeRoundtrip("1970-05-31T00:00:00Z"); // may + TestDateTimeRoundtrip("1970-06-30T00:00:00Z"); // jun + TestDateTimeRoundtrip("1970-07-31T00:00:00Z"); // jul + TestDateTimeRoundtrip("1970-08-31T00:00:00Z"); // aug + TestDateTimeRoundtrip("1970-09-30T00:00:00Z"); // sep + TestDateTimeRoundtrip("1970-10-31T00:00:00Z"); // oct + TestDateTimeRoundtrip("1970-11-30T00:00:00Z"); // nov + TestDateTimeRoundtrip("1970-12-31T00:00:00Z"); // dec +} + +TEST(DateTime, ParseTimeIso8601AcceptsLowercaseTZ) +{ + TestDateTimeRoundtrip("1970-01-01t00:00:00Z", "1970-01-01T00:00:00Z"); + TestDateTimeRoundtrip("1970-01-01T00:00:00z", "1970-01-01T00:00:00Z"); +} + +TEST(DateTime, ParseTimeRoundtripAcceptsInvalidNoTrailingTimezone) +{ + // No digits after the dot, or non-digits. This is not a valid input, but we should not choke on + // it, Simply ignore the bad fraction + std::string const badStrings[] = {"2013-11-19T14:30:59.Z", "2013-11-19T14:30:59.a12Z"}; + std::string const strCorrected = "2013-11-19T14:30:59Z"; + + for (auto const& str : badStrings) + { + auto const dt = DateTime::FromString(str, DateTime::DateFormat::Iso8601); + auto const str2 = dt.ToString(DateTime::DateFormat::Iso8601); + EXPECT_EQ(str2, strCorrected); + } +} + +TEST(DateTime, ParseTimeInvalid2) +{ + // Various unsupported cases. In all cases, we have produce an empty date time + std::string const badStrings[] = { + "", // empty + ".Z", // too short + ".Zx", // no trailing Z + "3.14Z" // not a valid date + "a971-01-01T00:00:00Z", // any non digits or valid separators + "1a71-01-01T00:00:00Z", + "19a1-01-01T00:00:00Z", + "197a-01-01T00:00:00Z", + "1971a01-01T00:00:00Z", + "1971-a1-01T00:00:00Z", + "1971-0a-01T00:00:00Z", + "1971-01a01T00:00:00Z", + "1971-01-a1T00:00:00Z", + "1971-01-0aT00:00:00Z", + // "1971-01-01a00:00:00Z", parsed as complete date + "1971-01-01Ta0:00:00Z", + "1971-01-01T0a:00:00Z", + "1971-01-01T00a00:00Z", + "1971-01-01T00:a0:00Z", + "1971-01-01T00:0a:00Z", + "1971-01-01T00:00a00Z", + "1971-01-01T00:00:a0Z", + "1971-01-01T00:00:0aZ", + // "1971-01-01T00:00:00a", accepted as per invalid_no_trailing_timezone above + "1", // truncation + "19", + "197", + "1970", + "1970-", + "1970-0", + "1970-01", + "1970-01-", + "1970-01-0", + // "1970-01-01", complete date + "1970-01-01T", + "1970-01-01T0", + "1970-01-01T00", + "1970-01-01T00:", + "1970-01-01T00:0", + "1970-01-01T00:00", + "1970-01-01T00:00:", + "1970-01-01T00:00:0", + // "1970-01-01T00:00:00", // accepted as invalid timezone above + "1899-01-01T00:00:00Z", // year too small + "1971-00-01T00:00:00Z", // month too small + "1971-20-01T00:00:00Z", // month too big + "1971-13-01T00:00:00Z", + "1971-01-00T00:00:00Z", // day too small + "1971-01-32T00:00:00Z", // day too big + "1971-02-30T00:00:00Z", // day too big for feb + "1971-02-30T00:00:00Z", // day too big for feb (non-leap year) + "1971-03-32T00:00:00Z", // other months + "1971-04-31T00:00:00Z", + "1971-05-32T00:00:00Z", + "1971-06-31T00:00:00Z", + "1971-07-32T00:00:00Z", + "1971-08-32T00:00:00Z", + "1971-09-31T00:00:00Z", + "1971-10-32T00:00:00Z", + "1971-11-31T00:00:00Z", + "1971-12-32T00:00:00Z", + "1971-01-01T70:00:00Z", // hour too big + "1971-01-01T24:00:00Z", + "1971-01-01T00:60:00Z", // minute too big + "1971-01-01T00:00:70Z", // second too big + "1971-01-01T00:00:61Z", + "1899-01-01T00:00:00Z", // underflow + "1900-01-01T00:00:00+00:01", // time zone underflow + // "1970-01-01T00:00:00.Z", // accepted as invalid timezone above + "1970-01-01T00:00:00+24:00", // bad tzoffsets + "1970-01-01T00:00:00-30:00", + "1970-01-01T00:00:00+21:60", + "1970-01-01T00:00:00-24:00", + "1970-01-01T00:00:00-21:60", + "1971-01-00", // zero month day + }; + + for (auto const& str : badStrings) + { + auto const dt = DateTime::FromString(str, DateTime::DateFormat::Iso8601); + EXPECT_EQ(dt.ToInterval(), 0); + } +}