DateTime: API review feedback (#938)

This commit is contained in:
Anton Kolesnyk 2020-11-11 17:48:00 -08:00 committed by GitHub
parent 2ce6e28eb0
commit 56475b003a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 401 additions and 245 deletions

View File

@ -8,7 +8,7 @@
#pragma once
#include <stdexcept>
#include <chrono>
#include <string>
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<int64_t, std::ratio<1, 10000000>> 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<IntervalType>(-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<IntervalType>(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<DateTime::IntervalType>(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

View File

@ -6,6 +6,7 @@
#include <cstdio>
#include <cstring>
#include <limits>
#include <stdexcept>
#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<IntervalType>(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<int64_t>(m_interval);
int64_t const secondsSince1601 = epochAdjusted / TicksPerSecond; // convert to seconds
int const fracSec = static_cast<int>(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<int>(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<IntervalType>(daysSince1601) * SecondsInDay
+ static_cast<IntervalType>(hour) * SecondsInHour
+ static_cast<IntervalType>(minute) * SecondsInMinute + sec;
secondsSince1601 = static_cast<int64_t>(daysSince1601) * SecondsInDay
+ static_cast<int64_t>(hour) * SecondsInHour
+ static_cast<int64_t>(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<int64_t>(daysSince1601) * SecondsInDay;
result.m_interval = static_cast<IntervalType>(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<IntervalType>(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;
}

View File

@ -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<DateTime::Duration>(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<DateTime::Duration>(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<DateTime::Duration>(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<DateTime::Duration>(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<DateTime::Duration>(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<DateTime::Duration>(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<int>::max() - 1);
TestRfc1123IsTimeT(
"19 Jan 2038 03:14:06 GMT", static_cast<int64_t>(std::numeric_limits<int>::max()) - 1);
TestRfc1123IsTimeT("19 Jan 2038 03:13:07 -0001", std::numeric_limits<int>::max());
TestRfc1123IsTimeT("19 Jan 2038 03:14:07 -0000", std::numeric_limits<int>::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<DateTime::Duration>(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<DateTime::Duration>(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<DateTime::Duration>(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);
}

View File

@ -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<int> n);
EXPECT_NO_THROW(Azure::Core::Http::RawResponse r(
1, 1, Azure::Core::Http::HttpStatusCode::Accepted, "phrase"));