From 56475b003aad535ec0ac9c77d801f1821e13d1f9 Mon Sep 17 00:00:00 2001 From: Anton Kolesnyk <41349689+antkmsft@users.noreply.github.com> Date: Wed, 11 Nov 2020 17:48:00 -0800 Subject: [PATCH] DateTime: API review feedback (#938) --- .../azure-core/inc/azure/core/datetime.hpp | 276 ++++++++++-------- sdk/core/azure-core/src/datetime.cpp | 198 +++++++++---- sdk/core/azure-core/test/ut/datetime.cpp | 170 +++++++---- .../azure-core/test/ut/simplified_header.cpp | 2 +- 4 files changed, 401 insertions(+), 245 deletions(-) diff --git a/sdk/core/azure-core/inc/azure/core/datetime.hpp b/sdk/core/azure-core/inc/azure/core/datetime.hpp index 55a925e0b..52b35e208 100644 --- a/sdk/core/azure-core/inc/azure/core/datetime.hpp +++ b/sdk/core/azure-core/inc/azure/core/datetime.hpp @@ -8,7 +8,7 @@ #pragma once -#include +#include #include namespace Azure { namespace Core { @@ -16,29 +16,26 @@ 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 format applied to the fraction part from any @DateFormat - * + * @brief Units of measurement the difference between instances of @DateTime. + */ + // 1 == 100 ns (1 / 10,000,000 of a second, 7 fractional digits). + typedef std::chrono::duration> Duration; + + /** + * @brief Defines the format applied to the fraction part of any @DateTime. */ enum class TimeFractionFormat { - /// Decimals are not included when there are no decimals in the source Datetime and any zeros - /// from the right are also removed. + /// Include only meaningful fractional time digits, up to and excluding trailing zeroes. DropTrailingZeros, - /// Decimals are included for any Datetime. + /// Include all the fractional time digits up to maximum precision, even if the entire value + /// is zero. AllDigits, - /// Decimals are removed for any Datetime. + /// Drop all the fractional time digits. Truncate }; @@ -50,48 +47,46 @@ namespace Azure { namespace Core { /// RFC 1123. Rfc1123, - /// ISO 8601. - Iso8601, + /// RFC 3339. + Rfc3339, }; /** * @brief Get the current UTC time. */ - static DateTime UtcNow(); - - /// An invalid UTC timestamp value. - static constexpr IntervalType UtcTimestampInvalid = static_cast(-1); + static DateTime Now(); /** - * @brief Get seconds since Unix/POSIX time epoch at `01-01-1970 00:00:00`. - * If time is before epoch, @UtcTimestampInvalid is returned. + * @brief Construct an instance of @DateTime. + * + * @param year Year. + * @param month Month. + * @param day Day. + * @param hour Hour. + * @param minute Minute. + * @param second Seconds. + * + * @throw std::invalid_argument If any parameter is invalid. */ - 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) {} + explicit DateTime( + int16_t year, + int8_t month = 1, + int8_t day = 1, + int8_t hour = 0, + int8_t minute = 0, + int8_t second = 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. + * @param dateTime A string with the date and time. + * @param format A format to which \p dateTime string adheres to. * - * @return @DateTime that was constructed from the \p timeString; Uninitialized - * (!@IsInitialized()) @DateTime if parsing \p timeString was not successful. + * @return @DateTime that was constructed from the \p dateTime string. * - * @throw DateTimeException If \p format is not recognized. + * @throw std::invalid_argument If \p format is not recognized, or if parsing error. */ - static DateTime FromString( - std::string const& timeString, - DateFormat format = DateFormat::Rfc1123); + static DateTime Parse(std::string const& dateTime, DateFormat format); private: /** @@ -99,11 +94,11 @@ namespace Azure { namespace Core { * * @param format The representation format to use. * @param fractionFormat The format for the fraction part of the Datetime. Only supported by - * ISO8601 + * RFC 3339. * - * @throw DateTimeException If year exceeds 9999, or if \p format is not recognized. + * @throw std::length_error If year exceeds 9999, or if \p format is not recognized. */ - std::string ToString(DateFormat format, TimeFractionFormat fractionFormat) const; + std::string GetString(DateFormat format, TimeFractionFormat fractionFormat) const; public: /** @@ -111,109 +106,134 @@ namespace Azure { namespace Core { * * @param format The representation format to use. * - * @throw DateTimeException If year exceeds 9999, or if \p format is not recognized. + * @throw std::length_error If year exceeds 9999, or if \p format is not recognized. */ - std::string ToString(DateFormat format = DateFormat::Rfc1123) const + std::string GetString(DateFormat format) const { - return ToString(format, TimeFractionFormat::DropTrailingZeros); + return GetString(format, TimeFractionFormat::DropTrailingZeros); }; /** - * @brief Get a string representation of the @DateTime formated with ISO8601. + * @brief Get a string representation of the @DateTime formatted with RFC 3339. * - * @param fractionFormat The format that is applied to the fraction part from the ISO8601 date. + * @param fractionFormat The format that is applied to the fraction part from the RFC 3339 date. * - * @throw DateTimeException If year exceeds 9999, or if \p fractionFormat is not recognized. + * @throw std::length_error If year exceeds 9999. + * @throw std::invalid_argument If \p format is not recognized. */ - std::string ToIso8601String(TimeFractionFormat fractionFormat) const + std::string GetRfc3339String(TimeFractionFormat fractionFormat) const { - return ToString(DateFormat::Iso8601, fractionFormat); + return GetString(DateFormat::Rfc3339, fractionFormat); }; - /// 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) + /** + * @brief Add \p duration to this @DateTime. + * @param duration @Duration to add. + * @return Reference to this @DateTime. + */ + DateTime& operator+=(Duration const& duration) { - return milliseconds * TicksPerMillisecond; + m_since1601 += duration; + return *this; } - /// Create an interval from seconds. - static IntervalType FromSeconds(unsigned int seconds) { return seconds * TicksPerSecond; } + /** + * @brief Subtract \p duration from this @DateTime. + * @param duration @Duration to subtract from this @DateTime. + * @return Reference to this @DateTime. + */ + DateTime& operator-=(Duration const& duration) + { + m_since1601 -= duration; + return *this; + } - /// Create an interval from minutes. - static IntervalType FromMinutes(unsigned int minutes) { return minutes * TicksPerMinute; } + /** + * @brief Subtract @Duration from @DateTime. + * @param duration @Duration to subtract from this @DateTime. + * @return New DateTime representing subtraction result. + */ + DateTime operator-(Duration const& duration) const { return DateTime(m_since1601 - duration); } - /// Create an interval from hours. - static IntervalType FromHours(unsigned int hours) { return hours * TicksPerHour; } + /** + * @brief Add @Duration to @DateTime. + * @param duration @Duration to add to this @DateTime. + * @return New DateTime representing addition result. + */ + DateTime operator+(Duration const& duration) const { return DateTime(m_since1601 + duration); } - /// Create an interval from days. - static IntervalType FromDays(unsigned int days) { return days * TicksPerDay; } + /** + * @brief Get @Duration between two instances of @DateTime. + * @param other @DateTime to subtract from this @DateTime. + * @return @Duration between this @DateTime and the \p other. + */ + Duration operator-(DateTime const& other) const { return m_since1601 - other.m_since1601; } - /// Checks whether this instance of @DateTime is initialized. - bool IsInitialized() const { return m_interval != 0; } + /** + * @brief Compare with \p other @DateTime for equality. + * @param other Other @DateTime to compare with. + * @return `true` if @DateTime instances are equal, `false` otherwise. + */ + constexpr bool operator==(DateTime const& other) const + { + return m_since1601 == other.m_since1601; + } + + /** + * @brief Compare with \p other @DateTime for inequality. + * @param other Other @DateTime to compare with. + * @return `true` if @DateTime instances are not equal, `false` otherwise. + */ + constexpr bool operator!=(const DateTime& other) const { return !(*this == other); } + + /** + * @brief Check if \p other @DateTime precedes this @DateTime chronologically. + * @param other @DateTime to compare with. + * @return `true` if the \p other @DateTime precedes this, `false` otherwise. + */ + constexpr bool operator>(const DateTime& other) const { return !(*this <= other); } + + /** + * @brief Check if \p other @DateTime is chronologically after this @DateTime. + * @param other @DateTime to compare with. + * @return `true` if the \p other @DateTime is chonologically after this @DateTime, `false` + * otherwise. + */ + constexpr bool operator<(const DateTime& other) const + { + return this->m_since1601 < other.m_since1601; + } + + /** + * @brief Check if \p other @DateTime precedes this @DateTime chronologically, or is equal to + * it. + * @param other @DateTime to compare with. + * @return `true` if the \p other @DateTime precedes or is equal to this @DateTime, `false` + * otherwise. + */ + constexpr bool operator>=(const DateTime& other) const { return !(*this < other); } + + /** + * @brief Check if \p other @DateTime is chronologically after or equal to this @DateTime. + * @param other @DateTime to compare with. + * @return `true` if the \p other @DateTime is chonologically after or equal to this @DateTime, + * `false` otherwise. + */ + constexpr bool operator<=(const DateTime& other) const + { + return (*this == other) || (*this < other); + } + + /** + * @brief Get this @DateTime representation as a @Duration from the start of the + * implementation-defined epoch. + * @return @Duration since the start of the implementation-defined epoch. + */ + constexpr explicit operator Duration() const { return m_since1601; } 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) {} + explicit DateTime(Duration const& since1601) : m_since1601(since1601) {} + Duration m_since1601; }; }} // namespace Azure::Core diff --git a/sdk/core/azure-core/src/datetime.cpp b/sdk/core/azure-core/src/datetime.cpp index 6100eda70..9b3afdb71 100644 --- a/sdk/core/azure-core/src/datetime.cpp +++ b/sdk/core/azure-core/src/datetime.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -17,7 +18,7 @@ using namespace Azure::Core; -DateTime DateTime::UtcNow() +DateTime DateTime::Now() { #ifdef _WIN32 FILETIME fileTime = {}; @@ -27,19 +28,21 @@ DateTime DateTime::UtcNow() largeInt.LowPart = fileTime.dwLowDateTime; largeInt.HighPart = fileTime.dwHighDateTime; - return DateTime(largeInt.QuadPart); + return DateTime(Duration(largeInt.QuadPart)); #else + static constexpr int64_t WindowsToPosixOffsetSeconds = 11644473600LL; + struct timeval time = {}; if (gettimeofday(&time, nullptr) != 0) { - return DateTime(); + return DateTime(Duration()); } - IntervalType result = WindowsToPosixOffsetSeconds + time.tv_sec; - result *= TicksPerSecond; // convert to 10e-7 + int64_t result = WindowsToPosixOffsetSeconds + time.tv_sec; + result *= DateTime::Duration::period::den; // convert to 10e-7 result += time.tv_usec * 10; // convert and add microseconds, 10e-6 to 10e-7 - return DateTime(static_cast(result)); + return DateTime(Duration(result)); #endif } @@ -50,6 +53,9 @@ struct ComputeYearResult int SecondsLeftThisYear; }; +constexpr auto MinYear = 1601; +constexpr auto MaxYear = 9999; + constexpr int SecondsInMinute = 60; constexpr int SecondsInHour = SecondsInMinute * 60; constexpr int SecondsInDay = 24 * SecondsInHour; @@ -127,19 +133,20 @@ constexpr char const monthNames[] = "Jan\0Feb\0Mar\0Apr\0May\0Jun\0Jul\0Aug\0Sep } // namespace -std::string DateTime::ToString(DateFormat format, TimeFractionFormat fractionFormat) const +std::string DateTime::GetString(DateFormat format, TimeFractionFormat fractionFormat) const { - if (m_interval > 2650467743999999999LL) + static constexpr auto const EndOfYear9999 = 2650467743999999999LL; + if (m_since1601.count() > EndOfYear9999) { - throw DateTimeException("The requested year exceeds the year 9999."); + throw std::invalid_argument("The requested year exceeds the year 9999."); } - int64_t const epochAdjusted = static_cast(m_interval); - int64_t const secondsSince1601 = epochAdjusted / TicksPerSecond; // convert to seconds - int const fracSec = static_cast(epochAdjusted % TicksPerSecond); + int64_t const epochAdjusted = m_since1601.count(); + int64_t const secondsSince1601 = epochAdjusted / Duration::period::den; // convert to seconds + int const fracSec = static_cast(epochAdjusted % Duration::period::den); auto const yearData = ComputeYear(secondsSince1601); - int const year = yearData.Year + 1601; + int const year = yearData.Year + MinYear; int const yearDay = yearData.SecondsLeftThisYear / SecondsInDay; int leftover = yearData.SecondsLeftThisYear % SecondsInDay; @@ -190,7 +197,7 @@ std::string DateTime::ToString(DateFormat format, TimeFractionFormat fractionFor outCursor += 4; return std::string(outBuffer, outCursor); - case DateFormat::Iso8601: + case DateFormat::Rfc3339: #ifdef _MSC_VER sprintf_s( #else @@ -242,7 +249,7 @@ std::string DateTime::ToString(DateFormat format, TimeFractionFormat fractionFor return std::string(outBuffer, outCursor); default: - throw DateTimeException("Unrecognized date format."); + throw std::invalid_argument("Unrecognized date format."); } } @@ -396,10 +403,8 @@ zone = "UT" / "GMT" ; Universal Time ; hours+min. (HHMM) */ -DateTime DateTime::FromString(std::string const& dateString, DateFormat format) +DateTime DateTime::Parse(std::string const& dateString, DateFormat format) { - DateTime result = {}; - int64_t secondsSince1601 = 0; uint64_t fracSec = 0; @@ -430,12 +435,12 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) } else { - return result; + throw std::invalid_argument("Error parsing DateTime: Day of the month."); } if (monthDay == 0) { - return result; + throw std::invalid_argument("Error parsing DateTime: Invalid month day number (0)."); } int month = 0; @@ -449,13 +454,13 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) ++month; if (month == 12) { - return result; + throw std::invalid_argument("Error parsing DateTime: Month number."); } } if (str[3] != ' ') { - return result; + throw std::invalid_argument("Error parsing DateTime."); } str += 4; // parsed month @@ -463,19 +468,19 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) if (!IsDigit(str[0]) || !IsDigit(str[1]) || !IsDigit(str[2]) || !IsDigit(str[3]) || str[4] != ' ') { - return result; + throw std::invalid_argument("Error parsing DateTime: Year."); } int year = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0'); - if (year < 1601) + if (year < MinYear) { - return result; + throw std::invalid_argument("Error parsing DateTime: Year is less than 1601."); } // days in month validity check if (!ValidateDay(monthDay, month, year)) { - return result; + throw std::invalid_argument("Error parsing DateTime: Invalid day in month."); } str += 5; // parsed year @@ -484,13 +489,13 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) if (!IsDigit<2>(str[0]) || !IsDigit(str[1]) || str[2] != ':' || !IsDigit<5>(str[3]) || !IsDigit(str[4])) { - return result; + throw std::invalid_argument("Error parsing DateTime: Hour and minutes."); } int const hour = StringToDoubleDigitInt(str); if (hour > 23) { - return result; + throw std::invalid_argument("Error parsing DateTime: hour > 23."); } str += 3; // parsed hour @@ -502,7 +507,7 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) { if (!IsDigit<6>(str[1]) || !IsDigit(str[2]) || str[3] != ' ') { - return result; + throw std::invalid_argument("Error parsing DateTime."); } sec = StringToDoubleDigitInt(str + 1); @@ -514,15 +519,15 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) } else { - return result; + throw std::invalid_argument("Error parsing DateTime."); } if (sec > 60) { // 60 to allow leap seconds - return result; + throw std::invalid_argument("Error parsing DateTime: Seconds > 60."); } - year -= 1601; + year -= MinYear; int const daysSince1601 = year * DaysInYear + CountLeapYears(year) + yearDay; if (parsedWeekday != 7) @@ -530,13 +535,13 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) int const actualWeekday = (daysSince1601 + 1) % 7; if (parsedWeekday != actualWeekday) { - return result; + throw std::invalid_argument("Error parsing DateTime: Weekday."); } } - secondsSince1601 = static_cast(daysSince1601) * SecondsInDay - + static_cast(hour) * SecondsInHour - + static_cast(minute) * SecondsInMinute + sec; + secondsSince1601 = static_cast(daysSince1601) * SecondsInDay + + static_cast(hour) * SecondsInHour + + static_cast(minute) * SecondsInMinute + sec; fracSec = 0; if (!StringStartsWith(str, "GMT") && !StringStartsWith(str, "UT")) @@ -575,7 +580,7 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) } else { - return result; + throw std::invalid_argument("Error parsing DateTime: Time zone."); } secondsSince1601 @@ -583,22 +588,23 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) if (secondsSince1601 < 0) { - return result; + throw std::invalid_argument( + "Error parsing DateTime: year is < 1601 after time zone adjustments."); } } } - else if (format == DateFormat::Iso8601) + else if (format == DateFormat::Rfc3339) { // parse year if (!IsDigit(str[0]) || !IsDigit(str[1]) || !IsDigit(str[2]) || !IsDigit(str[3])) { - return result; + throw std::invalid_argument("Error parsing DateTime: Year."); } int year = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0'); - if (year < 1601) + if (year < MinYear) { - return result; + throw std::invalid_argument("Error parsing DateTime: Year < 1601."); } str += 4; @@ -610,13 +616,13 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) // parse month if (!IsDigit<1>(str[0]) || !IsDigit(str[1])) { - return result; + throw std::invalid_argument("Error parsing DateTime: Month number."); } int month = StringToDoubleDigitInt(str); if (month < 1 || month > 12) { - return result; + throw std::invalid_argument("Error parsing DateTime: Invalid month number."); } month -= 1; @@ -630,27 +636,26 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) // parse day if (!IsDigit<3>(str[0]) || !IsDigit(str[1])) { - return result; + throw std::invalid_argument("Error parsing DateTime: Day."); } int monthDay = StringToDoubleDigitInt(str); if (!ValidateDay(monthDay, month, year)) { - return result; + throw std::invalid_argument("Error parsing DateTime: Day of the month."); } int const yearDay = GetYearDay(month, monthDay, year); str += 2; - year -= 1601; + year -= MinYear; int daysSince1601 = year * DaysInYear + CountLeapYears(year) + yearDay; if (str[0] != 'T' && str[0] != 't') { // No time secondsSince1601 = static_cast(daysSince1601) * SecondsInDay; - result.m_interval = static_cast(secondsSince1601 * TicksPerSecond + fracSec); - return result; + return DateTime(std::chrono::seconds(secondsSince1601) + DateTime::Duration(fracSec)); } ++str; // skip 'T' @@ -658,14 +663,14 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) // parse hour if (!IsDigit<2>(str[0]) || !IsDigit(str[1])) { - return result; + throw std::invalid_argument("Error parsing DateTime: Hour."); } int const hour = StringToDoubleDigitInt(str); str += 2; if (hour > 23) { - return result; + throw std::invalid_argument("Error parsing DateTime: Invalid hour number."); } if (*str == ':') @@ -676,7 +681,7 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) // parse minute if (!IsDigit<5>(str[0]) || !IsDigit(str[1])) { - return result; + throw std::invalid_argument("Error parsing DateTime: Minute."); } int const minute = StringToDoubleDigitInt(str); @@ -693,14 +698,14 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) // parse seconds if (!IsDigit<6>(str[0]) || !IsDigit(str[1])) { - return result; + throw std::invalid_argument("Error parsing DateTime: Seconds."); } int const sec = StringToDoubleDigitInt(str); // We allow 60 to account for leap seconds if (sec > 60) { - return result; + throw std::invalid_argument("Error parsing DateTime: Invalid seconds number."); } str += 2; @@ -754,7 +759,7 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) if (!IsDigit<2>(str[1]) || !IsDigit(str[2]) || str[3] != ':' || !IsDigit<5>(str[4]) || !IsDigit(str[5])) { - return result; + throw std::invalid_argument("Error parsing DateTime."); } secondsSince1601 = AdjustTimezone( @@ -764,7 +769,7 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) StringToDoubleDigitInt(str + 4)); if (secondsSince1601 < 0) { - return result; + throw std::invalid_argument("Error parsing DateTime."); } } else @@ -774,9 +779,84 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format) } else { - throw DateTimeException("Unrecognized date format."); + throw std::invalid_argument("Unrecognized date format."); } - result.m_interval = static_cast(secondsSince1601 * TicksPerSecond + fracSec); - return result; + return DateTime(std::chrono::seconds(secondsSince1601) + DateTime::Duration(fracSec)); +} + +DateTime::DateTime( + int16_t year, + int8_t month, + int8_t day, + int8_t hour, + int8_t minute, + int8_t second) +{ + // We should combine creation/validation logic, so it is reusable from both parsing functions and + // from this constructor + if (year > MaxYear || year < MinYear) + { + throw std::invalid_argument( + year > MaxYear ? "The requested year exceeds the year 9999." + : "The requested year is less than the year 1601."); + } + + if (month <= 0 || month > 12) + { + throw std::invalid_argument("Invalid month value."); + } + + if (day <= 0 || day > 31) + { + throw std::invalid_argument("Invalid day value."); + } + + if (hour < 0 || hour > 23) + { + throw std::invalid_argument("Invalid hour value."); + } + + if (minute < 0 || minute > 59) + { + throw std::invalid_argument("Invalid minute value."); + } + + if (second < 0 || second > 60) + { + throw std::invalid_argument("Invalid seconds value."); + } + + if (!ValidateDay(day, month - 1, year)) + { + throw std::invalid_argument("Invalid day of the month."); + } + + char outBuffer[38]{}; // Thu, 01 Jan 1970 00:00:00 GMT\0 + // 1970-01-01T00:00:00.1234567Z\0 + + char* outCursor = outBuffer; +#ifdef _MSC_VER + sprintf_s( +#else + std::sprintf( +#endif + outCursor, +#ifdef _MSC_VER + 20, +#endif + "%04d-%02d-%02dT%02d:%02d:%02d", + year, + month, + day, + hour, + minute, + second); + + outCursor += 19; + + *outCursor = 'Z'; + ++outCursor; + auto const dt = DateTime::Parse(std::string(outBuffer, outCursor), DateFormat::Rfc3339); + m_since1601 = dt.m_since1601; } diff --git a/sdk/core/azure-core/test/ut/datetime.cpp b/sdk/core/azure-core/test/ut/datetime.cpp index d4d241664..9970d11d1 100644 --- a/sdk/core/azure-core/test/ut/datetime.cpp +++ b/sdk/core/azure-core/test/ut/datetime.cpp @@ -9,49 +9,51 @@ using namespace Azure::Core; +namespace { +static const auto Year1601 = DateTime(1601); +} // namespace + TEST(DateTime, ParseDateAndTimeBasic) { - auto dt1 = DateTime::FromString("20130517T00:00:00Z", DateTime::DateFormat::Iso8601); - EXPECT_NE(0u, dt1.ToInterval()); + auto dt1 = DateTime::Parse("20130517T00:00:00Z", DateTime::DateFormat::Rfc3339); + auto dt2 = DateTime::Parse("Fri, 17 May 2013 00:00:00 GMT", DateTime::DateFormat::Rfc1123); + EXPECT_NE(0, static_cast(dt2).count()); - 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()); + EXPECT_EQ(dt1, dt2); } TEST(DateTime, ParseDateAndTimeExtended) { - auto dt1 = DateTime::FromString("2013-05-17T00:00:00Z", DateTime::DateFormat::Iso8601); - EXPECT_NE(0u, dt1.ToInterval()); + auto dt1 = DateTime::Parse("2013-05-17T00:00:00Z", DateTime::DateFormat::Rfc3339); + EXPECT_NE(0, static_cast(dt1).count()); - auto dt2 = DateTime::FromString("Fri, 17 May 2013 00:00:00 GMT", DateTime::DateFormat::Rfc1123); - EXPECT_NE(0u, dt2.ToInterval()); + auto dt2 = DateTime::Parse("Fri, 17 May 2013 00:00:00 GMT", DateTime::DateFormat::Rfc1123); + EXPECT_NE(0, static_cast(dt2).count()); - EXPECT_EQ(dt1.ToInterval(), dt2.ToInterval()); + EXPECT_EQ(dt1, dt2); } TEST(DateTime, ParseDateBasic) { { - auto dt = DateTime::FromString("20130517", DateTime::DateFormat::Iso8601); - EXPECT_NE(0u, dt.ToInterval()); + auto dt = DateTime::Parse("20130517", DateTime::DateFormat::Rfc3339); + EXPECT_NE(0, static_cast(dt).count()); } } TEST(DateTime, ParseDateExtended) { { - auto dt = DateTime::FromString("2013-05-17", DateTime::DateFormat::Iso8601); - EXPECT_NE(0u, dt.ToInterval()); + auto dt = DateTime::Parse("2013-05-17", DateTime::DateFormat::Rfc3339); + EXPECT_NE(0, static_cast(dt).count()); } } 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); + auto dt = DateTime::Parse(str, DateTime::DateFormat::Rfc3339); + auto const str2 = dt.GetString(DateTime::DateFormat::Rfc3339); EXPECT_EQ(str2, strExpected); } @@ -77,22 +79,22 @@ TEST(DateTime, decimals) { { std::string strExpected("2020-10-13T21:06:15.3300000Z"); - auto dt = DateTime::FromString("2020-10-13T21:06:15.33Z", DateTime::DateFormat::Iso8601); - auto const str2 = dt.ToIso8601String(DateTime::TimeFractionFormat::AllDigits); + auto dt = DateTime::Parse("2020-10-13T21:06:15.33Z", DateTime::DateFormat::Rfc3339); + auto const str2 = dt.GetRfc3339String(DateTime::TimeFractionFormat::AllDigits); EXPECT_EQ(str2, strExpected); } { std::string strExpected("2020-10-13T21:06:15.0000000Z"); - auto dt = DateTime::FromString("2020-10-13T21:06:15Z", DateTime::DateFormat::Iso8601); - auto const str2 = dt.ToIso8601String(DateTime::TimeFractionFormat::AllDigits); + auto dt = DateTime::Parse("2020-10-13T21:06:15Z", DateTime::DateFormat::Rfc3339); + auto const str2 = dt.GetRfc3339String(DateTime::TimeFractionFormat::AllDigits); EXPECT_EQ(str2, strExpected); } { std::string strExpected("2020-10-13T21:06:15.1234500Z"); - auto dt = DateTime::FromString("2020-10-13T21:06:15.12345Z", DateTime::DateFormat::Iso8601); - auto const str2 = dt.ToIso8601String(DateTime::TimeFractionFormat::AllDigits); + auto dt = DateTime::Parse("2020-10-13T21:06:15.12345Z", DateTime::DateFormat::Rfc3339); + auto const str2 = dt.GetRfc3339String(DateTime::TimeFractionFormat::AllDigits); EXPECT_EQ(str2, strExpected); } } @@ -101,27 +103,26 @@ TEST(DateTime, noDecimals) { { std::string strExpected("2020-10-13T21:06:15Z"); - auto dt = DateTime::FromString("2020-10-13T21:06:15Z", DateTime::DateFormat::Iso8601); - auto const str2 = dt.ToIso8601String(DateTime::TimeFractionFormat::Truncate); + auto dt = DateTime::Parse("2020-10-13T21:06:15Z", DateTime::DateFormat::Rfc3339); + auto const str2 = dt.GetRfc3339String(DateTime::TimeFractionFormat::Truncate); EXPECT_EQ(str2, strExpected); } { std::string strExpected("2020-10-13T21:06:15Z"); - auto dt = DateTime::FromString("2020-10-13T21:06:15.99999Z", DateTime::DateFormat::Iso8601); - auto const str2 = dt.ToIso8601String(DateTime::TimeFractionFormat::Truncate); + auto dt = DateTime::Parse("2020-10-13T21:06:15.99999Z", DateTime::DateFormat::Rfc3339); + auto const str2 = dt.GetRfc3339String(DateTime::TimeFractionFormat::Truncate); EXPECT_EQ(str2, strExpected); } } -TEST(DateTime, sameResultFromDefaultISO) +TEST(DateTime, sameResultFromDefaultRfc3339) { { - auto dt = DateTime::FromString("2020-10-13T21:06:15.33000000Z", DateTime::DateFormat::Iso8601); - auto dt2 - = DateTime::FromString("2020-10-13T21:06:15.330000000Z", DateTime::DateFormat::Iso8601); - auto const str1 = dt.ToIso8601String(DateTime::TimeFractionFormat::DropTrailingZeros); - auto const str2 = dt2.ToString(DateTime::DateFormat::Iso8601); + auto dt = DateTime::Parse("2020-10-13T21:06:15.33000000Z", DateTime::DateFormat::Rfc3339); + auto dt2 = DateTime::Parse("2020-10-13T21:06:15.330000000Z", DateTime::DateFormat::Rfc3339); + auto const str1 = dt.GetRfc3339String(DateTime::TimeFractionFormat::DropTrailingZeros); + auto const str2 = dt2.GetString(DateTime::DateFormat::Rfc3339); EXPECT_EQ(str1, str2); } } @@ -156,17 +157,18 @@ TEST(DateTime, ParseTimeRoundripYear9999) { TestDateTimeRoundtrip("9999-12-31T23 TEST(DateTime, EmittingTimeCorrectDay) { - auto const test = DateTime() + 132004507640000000LL; // 2019-04-22T23:52:44 is a Monday - auto const actual = test.ToString(DateTime::DateFormat::Rfc1123); + auto const test = Year1601 + std::chrono::seconds(13200450764); // 2019-04-22T23:52:44 is a Monday + auto const actual = test.GetString(DateTime::DateFormat::Rfc1123); std::string const expected("Mon"); EXPECT_EQ(actual.substr(0, 3), expected); } namespace { -void TestRfc1123IsTimeT(char const* str, DateTime::IntervalType t) +void TestRfc1123IsTimeT(char const* str, int64_t t) { - auto const dt = DateTime::FromString(str, DateTime::DateFormat::Rfc1123); - auto interval = dt.ToInterval(); + auto const dt = DateTime::Parse(str, DateTime::DateFormat::Rfc1123); + int64_t interval = static_cast(dt).count(); + EXPECT_EQ(0, interval % 10000000); interval /= 10000000; interval -= 11644473600; // NT epoch adjustment @@ -188,7 +190,8 @@ TEST(DateTime, ParseTimeRfc1123AcceptsEachDay) 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:14:06 GMT", static_cast(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); @@ -338,12 +341,11 @@ TEST(DateTime, ParseTimeRfc1123InvalidCases) for (auto const& str : badStrings) { - auto const dt = DateTime::FromString(str, DateTime::DateFormat::Rfc1123); - EXPECT_EQ(0, dt.ToInterval()); + EXPECT_THROW(DateTime::Parse(str, DateTime::DateFormat::Rfc1123), std::invalid_argument); } } -TEST(DateTime, ParseTimeIso8601BoundaryCases) +TEST(DateTime, ParseTimeRfc3339BoundaryCases) { // boundary cases: TestDateTimeRoundtrip("1970-01-01T00:00:00Z"); // epoch @@ -354,7 +356,7 @@ TEST(DateTime, ParseTimeIso8601BoundaryCases) TestDateTimeRoundtrip("2038-01-19T03:14:07-00:00", "2038-01-19T03:14:07Z"); } -TEST(DateTime, ParseTimeIso8601UsesEachTimezoneDigit) +TEST(DateTime, ParseTimeRfc3339UsesEachTimezoneDigit) { 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"); @@ -363,7 +365,7 @@ TEST(DateTime, ParseTimeIso8601UsesEachTimezoneDigit) TestDateTimeRoundtrip("2019-01-14T23:16:21+01:00", "2019-01-14T22:16:21Z"); } -TEST(DateTime, ParseTimeIso8601UsesEachDigit) +TEST(DateTime, ParseTimeRfc3339UsesEachDigit) { TestDateTimeRoundtrip("1970-01-01T00:00:01Z"); TestDateTimeRoundtrip("1970-01-01T00:01:00Z"); @@ -384,7 +386,7 @@ TEST(DateTime, ParseTimeIso8601UsesEachDigit) TestDateTimeRoundtrip("1970-01-01T00:00:60Z", "1970-01-01T00:01:00Z"); // leap seconds } -TEST(DateTime, ParseTimeIso8601AcceptsMonthMaxDays) +TEST(DateTime, ParseTimeRfc3339AcceptsMonthMaxDays) { TestDateTimeRoundtrip("1970-01-31T00:00:00Z"); // jan TestDateTimeRoundtrip("2019-02-28T00:00:00Z"); // non leap year allows feb 28 @@ -401,7 +403,7 @@ TEST(DateTime, ParseTimeIso8601AcceptsMonthMaxDays) TestDateTimeRoundtrip("1970-12-31T00:00:00Z"); // dec } -TEST(DateTime, ParseTimeIso8601AcceptsLowercaseTZ) +TEST(DateTime, ParseTimeRfc3339AcceptsLowercaseTZ) { TestDateTimeRoundtrip("1970-01-01t00:00:00Z", "1970-01-01T00:00:00Z"); TestDateTimeRoundtrip("1970-01-01T00:00:00z", "1970-01-01T00:00:00Z"); @@ -416,8 +418,8 @@ TEST(DateTime, ParseTimeRoundtripAcceptsInvalidNoTrailingTimezone) for (auto const& str : badStrings) { - auto const dt = DateTime::FromString(str, DateTime::DateFormat::Iso8601); - auto const str2 = dt.ToString(DateTime::DateFormat::Iso8601); + auto const dt = DateTime::Parse(str, DateTime::DateFormat::Rfc3339); + auto const str2 = dt.GetString(DateTime::DateFormat::Rfc3339); EXPECT_EQ(str2, strCorrected); } } @@ -504,21 +506,75 @@ TEST(DateTime, ParseTimeInvalid2) for (auto const& str : badStrings) { - auto const dt = DateTime::FromString(str, DateTime::DateFormat::Iso8601); - EXPECT_EQ(dt.ToInterval(), 0); + EXPECT_THROW(DateTime::Parse(str, DateTime::DateFormat::Rfc3339), std::invalid_argument); } } TEST(DateTime, ParseDatesBefore1900) { TestDateTimeRoundtrip("1899-01-01T00:00:00Z"); - auto dt1 = DateTime::FromString("1899-01-01T00:00:00Z", DateTime::DateFormat::Iso8601); - auto dt2 = DateTime::FromString("Sun, 1 Jan 1899 00:00:00 GMT", DateTime::DateFormat::Rfc1123); - EXPECT_EQ(dt1.ToInterval(), dt2.ToInterval()); + auto dt1 = DateTime::Parse("1899-01-01T00:00:00Z", DateTime::DateFormat::Rfc3339); + auto dt2 = DateTime::Parse("Sun, 1 Jan 1899 00:00:00 GMT", DateTime::DateFormat::Rfc1123); + EXPECT_EQ(dt1, dt2); TestDateTimeRoundtrip("1601-01-01T00:00:00Z"); - auto dt3 = DateTime::FromString("1601-01-01T00:00:00Z", DateTime::DateFormat::Iso8601); - auto dt4 = DateTime::FromString("Mon, 1 Jan 1601 00:00:00 GMT", DateTime::DateFormat::Rfc1123); - EXPECT_EQ(dt3.ToInterval(), dt4.ToInterval()); - EXPECT_EQ(0u, dt3.ToInterval()); + auto dt3 = DateTime::Parse("1601-01-01T00:00:00Z", DateTime::DateFormat::Rfc3339); + auto dt4 = DateTime::Parse("Mon, 1 Jan 1601 00:00:00 GMT", DateTime::DateFormat::Rfc1123); + EXPECT_EQ(dt3, dt4); + EXPECT_EQ(0, static_cast(dt3).count()); +} + +TEST(DateTime, ConstructorAndDuration) +{ + auto dt1 = DateTime::Parse("2020-11-03T15:30:45.1234567Z", DateTime::DateFormat::Rfc3339); + auto dt2 = DateTime(2020, 11, 03, 15, 30, 45); + dt2 += std::chrono::duration_cast(std::chrono::nanoseconds(123456700)); + EXPECT_EQ(dt1, dt2); + + using namespace std::chrono_literals; + auto duration = 8h + 29min + 14s + 876543300ns; + + auto dt3 = dt1 + std::chrono::duration_cast(duration); + + auto dt4 = DateTime::Parse("2020-11-04T00:00:00Z", DateTime::DateFormat::Rfc3339); + EXPECT_EQ(dt3, dt4); +} + +TEST(DateTime, ArithmeticOperators) +{ + auto const dt1 = DateTime(2020, 11, 03, 15, 30, 45); + auto const dt2 = DateTime(2020, 11, 04, 15, 30, 45); + auto dt3 = dt1; + EXPECT_EQ(dt3, dt1); + EXPECT_EQ(dt1, dt3); + EXPECT_NE(dt3, dt2); + EXPECT_NE(dt2, dt3); + EXPECT_LT(dt1, dt2); + EXPECT_LE(dt1, dt2); + EXPECT_LE(dt1, dt3); + EXPECT_LE(dt3, dt1); + EXPECT_LE(dt3, dt2); + EXPECT_GT(dt2, dt1); + EXPECT_GE(dt2, dt1); + + using namespace std::chrono_literals; + auto const diff = dt2 - dt1; + EXPECT_EQ(24h, diff); + EXPECT_LE(24h, diff); + EXPECT_GE(24h, diff); + + dt3 += 24h; + EXPECT_EQ(dt3, dt2); + EXPECT_NE(dt3, dt1); + + dt3 -= 24h; + EXPECT_EQ(dt3, dt1); + EXPECT_NE(dt3, dt2); + + dt3 = dt1 + 12h; + EXPECT_GT(dt3, dt1); + EXPECT_LT(dt3, dt2); + + dt3 = dt2 - 24h; + EXPECT_EQ(dt3, dt1); } diff --git a/sdk/core/azure-core/test/ut/simplified_header.cpp b/sdk/core/azure-core/test/ut/simplified_header.cpp index 01c0c4533..9faf21a73 100644 --- a/sdk/core/azure-core/test/ut/simplified_header.cpp +++ b/sdk/core/azure-core/test/ut/simplified_header.cpp @@ -16,7 +16,7 @@ TEST(Logging, simplifiedHeader) { EXPECT_NO_THROW(Azure::Core::Context c); - EXPECT_NO_THROW(Azure::Core::DateTime::DateTime::FromSeconds(10)); + EXPECT_NO_THROW(Azure::Core::DateTime(2020, 11, 03, 15, 30, 44)); EXPECT_NO_THROW(Azure::Core::Nullable n); EXPECT_NO_THROW(Azure::Core::Http::RawResponse r( 1, 1, Azure::Core::Http::HttpStatusCode::Accepted, "phrase"));