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.
This commit is contained in:
Ahson Khan 2021-03-09 13:29:20 -08:00 committed by GitHub
parent 6582d3a6dc
commit dd3c79c9b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 185 additions and 82 deletions

View File

@ -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

View File

@ -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<int16_t>((years400 * 400) + (years100 * 100) + (years4 * 4) + years1);
*year += static_cast<int16_t>((years400 * 400) + (years100 * 100) + (years4 * 4) + years1);
dayOfWeek = WeekDayAndMonthDayOfYear(
&month,
&day,
year,
*dayOfWeek = WeekDayAndMonthDayOfYear(
month,
day,
*year,
static_cast<int16_t>(DivideAndUpdateRemainder(&remainder, OneDayIn100ns) + 1));
hour = static_cast<int8_t>(DivideAndUpdateRemainder(&remainder, OneHourIn100ns));
minute = static_cast<int8_t>(DivideAndUpdateRemainder(&remainder, OneMinuteIn100ns));
second = static_cast<int8_t>(DivideAndUpdateRemainder(&remainder, OneSecondIn100ns));
fracSec = static_cast<int32_t>(remainder);
*hour = static_cast<int8_t>(DivideAndUpdateRemainder(&remainder, OneHourIn100ns));
*minute = static_cast<int8_t>(DivideAndUpdateRemainder(&remainder, OneMinuteIn100ns));
*second = static_cast<int8_t>(DivideAndUpdateRemainder(&remainder, OneSecondIn100ns));
*fracSec = static_cast<int32_t>(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<int>(day) << ' ' << MonthNames[month - 1] << ' ' << std::setw(4)
<< static_cast<int>(year) << ' ' << std::setw(2) << static_cast<int>(hour) << ':'
<< std::setw(2) << static_cast<int>(minute) << ':' << std::setw(2)
<< static_cast<int>(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<int>(year) << '-' << std::setw(2)
<< static_cast<int>(month) << '-' << std::setw(2) << static_cast<int>(day) << 'T'
<< std::setw(2) << static_cast<int>(hour) << ':' << std::setw(2)
<< static_cast<int>(minute) << ':' << std::setw(2) << static_cast<int>(second);
if (fractionFormat == TimeFractionFormat::AllDigits)
{
dateString << '.' << std::setw(7) << static_cast<int>(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<int>(frac);
}
dateString << 'Z';
return DateTime::ToStringRfc1123();
}
else if (format == DateFormat::Rfc1123)
{
dateString << DayNames[dayOfWeek] << ", " << std::setfill('0') << std::setw(2)
<< static_cast<int>(day) << ' ' << MonthNames[month - 1] << ' ' << std::setw(4)
<< static_cast<int>(year) << ' ' << std::setw(2) << static_cast<int>(hour) << ':'
<< std::setw(2) << static_cast<int>(minute) << ':' << std::setw(2)
<< static_cast<int>(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<int64_t>(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<int>(year) << '-' << std::setw(2)
<< static_cast<int>(month) << '-' << std::setw(2) << static_cast<int>(day) << 'T'
<< std::setw(2) << static_cast<int>(hour) << ':' << std::setw(2)
<< static_cast<int>(minute) << ':' << std::setw(2) << static_cast<int>(second);
if (fractionFormat == TimeFractionFormat::AllDigits)
{
dateString << '.' << std::setw(7) << static_cast<int>(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<int>(frac);
}
dateString << 'Z';
return dateString.str();
}

View File

@ -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<DateTime::DateFormat>(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<DateTime::TimeFractionFormat>(3)),
std::invalid_argument);
}
TEST(DateTime, ParseTimeInvalid2)
{
// Various unsupported cases. In all cases, we have produce an empty date time