From dd3c79c9b4f5ea1fa83b6fce635d3358f12c477e Mon Sep 17 00:00:00 2001 From: Ahson Khan Date: Tue, 9 Mar 2021 13:29:20 -0800 Subject: [PATCH] Make sure DateTime.ToString() works without requiring additional parameters by choosing RFC 3339 as the default. (#1812) * Make sure DateTime.ToString() works without requiring additional parameters by choosing RFC 3339 as the default. * Address naming feedback and add some tests. * Fix test warning by using static cast. --- .../azure-core/inc/azure/core/datetime.hpp | 27 ++- sdk/core/azure-core/src/datetime.cpp | 205 +++++++++++------- sdk/core/azure-core/test/ut/datetime.cpp | 35 +++ 3 files changed, 185 insertions(+), 82 deletions(-) diff --git a/sdk/core/azure-core/inc/azure/core/datetime.hpp b/sdk/core/azure-core/inc/azure/core/datetime.hpp index 5498bb5f2..0a9e92f26 100644 --- a/sdk/core/azure-core/inc/azure/core/datetime.hpp +++ b/sdk/core/azure-core/inc/azure/core/datetime.hpp @@ -74,6 +74,20 @@ namespace Azure { namespace Core { int8_t localDiffMinutes, bool roundFracSecUp = false); + void ThrowIfUnsupportedYear() const; + + void GetDateTimeParts( + int16_t* year, + int8_t* month, + int8_t* day, + int8_t* hour, + int8_t* minute, + int8_t* second, + int32_t* fracSec, + int8_t* dayOfWeek) const; + + std::string ToStringRfc1123() const; + public: /** * @brief Construct an instance of #Azure::Core::DateTime. @@ -163,6 +177,15 @@ namespace Azure { namespace Core { */ static DateTime Parse(std::string const& dateTime, DateFormat format); + /** + * @brief Get a string representation of the #Azure::Core::DateTime. + * + * @param format The representation format to use, defaulted to use RFC 3339. + * + * @throw std::invalid_argument If year exceeds 9999, or if \p format is not recognized. + */ + std::string ToString(DateFormat format = DateFormat::Rfc3339) const; + /** * @brief Get a string representation of the #Azure::Core::DateTime. * @@ -172,9 +195,7 @@ namespace Azure { namespace Core { * * @throw std::invalid_argument If year exceeds 9999, or if \p format is not recognized. */ - std::string ToString( - DateFormat format, - TimeFractionFormat fractionFormat = TimeFractionFormat::DropTrailingZeros) const; + std::string ToString(DateFormat format, TimeFractionFormat fractionFormat) const; }; inline Details::Clock::time_point Details::Clock::now() noexcept diff --git a/sdk/core/azure-core/src/datetime.cpp b/sdk/core/azure-core/src/datetime.cpp index 468a252df..2e81eabc4 100644 --- a/sdk/core/azure-core/src/datetime.cpp +++ b/sdk/core/azure-core/src/datetime.cpp @@ -729,42 +729,39 @@ DateTime DateTime::Parse(std::string const& dateTime, DateFormat format) } } -std::string DateTime::ToString(DateFormat format, TimeFractionFormat fractionFormat) const +void DateTime::ThrowIfUnsupportedYear() const { + static DateTime const Year0001 = DateTime(); + static DateTime const Eoy9999 = DateTime(9999, 12, 31, 23, 59, 59, 9999999, -1, 0, 0); + + auto outOfRange = 0; + if (*this < Year0001) { - static DateTime const Year0001 = DateTime(); - static DateTime const Eoy9999 = DateTime(9999, 12, 31, 23, 59, 59, 9999999, -1, 0, 0); - - auto outOfRange = 0; - if (*this < Year0001) - { - outOfRange = -1; - } - else if (*this > Eoy9999) - { - outOfRange = +1; - } - - if (outOfRange != 0) - { - throw std::invalid_argument( - std::string("Cannot represent Azure::Core::DateTime as std::string: the date is ") - + (outOfRange < 0 ? "before 0001-01-01." : "after 9999-12-31 23:59:59.9999999.")); - } + outOfRange = -1; + } + else if (*this > Eoy9999) + { + outOfRange = +1; } - int16_t year = 1; + if (outOfRange != 0) + { + throw std::invalid_argument( + std::string("Cannot represent Azure::Core::DateTime as std::string: the date is ") + + (outOfRange < 0 ? "before 0001-01-01." : "after 9999-12-31 23:59:59.9999999.")); + } +} - // The values that are not supposed to be read before they are written are set to -123... to avoid - // warnings on some compilers, yet provide a clearly bad value to make it obvious if things don't - // work as expected. - int8_t month = -123; - int8_t day = -123; - int8_t hour = -123; - int8_t minute = -123; - int8_t second = -123; - int32_t fracSec = -1234567890; - int8_t dayOfWeek = -123; +void DateTime::GetDateTimeParts( + int16_t* year, + int8_t* month, + int8_t* day, + int8_t* hour, + int8_t* minute, + int8_t* second, + int32_t* fracSec, + int8_t* dayOfWeek) const +{ { auto remainder = time_since_epoch().count(); @@ -789,66 +786,116 @@ std::string DateTime::ToString(DateFormat format, TimeFractionFormat fractionFor years1 = DivideAndUpdateRemainder(&remainder, OneYearIn100ns); } - year += static_cast((years400 * 400) + (years100 * 100) + (years4 * 4) + years1); + *year += static_cast((years400 * 400) + (years100 * 100) + (years4 * 4) + years1); - dayOfWeek = WeekDayAndMonthDayOfYear( - &month, - &day, - year, + *dayOfWeek = WeekDayAndMonthDayOfYear( + month, + day, + *year, static_cast(DivideAndUpdateRemainder(&remainder, OneDayIn100ns) + 1)); - hour = static_cast(DivideAndUpdateRemainder(&remainder, OneHourIn100ns)); - minute = static_cast(DivideAndUpdateRemainder(&remainder, OneMinuteIn100ns)); - second = static_cast(DivideAndUpdateRemainder(&remainder, OneSecondIn100ns)); - fracSec = static_cast(remainder); + *hour = static_cast(DivideAndUpdateRemainder(&remainder, OneHourIn100ns)); + *minute = static_cast(DivideAndUpdateRemainder(&remainder, OneMinuteIn100ns)); + *second = static_cast(DivideAndUpdateRemainder(&remainder, OneSecondIn100ns)); + *fracSec = static_cast(remainder); } +} + +std::string DateTime::ToStringRfc1123() const +{ + ThrowIfUnsupportedYear(); + + int16_t year = 1; + + // The values that are not supposed to be read before they are written are set to -123... to + // avoid warnings on some compilers, yet provide a clearly bad value to make it obvious if + // things don't work as expected. + int8_t month = -123; + int8_t day = -123; + int8_t hour = -123; + int8_t minute = -123; + int8_t second = -123; + int32_t fracSec = -1234567890; + int8_t dayOfWeek = -123; + + GetDateTimeParts(&year, &month, &day, &hour, &minute, &second, &fracSec, &dayOfWeek); std::ostringstream dateString; - if (format == DateFormat::Rfc3339) + + dateString << DayNames[dayOfWeek] << ", " << std::setfill('0') << std::setw(2) + << static_cast(day) << ' ' << MonthNames[month - 1] << ' ' << std::setw(4) + << static_cast(year) << ' ' << std::setw(2) << static_cast(hour) << ':' + << std::setw(2) << static_cast(minute) << ':' << std::setw(2) + << static_cast(second) << " GMT"; + + return dateString.str(); +} + +std::string DateTime::ToString(DateFormat format) const +{ + if (format == DateFormat::Rfc1123) { - dateString << std::setfill('0') << std::setw(4) << static_cast(year) << '-' << std::setw(2) - << static_cast(month) << '-' << std::setw(2) << static_cast(day) << 'T' - << std::setw(2) << static_cast(hour) << ':' << std::setw(2) - << static_cast(minute) << ':' << std::setw(2) << static_cast(second); - - if (fractionFormat == TimeFractionFormat::AllDigits) - { - dateString << '.' << std::setw(7) << static_cast(fracSec); - } - else if (fracSec != 0 && fractionFormat != TimeFractionFormat::Truncate) - { - // Append fractional second, which is a 7-digit value with no trailing zeros - // This way, '0001200' becomes '00012' - auto setw = 1; - auto frac = fracSec; - for (auto div = 1000000; div >= 1; div /= 10) - { - if ((fracSec % div) == 0) - { - frac /= div; - break; - } - ++setw; - } - - dateString << '.' << std::setw(setw) << static_cast(frac); - } - - dateString << 'Z'; + return DateTime::ToStringRfc1123(); } - else if (format == DateFormat::Rfc1123) - { - dateString << DayNames[dayOfWeek] << ", " << std::setfill('0') << std::setw(2) - << static_cast(day) << ' ' << MonthNames[month - 1] << ' ' << std::setw(4) - << static_cast(year) << ' ' << std::setw(2) << static_cast(hour) << ':' - << std::setw(2) << static_cast(minute) << ':' << std::setw(2) - << static_cast(second) << " GMT"; - } - else + return ToString(format, TimeFractionFormat::DropTrailingZeros); +} + +std::string DateTime::ToString(DateFormat format, TimeFractionFormat fractionFormat) const +{ + if (format != DateFormat::Rfc3339) { throw std::invalid_argument( "Unrecognized date format (" + std::to_string(static_cast(format)) + ")."); } + ThrowIfUnsupportedYear(); + + int16_t year = 1; + + // The values that are not supposed to be read before they are written are set to -123... to avoid + // warnings on some compilers, yet provide a clearly bad value to make it obvious if things don't + // work as expected. + int8_t month = -123; + int8_t day = -123; + int8_t hour = -123; + int8_t minute = -123; + int8_t second = -123; + int32_t fracSec = -1234567890; + int8_t dayOfWeek = -123; + + GetDateTimeParts(&year, &month, &day, &hour, &minute, &second, &fracSec, &dayOfWeek); + + std::ostringstream dateString; + + dateString << std::setfill('0') << std::setw(4) << static_cast(year) << '-' << std::setw(2) + << static_cast(month) << '-' << std::setw(2) << static_cast(day) << 'T' + << std::setw(2) << static_cast(hour) << ':' << std::setw(2) + << static_cast(minute) << ':' << std::setw(2) << static_cast(second); + + if (fractionFormat == TimeFractionFormat::AllDigits) + { + dateString << '.' << std::setw(7) << static_cast(fracSec); + } + else if (fracSec != 0 && fractionFormat != TimeFractionFormat::Truncate) + { + // Append fractional second, which is a 7-digit value with no trailing zeros + // This way, '0001200' becomes '00012' + auto setw = 1; + auto frac = fracSec; + for (auto div = 1000000; div >= 1; div /= 10) + { + if ((fracSec % div) == 0) + { + frac /= div; + break; + } + ++setw; + } + + dateString << '.' << std::setw(setw) << static_cast(frac); + } + + dateString << 'Z'; + return dateString.str(); } diff --git a/sdk/core/azure-core/test/ut/datetime.cpp b/sdk/core/azure-core/test/ut/datetime.cpp index 057a97355..db566e5e2 100644 --- a/sdk/core/azure-core/test/ut/datetime.cpp +++ b/sdk/core/azure-core/test/ut/datetime.cpp @@ -34,6 +34,7 @@ TEST(DateTime, ParseDateBasic) { auto dt = DateTime::Parse("20130517", DateTime::DateFormat::Rfc3339); EXPECT_NE(0, dt.time_since_epoch().count()); + EXPECT_EQ(dt.ToString(), "2013-05-17T00:00:00Z"); } } @@ -135,6 +136,7 @@ TEST(DateTime, sameResultFromDefaultRfc3339) DateTime::DateFormat::Rfc3339, DateTime::TimeFractionFormat::DropTrailingZeros); auto const str2 = dt2.ToString(DateTime::DateFormat::Rfc3339); EXPECT_EQ(str1, str2); + EXPECT_EQ(str1, dt2.ToString()); } } @@ -446,6 +448,39 @@ TEST(DateTime, ParseTimeRoundtripAcceptsInvalidNoTrailingTimezone) } } +TEST(DateTime, ToStringNoArg) +{ + auto dt = DateTime::Parse("2013-05-17T01:02:03.1230000Z", DateTime::DateFormat::Rfc3339); + EXPECT_EQ(dt.ToString(), "2013-05-17T01:02:03.123Z"); +} + +TEST(DateTime, ToStringOneArg) +{ + auto dt = DateTime::Parse("2013-05-17T01:02:03.1230000Z", DateTime::DateFormat::Rfc3339); + EXPECT_EQ(dt.ToString(DateTime::DateFormat::Rfc3339), "2013-05-17T01:02:03.123Z"); + EXPECT_EQ(dt.ToString(DateTime::DateFormat::Rfc1123), "Fri, 17 May 2013 01:02:03 GMT"); +} + +TEST(DateTime, ToStringInvalid) +{ + auto dt = DateTime::Parse("2013-05-17T01:02:03.1230000Z", DateTime::DateFormat::Rfc3339); + + EXPECT_THROW(dt.ToString(static_cast(2)), std::invalid_argument); + + EXPECT_THROW( + dt.ToString(DateTime::DateFormat::Rfc1123, DateTime::TimeFractionFormat::AllDigits), + std::invalid_argument); + EXPECT_THROW( + dt.ToString(DateTime::DateFormat::Rfc1123, DateTime::TimeFractionFormat::DropTrailingZeros), + std::invalid_argument); + EXPECT_THROW( + dt.ToString(DateTime::DateFormat::Rfc1123, DateTime::TimeFractionFormat::Truncate), + std::invalid_argument); + EXPECT_THROW( + dt.ToString(DateTime::DateFormat::Rfc1123, static_cast(3)), + std::invalid_argument); +} + TEST(DateTime, ParseTimeInvalid2) { // Various unsupported cases. In all cases, we have produce an empty date time