Add DateTime: supporting standardized string date and time representations. (#718)

This commit is contained in:
Anton Kolesnyk 2020-10-09 19:13:48 -07:00 committed by GitHub
parent cd2a8a3812
commit 6ae0c0daac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 1433 additions and 6 deletions

View File

@ -26,21 +26,23 @@ add_library (
src/context.cpp
src/credentials/credentials.cpp
src/credentials/policy/policies.cpp
src/datetime.cpp
src/http/body_stream.cpp
${CURL_TRANSPORT_ADAPTER_SRC}
src/http/http.cpp
src/http/logging_policy.cpp
src/http/policy.cpp
src/http/request.cpp
src/http/raw_response.cpp
src/http/request.cpp
src/http/retry_policy.cpp
src/http/transport_policy.cpp
src/http/telemetry_policy.cpp
src/http/transport_policy.cpp
src/http/url.cpp
${BUILD_WIN_TRANSPORT}
src/logging/logging.cpp
src/strings.cpp
src/version.cpp)
src/version.cpp
)
target_include_directories (${TARGET_NAME} PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/inc> $<INSTALL_INTERFACE:include/az_core>)

View File

@ -0,0 +1,174 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
/**
* @file
* @brief Support for date and time standardized string formats.
*/
#pragma once
#include <stdexcept>
#include <string>
namespace Azure { namespace Core {
/**
* @brief Manages date and time in standardized string formats.
*/
class DateTime {
public:
/// A type that represents tick spans.
typedef uint64_t IntervalType;
private:
// Number of seconds between 01-01-1970 and 01-01-1601.
static constexpr IntervalType WindowsToPosixOffsetSeconds = 11644473600LL;
public:
/**
* @brief Defines the supported date and time string formats.
*/
enum class DateFormat
{
/// RFC 1123.
Rfc1123,
/// ISO 8601.
Iso8601
};
/**
* @brief Get the current UTC time.
*/
static DateTime UtcNow();
/// An invalid UTC timestamp value.
static constexpr IntervalType UtcTimestampInvalid = static_cast<IntervalType>(-1);
/**
* @brief Get seconds since Unix/POSIX time epoch at `01-01-1970 00:00:00`.
* If time is before epoch, @UtcTimestampInvalid is returned.
*/
static IntervalType UtcTimestamp()
{
auto const seconds = UtcNow().ToInterval() / WindowsToPosixOffsetSeconds;
return (seconds >= WindowsToPosixOffsetSeconds) ? (seconds - WindowsToPosixOffsetSeconds)
: UtcTimestampInvalid;
}
/**
* @brief Construct an uninitialized (!@IsInitialized()) instance of @DateTime.
*/
DateTime() : m_interval(0) {}
/**
* @brief Create @DateTime from a string representing time in UTC in the specified format.
*
* @param timeString A string with the date and time.
* @param format A format to which /p timeString adheres to.
*
* @return @DateTime that was constructed from the \p timeString; Uninitialized
* (!@IsInitialized()) @DateTime if parsing \p timeString was not successful.
*
* @throw DateTimeException If \p format is not recognized.
*/
static DateTime FromString(
std::string const& timeString,
DateFormat format = DateFormat::Rfc1123);
/**
* @brief Get a string representation of the @DateTime.
*
* @param format The representation format to use.
*
* @throw DateTimeException If year exceeds 9999, or if \p format is not recognized.
*/
std::string ToString(DateFormat format = DateFormat::Rfc1123) const;
/// Get the integral time value.
IntervalType ToInterval() const { return m_interval; }
/// Subtract an interval from @DateTime.
DateTime operator-(IntervalType value) const { return DateTime(m_interval - value); }
/// Add an interval to @DateTime.
DateTime operator+(IntervalType value) const { return DateTime(m_interval + value); }
/// Compare two instances of @DateTime for equality.
bool operator==(DateTime dt) const { return m_interval == dt.m_interval; }
/// Compare two instances of @DateTime for inequality.
bool operator!=(const DateTime& dt) const { return !(*this == dt); }
/// Compare the chronological order of two @DateTime instances.
bool operator>(const DateTime& dt) const { return this->m_interval > dt.m_interval; }
/// Compare the chronological order of two @DateTime instances.
bool operator<(const DateTime& dt) const { return this->m_interval < dt.m_interval; }
/// Compare the chronological order of two @DateTime instances.
bool operator>=(const DateTime& dt) const { return this->m_interval >= dt.m_interval; }
/// Compare the chronological order of two @DateTime instances.
bool operator<=(const DateTime& dt) const { return this->m_interval <= dt.m_interval; }
/// Create an interval from milliseconds.
static IntervalType FromMilliseconds(unsigned int milliseconds)
{
return milliseconds * TicksPerMillisecond;
}
/// Create an interval from seconds.
static IntervalType FromSeconds(unsigned int seconds) { return seconds * TicksPerSecond; }
/// Create an interval from minutes.
static IntervalType FromMinutes(unsigned int minutes) { return minutes * TicksPerMinute; }
/// Create an interval from hours.
static IntervalType FromHours(unsigned int hours) { return hours * TicksPerHour; }
/// Create an interval from days.
static IntervalType FromDays(unsigned int days) { return days * TicksPerDay; }
/// Checks whether this instance of @DateTime is initialized.
bool IsInitialized() const { return m_interval != 0; }
private:
friend IntervalType operator-(DateTime t1, DateTime t2);
static constexpr IntervalType TicksPerMillisecond = static_cast<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) {}
};
}} // namespace Azure::Core

View File

@ -0,0 +1,792 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include "azure/core/datetime.hpp"
#include <cstdio>
#include <cstring>
#include <limits>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#else
#include <sys/time.h>
#endif
using namespace Azure::Core;
DateTime DateTime::UtcNow()
{
#ifdef _WIN32
FILETIME fileTime = {};
GetSystemTimeAsFileTime(&fileTime);
ULARGE_INTEGER largeInt = {};
largeInt.LowPart = fileTime.dwLowDateTime;
largeInt.HighPart = fileTime.dwHighDateTime;
return DateTime(largeInt.QuadPart);
#else
struct timeval time = {};
if (gettimeofday(&time, nullptr) != 0)
{
return DateTime();
}
IntervalType result = WindowsToPosixOffsetSeconds + time.tv_sec;
result *= TicksPerSecond; // convert to 10e-7
result += time.tv_usec * 10; // convert and add microseconds, 10e-6 to 10e-7
return DateTime(static_cast<IntervalType>(result));
#endif
}
namespace {
struct ComputeYearResult
{
int Year;
int SecondsLeftThisYear;
};
constexpr int SecondsInMinute = 60;
constexpr int SecondsInHour = SecondsInMinute * 60;
constexpr int SecondsInDay = 24 * SecondsInHour;
constexpr int DaysInYear = 365;
constexpr ComputeYearResult ComputeYear(int64_t secondsSince1900)
{
constexpr int64_t SecondsFrom1601To1900 = 9435484800LL;
constexpr int DaysIn4Years = DaysInYear * 4 + 1;
constexpr int DaysIn100Years = DaysIn4Years * 25 - 1;
constexpr int DaysIn400Years = DaysIn100Years * 4 + 1;
constexpr int SecondsInYear = SecondsInDay * DaysInYear;
constexpr int SecondsIn4Years = SecondsInDay * DaysIn4Years;
constexpr int64_t SecondsIn100Years = static_cast<int64_t>(SecondsInDay) * DaysIn100Years;
constexpr int64_t SecondsIn400Years = static_cast<int64_t>(SecondsInDay) * DaysIn400Years;
int64_t secondsLeft
= secondsSince1900 + SecondsFrom1601To1900; // shift to start of this 400 year cycle
int year400 = static_cast<int>(secondsLeft / SecondsIn400Years);
secondsLeft -= year400 * SecondsIn400Years;
int year100 = static_cast<int>(secondsLeft / SecondsIn100Years);
secondsLeft -= year100 * SecondsIn100Years;
int year4 = static_cast<int>(secondsLeft / SecondsIn4Years);
int secondsInt = static_cast<int>(secondsLeft - static_cast<int64_t>(year4) * SecondsIn4Years);
int year1 = secondsInt / SecondsInYear;
secondsInt -= year1 * SecondsInYear;
// shift back to 1900 base from 1601:
return {year400 * 400 + year100 * 100 + year4 * 4 + year1 - 299, secondsInt};
}
constexpr bool IsLeapYear(int year)
{
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
}
// The following table assumes no leap year; leap year is added separately
constexpr unsigned short const CumulativeDaysToMonth[12] = {
0, // Jan
31, // Feb
59, // Mar
90, // Apr
120, // May
151, // Jun
181, // Jul
212, // Aug
243, // Sep
273, // Oct
304, // Nov
334 // Dec
};
constexpr unsigned short const CumulativeDaysToMonthLeap[12] = {
0, // Jan
31, // Feb
60, // Mar
91, // Apr
121, // May
152, // Jun
182, // Jul
213, // Aug
244, // Sep
274, // Oct
305, // Nov
335 // Dec
};
constexpr char const dayNames[] = "Sun\0Mon\0Tue\0Wed\0Thu\0Fri\0Sat";
constexpr char const monthNames[] = "Jan\0Feb\0Mar\0Apr\0May\0Jun\0Jul\0Aug\0Sep\0Oct\0Nov\0Dec";
constexpr int64_t TicksFromWindowsEpochTo1900 = 0x014F373BFDE04000LL;
} // namespace
std::string DateTime::ToString(DateFormat format) const
{
if (m_interval > 2650467743990000000LL)
{
throw DateTimeException("The requested year exceeds the year 9999.");
}
int64_t const epochAdjusted = static_cast<int64_t>(m_interval) - TicksFromWindowsEpochTo1900;
int64_t const secondsSince1900 = epochAdjusted / TicksPerSecond; // convert to seconds
int const fracSec = static_cast<int>(epochAdjusted % TicksPerSecond);
auto const yearData = ComputeYear(secondsSince1900);
int const year = yearData.Year;
int const yearDay = yearData.SecondsLeftThisYear / SecondsInDay;
int leftover = yearData.SecondsLeftThisYear % SecondsInDay;
int const hour = leftover / SecondsInHour;
leftover = leftover % SecondsInHour;
int const minute = leftover / SecondsInMinute;
leftover = leftover % SecondsInMinute;
auto const& monthTable = IsLeapYear(year) ? CumulativeDaysToMonthLeap : CumulativeDaysToMonth;
int month = 0;
while (month < 11 && monthTable[month + 1] <= yearDay)
{
++month;
}
auto const monthDay = yearDay - monthTable[month] + 1;
auto const weekday = static_cast<int>((secondsSince1900 / SecondsInDay + 1) % 7);
char outBuffer[38]{}; // Thu, 01 Jan 1970 00:00:00 GMT\0
// 1970-01-01T00:00:00.1234567Z\0
char* outCursor = outBuffer;
switch (format)
{
case DateFormat::Rfc1123:
#ifdef _MSC_VER
sprintf_s(
#else
std::sprintf(
#endif
outCursor,
#ifdef _MSC_VER
26,
#endif
"%s, %02d %s %04d %02d:%02d:%02d",
dayNames + 4ULL * static_cast<uint64_t>(weekday),
monthDay,
monthNames + 4ULL * static_cast<uint64_t>(month),
year + 1900,
hour,
minute,
leftover);
outCursor += 25;
memcpy(outCursor, " GMT", 4);
outCursor += 4;
return std::string(outBuffer, outCursor);
case DateFormat::Iso8601:
#ifdef _MSC_VER
sprintf_s(
#else
std::sprintf(
#endif
outCursor,
#ifdef _MSC_VER
20,
#endif
"%04d-%02d-%02dT%02d:%02d:%02d",
year + 1900,
month + 1,
monthDay,
hour,
minute,
leftover);
outCursor += 19;
if (fracSec != 0)
{
// Append fractional second, which is a 7-digit value with no trailing zeros
// This way, '1200' becomes '00012'
size_t appended =
#ifdef _MSC_VER
sprintf_s(
#else
std::sprintf(
#endif
outCursor,
#ifdef _MSC_VER
9,
#endif
".%07d",
fracSec);
while (outCursor[appended - 1] == '0')
{
--appended; // trim trailing zeros
}
outCursor += appended;
}
*outCursor = 'Z';
++outCursor;
return std::string(outBuffer, outCursor);
default:
throw DateTimeException("Unrecognized date format.");
}
}
namespace {
constexpr bool StringStartsWith(char const* haystack, char const* needle)
{
while (*needle)
{
if (*haystack != *needle)
{
return false;
}
++haystack;
++needle;
}
return true;
}
template <int N = 9> constexpr bool IsDigit(char c)
{
return ((unsigned char)((unsigned char)(c) - '0') <= N);
}
constexpr int StringToDoubleDigitInt(char const* str)
{
return (static_cast<unsigned char>(str[0]) - '0') * 10
+ (static_cast<unsigned char>(str[1]) - '0');
}
constexpr bool ValidateDay(int day, int month, int year)
{
if (day < 1)
{
return false;
}
// Month is 0-based
switch (month)
{
case 1: // Feb
return day <= (28 + IsLeapYear(year));
break;
case 3: // Apr
case 5: // Jun
case 8: // Sep
case 10: // Nov
return day <= 30;
break;
default:
return day <= 31;
}
}
constexpr int GetYearDay(int month, int monthDay, int year)
{
return CumulativeDaysToMonth[month] + monthDay + (IsLeapYear(year) && month > 1) - 1;
}
constexpr int CountLeapYears(int yearsSince1900)
{
int tmpYears = yearsSince1900 + 299; // shift into 1601, the first 400 year cycle including 1900
int const year400 = tmpYears / 400;
tmpYears -= year400 * 400;
int result = year400 * 97;
int const year100 = tmpYears / 100;
tmpYears -= year100 * 100;
result += year100 * 24;
result += tmpYears / 4;
// subtract off leap years from 1601
result -= 72;
return result;
}
constexpr int64_t AdjustTimezone(
int64_t result,
unsigned char chSign,
int adjustHours,
int adjustMinutes)
{
if (adjustHours > 23)
{
return -1;
}
// adjustMinutes > 59 is impossible due to digit 5 check
int const tzAdjust = adjustMinutes * 60 + adjustHours * 60 * 60;
if (chSign == '-')
{
if (std::numeric_limits<int64_t>::max() - result < tzAdjust)
{
return -1;
}
result += tzAdjust;
}
else
{
if (tzAdjust > result)
{
return -1;
}
result -= tzAdjust;
}
return result;
}
} // namespace
/*
https://tools.ietf.org/html/rfc822
https://tools.ietf.org/html/rfc1123
date-time = [ day "," ] date time ; dd mm yy
; hh:mm:ss zzz
day = "Mon" / "Tue" / "Wed" / "Thu"
/ "Fri" / "Sat" / "Sun"
date = 1*2DIGIT month 2DIGIT ; day month year
; e.g. 20 Jun 82
RFC1123 changes this to:
date = 1*2DIGIT month 2*4DIGIT ; day month year
; e.g. 20 Jun 1982
This implementation only accepts 4 digit years.
month = "Jan" / "Feb" / "Mar" / "Apr"
/ "May" / "Jun" / "Jul" / "Aug"
/ "Sep" / "Oct" / "Nov" / "Dec"
time = hour zone ; ANSI and Military
hour = 2DIGIT ":" 2DIGIT [":" 2DIGIT]
; 00:00:00 - 23:59:59
zone = "UT" / "GMT" ; Universal Time
; North American : UT
/ "EST" / "EDT" ; Eastern: - 5/ - 4
/ "CST" / "CDT" ; Central: - 6/ - 5
/ "MST" / "MDT" ; Mountain: - 7/ - 6
/ "PST" / "PDT" ; Pacific: - 8/ - 7
// military time deleted by RFC 1123
/ ( ("+" / "-") 4DIGIT ) ; Local differential
; hours+min. (HHMM)
*/
DateTime DateTime::FromString(std::string const& dateString, DateFormat format)
{
DateTime result = {};
int64_t secondsSince1900 = 0;
uint64_t fracSec = 0;
auto str = dateString.c_str();
if (format == DateFormat::Rfc1123)
{
int parsedWeekday = 0;
for (; parsedWeekday < 7; ++parsedWeekday)
{
if (StringStartsWith(str, dayNames + static_cast<uint64_t>(parsedWeekday) * 4ULL)
&& str[3] == ',' && str[4] == ' ')
{
str += 5; // parsed day of week
break;
}
}
int monthDay;
if (IsDigit<3>(str[0]) && IsDigit(str[1]) && str[2] == ' ')
{
monthDay = StringToDoubleDigitInt(str); // validity checked later
str += 3; // parsed day
}
else if (IsDigit(str[0]) && str[1] == ' ')
{
monthDay = str[0] - '0';
str += 2; // parsed day
}
else
{
return result;
}
if (monthDay == 0)
{
return result;
}
int month = 0;
for (;;)
{
if (StringStartsWith(str, monthNames + static_cast<uint64_t>(month) * 4ULL))
{
break;
}
++month;
if (month == 12)
{
return result;
}
}
if (str[3] != ' ')
{
return result;
}
str += 4; // parsed month
if (!IsDigit(str[0]) || !IsDigit(str[1]) || !IsDigit(str[2]) || !IsDigit(str[3])
|| str[4] != ' ')
{
return result;
}
int year = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0');
if (year < 1900)
{
return result;
}
// days in month validity check
if (!ValidateDay(monthDay, month, year))
{
return result;
}
str += 5; // parsed year
int const yearDay = GetYearDay(month, monthDay, year);
if (!IsDigit<2>(str[0]) || !IsDigit(str[1]) || str[2] != ':' || !IsDigit<5>(str[3])
|| !IsDigit(str[4]))
{
return result;
}
int const hour = StringToDoubleDigitInt(str);
if (hour > 23)
{
return result;
}
str += 3; // parsed hour
int const minute = StringToDoubleDigitInt(str);
str += 2; // parsed mins
int sec = 0;
if (str[0] == ':')
{
if (!IsDigit<6>(str[1]) || !IsDigit(str[2]) || str[3] != ' ')
{
return result;
}
sec = StringToDoubleDigitInt(str + 1);
str += 4; // parsed seconds
}
else if (str[0] == ' ')
{
str += 1; // parsed seconds
}
else
{
return result;
}
if (sec > 60)
{ // 60 to allow leap seconds
return result;
}
year -= 1900;
int const daysSince1900 = year * DaysInYear + CountLeapYears(year) + yearDay;
if (parsedWeekday != 7)
{
int const actualWeekday = (daysSince1900 + 1) % 7;
if (parsedWeekday != actualWeekday)
{
return result;
}
}
secondsSince1900 = static_cast<IntervalType>(daysSince1900) * SecondsInDay
+ static_cast<IntervalType>(hour) * SecondsInHour
+ static_cast<IntervalType>(minute) * SecondsInMinute + sec;
fracSec = 0;
if (!StringStartsWith(str, "GMT") && !StringStartsWith(str, "UT"))
{
// some timezone adjustment necessary
auto tzCh = '-';
int tzHours;
int tzMinutes = 0;
if (StringStartsWith(str, "EDT"))
{
tzHours = 4;
}
else if (StringStartsWith(str, "EST") || StringStartsWith(str, "CDT"))
{
tzHours = 5;
}
else if (StringStartsWith(str, "CST") || StringStartsWith(str, "MDT"))
{
tzHours = 6;
}
else if (StringStartsWith(str, "MST") || StringStartsWith(str, "PDT"))
{
tzHours = 7;
}
else if (StringStartsWith(str, "PST"))
{
tzHours = 8;
}
else if (
(str[0] == '+' || str[0] == '-') && IsDigit<2>(str[1]) && IsDigit(str[2])
&& IsDigit<5>(str[3]) && IsDigit(str[4]))
{
tzCh = str[0];
tzHours = StringToDoubleDigitInt(str + 1);
tzMinutes = StringToDoubleDigitInt(str + 3);
}
else
{
return result;
}
secondsSince1900
= AdjustTimezone(secondsSince1900, static_cast<unsigned char>(tzCh), tzHours, tzMinutes);
if (secondsSince1900 < 0)
{
return result;
}
}
}
else if (format == DateFormat::Iso8601)
{
// parse year
if (!IsDigit(str[0]) || !IsDigit(str[1]) || !IsDigit(str[2]) || !IsDigit(str[3]))
{
return result;
}
int year = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0');
if (year < 1900)
{
return result;
}
str += 4;
if (*str == '-')
{
++str;
}
// parse month
if (!IsDigit<1>(str[0]) || !IsDigit(str[1]))
{
return result;
}
int month = StringToDoubleDigitInt(str);
if (month < 1 || month > 12)
{
return result;
}
month -= 1;
str += 2;
if (*str == '-')
{
++str;
}
// parse day
if (!IsDigit<3>(str[0]) || !IsDigit(str[1]))
{
return result;
}
int monthDay = StringToDoubleDigitInt(str);
if (!ValidateDay(monthDay, month, year))
{
return result;
}
int const yearDay = GetYearDay(month, monthDay, year);
str += 2;
year -= 1900;
int daysSince1900 = year * DaysInYear + CountLeapYears(year) + yearDay;
if (str[0] != 'T' && str[0] != 't')
{
// No time
secondsSince1900 = static_cast<int64_t>(daysSince1900) * SecondsInDay;
result.m_interval = static_cast<IntervalType>(
secondsSince1900 * TicksPerSecond + fracSec + TicksFromWindowsEpochTo1900);
return result;
}
++str; // skip 'T'
// parse hour
if (!IsDigit<2>(str[0]) || !IsDigit(str[1]))
{
return result;
}
int const hour = StringToDoubleDigitInt(str);
str += 2;
if (hour > 23)
{
return result;
}
if (*str == ':')
{
++str;
}
// parse minute
if (!IsDigit<5>(str[0]) || !IsDigit(str[1]))
{
return result;
}
int const minute = StringToDoubleDigitInt(str);
// minute > 59 is impossible because we checked that the first digit is <= 5 in the basic format
// check above
str += 2;
if (*str == ':')
{
++str;
}
// parse seconds
if (!IsDigit<6>(str[0]) || !IsDigit(str[1]))
{
return result;
}
int const sec = StringToDoubleDigitInt(str);
// We allow 60 to account for leap seconds
if (sec > 60)
{
return result;
}
str += 2;
if (str[0] == '.' && IsDigit(str[1]))
{
++str;
int digits = 7;
for (;;)
{
fracSec *= 10;
fracSec += static_cast<uint64_t>(*str) - static_cast<uint64_t>('0');
--digits;
++str;
if (digits == 0)
{
while (IsDigit(*str))
{
// consume remaining fractional second digits we can't use
++str;
}
break;
}
if (!IsDigit(*str))
{
// no more digits in the input, do the remaining multiplies we need
for (; digits != 0; --digits)
{
fracSec *= 10;
}
break;
}
}
}
secondsSince1900 = static_cast<int64_t>(daysSince1900) * SecondsInDay
+ static_cast<int64_t>(hour) * SecondsInHour
+ static_cast<int64_t>(minute) * SecondsInMinute + sec;
if (str[0] == 'Z' || str[0] == 'z')
{
// no adjustment needed for zulu time
}
else if (str[0] == '+' || str[0] == '-')
{
unsigned char const offsetDirection = static_cast<unsigned char>(str[0]);
if (!IsDigit<2>(str[1]) || !IsDigit(str[2]) || str[3] != ':' || !IsDigit<5>(str[4])
|| !IsDigit(str[5]))
{
return result;
}
secondsSince1900 = AdjustTimezone(
secondsSince1900,
offsetDirection,
StringToDoubleDigitInt(str + 1),
StringToDoubleDigitInt(str + 4));
if (secondsSince1900 < 0)
{
return result;
}
}
else
{
// the timezone is malformed, but cpprestsdk currently accepts this as no timezone
}
}
else
{
throw DateTimeException("Unrecognized date format.");
}
result.m_interval = static_cast<IntervalType>(
secondsSince1900 * TicksPerSecond + fracSec + TicksFromWindowsEpochTo1900);
return result;
}

View File

@ -22,18 +22,18 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
include(GoogleTest)
add_executable (
${TARGET_NAME}
context.cpp
datetime.cpp
http.cpp
http.hpp
logging.cpp
main.cpp
nullable.cpp
string.cpp
telemetry_policy.cpp
transport_adapter.cpp
transport_adapter.hpp
transport_adapter_file_upload.cpp
uuid.cpp
context.cpp)
)
target_link_libraries(${TARGET_NAME} PRIVATE azure-core gtest)

View File

@ -0,0 +1,459 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include "gtest/gtest.h"
#include <azure/core/datetime.hpp>
#include <limits>
using namespace Azure::Core;
TEST(DateTime, ParseDateAndTimeBasic)
{
auto dt1 = DateTime::FromString("20130517T00:00:00Z", DateTime::DateFormat::Iso8601);
EXPECT_NE(0u, dt1.ToInterval());
auto dt2 = DateTime::FromString("Fri, 17 May 2013 00:00:00 GMT", DateTime::DateFormat::Rfc1123);
EXPECT_NE(0u, dt2.ToInterval());
EXPECT_EQ(dt1.ToInterval(), dt2.ToInterval());
}
TEST(DateTime, ParseDateAndTimeExtended)
{
auto dt1 = DateTime::FromString("2013-05-17T00:00:00Z", DateTime::DateFormat::Iso8601);
EXPECT_NE(0u, dt1.ToInterval());
auto dt2 = DateTime::FromString("Fri, 17 May 2013 00:00:00 GMT", DateTime::DateFormat::Rfc1123);
EXPECT_NE(0u, dt2.ToInterval());
EXPECT_EQ(dt1.ToInterval(), dt2.ToInterval());
}
TEST(DateTime, ParseDateBasic)
{
{
auto dt = DateTime::FromString("20130517", DateTime::DateFormat::Iso8601);
EXPECT_NE(0u, dt.ToInterval());
}
}
TEST(DateTime, ParseDateExtended)
{
{
auto dt = DateTime::FromString("2013-05-17", DateTime::DateFormat::Iso8601);
EXPECT_NE(0u, dt.ToInterval());
}
}
namespace {
void TestDateTimeRoundtrip(std::string const& str, std::string const& strExpected)
{
auto dt = DateTime::FromString(str, DateTime::DateFormat::Iso8601);
auto const str2 = dt.ToString(DateTime::DateFormat::Iso8601);
EXPECT_EQ(str2, strExpected);
}
void TestDateTimeRoundtrip(std::string const& str) { TestDateTimeRoundtrip(str, str); }
} // namespace
TEST(DateTime, ParseTimeRoundrip1)
{
// Preserve all 7 digits after the comma:
TestDateTimeRoundtrip("2013-11-19T14:30:59.1234567Z");
}
TEST(DateTime, ParseTimeRoundrip2)
{
// lose the last '000'
TestDateTimeRoundtrip("2013-11-19T14:30:59.1234567000Z", "2013-11-19T14:30:59.1234567Z");
// lose the last '999' without rounding up
TestDateTimeRoundtrip("2013-11-19T14:30:59.1234567999Z", "2013-11-19T14:30:59.1234567Z");
}
TEST(DateTime, ParseTimeRoundrip3)
{
// leading 0-s after the comma, tricky to parse correctly
TestDateTimeRoundtrip("2013-11-19T14:30:59.00123Z");
}
TEST(DateTime, ParseTimeRoundrip4)
{
// another leading 0 test
TestDateTimeRoundtrip("2013-11-19T14:30:59.0000001Z");
}
TEST(DateTime, ParseTimeRoundrip5)
{
// this is going to be truncated
TestDateTimeRoundtrip("2013-11-19T14:30:59.00000001Z", "2013-11-19T14:30:59Z");
}
TEST(DateTime, ParseTimeRoundrip6)
{
// Only one digit after the dot
TestDateTimeRoundtrip("2013-11-19T14:30:59.5Z");
}
TEST(DateTime, ParseTimeRoundripYear1900) { TestDateTimeRoundtrip("1900-01-01T00:00:00Z"); }
TEST(DateTime, ParseTimeRoundripYear9999) { TestDateTimeRoundtrip("9999-12-31T23:59:59Z"); }
TEST(DateTime, EmittingTimeCorrectDay)
{
auto const test = DateTime() + 132004507640000000LL; // 2019-04-22T23:52:44 is a Monday
auto const actual = test.ToString(DateTime::DateFormat::Rfc1123);
std::string const expected("Mon");
EXPECT_EQ(actual.substr(0, 3), expected);
}
namespace {
void TestRfc1123IsTimeT(char const* str, DateTime::IntervalType t)
{
auto const dt = DateTime::FromString(str, DateTime::DateFormat::Rfc1123);
auto interval = dt.ToInterval();
EXPECT_EQ(0, interval % 10000000);
interval /= 10000000;
interval -= 11644473600; // NT epoch adjustment
EXPECT_EQ(t, interval);
}
} // namespace
TEST(DateTime, ParseTimeRfc1123AcceptsEachDay)
{
TestRfc1123IsTimeT("01 Jan 1970 00:00:00 GMT", 0);
TestRfc1123IsTimeT("Fri, 02 Jan 1970 00:00:00 GMT", 86400 * 1);
TestRfc1123IsTimeT("Sat, 03 Jan 1970 00:00:00 GMT", 86400 * 2);
TestRfc1123IsTimeT("Sun, 04 Jan 1970 00:00:00 GMT", 86400 * 3);
TestRfc1123IsTimeT("Mon, 05 Jan 1970 00:00:00 GMT", 86400 * 4);
TestRfc1123IsTimeT("Tue, 06 Jan 1970 00:00:00 GMT", 86400 * 5);
TestRfc1123IsTimeT("Wed, 07 Jan 1970 00:00:00 GMT", 86400 * 6);
}
TEST(DateTime, ParseTimeRfc1123BoundaryCases)
{
TestRfc1123IsTimeT("01 Jan 1970 00:00:00 GMT", 0);
TestRfc1123IsTimeT("19 Jan 2038 03:14:06 GMT", std::numeric_limits<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);
TestRfc1123IsTimeT("14 Jan 2019 23:16:21 -0001", 1547507841);
TestRfc1123IsTimeT("14 Jan 2019 23:16:21 +0001", 1547507721);
TestRfc1123IsTimeT("14 Jan 2019 23:16:21 -0100", 1547511381);
TestRfc1123IsTimeT("14 Jan 2019 23:16:21 +0100", 1547504181);
}
TEST(DateTime, ParseTimeRfc1123UseEachField)
{
TestRfc1123IsTimeT("02 Jan 1970 00:00:00 GMT", 86400);
TestRfc1123IsTimeT("12 Jan 1970 00:00:00 GMT", 950400);
TestRfc1123IsTimeT("01 Feb 1970 00:00:00 GMT", 2678400);
TestRfc1123IsTimeT("01 Jan 2000 00:00:00 GMT", 946684800);
TestRfc1123IsTimeT("01 Jan 2100 00:00:00 GMT", 4102444800);
TestRfc1123IsTimeT("01 Jan 1990 00:00:00 GMT", 631152000);
TestRfc1123IsTimeT("01 Jan 1971 00:00:00 GMT", 31536000);
TestRfc1123IsTimeT("01 Jan 1970 10:00:00 GMT", 36000);
TestRfc1123IsTimeT("01 Jan 1970 01:00:00 GMT", 3600);
TestRfc1123IsTimeT("01 Jan 1970 00:10:00 GMT", 600);
TestRfc1123IsTimeT("01 Jan 1970 00:01:00 GMT", 60);
TestRfc1123IsTimeT("01 Jan 1970 00:00:10 GMT", 10);
TestRfc1123IsTimeT("01 Jan 1970 00:00:01 GMT", 1);
TestRfc1123IsTimeT("01 Jan 1970 10:00:00 GMT", 36000);
TestRfc1123IsTimeT("01 Jan 1970 02:00:00 PST", 36000);
TestRfc1123IsTimeT("01 Jan 1970 03:00:00 PDT", 36000);
TestRfc1123IsTimeT("01 Jan 1970 03:00:00 MST", 36000);
TestRfc1123IsTimeT("01 Jan 1970 04:00:00 MDT", 36000);
TestRfc1123IsTimeT("01 Jan 1970 04:00:00 CST", 36000);
TestRfc1123IsTimeT("01 Jan 1970 05:00:00 CDT", 36000);
TestRfc1123IsTimeT("01 Jan 1970 05:00:00 EST", 36000);
TestRfc1123IsTimeT("01 Jan 1970 06:00:00 EDT", 36000);
TestRfc1123IsTimeT("01 Jan 1970 06:00:00 -0400", 36000);
TestRfc1123IsTimeT("01 Jan 1970 05:59:00 -0401", 36000);
}
TEST(DateTime, ParseTimeRfc1123MaxDays)
{
TestRfc1123IsTimeT("31 Jan 1970 00:00:00 GMT", 2592000);
TestRfc1123IsTimeT("28 Feb 2019 00:00:00 GMT", 1551312000); // non leap year allows feb 28
TestRfc1123IsTimeT("29 Feb 2020 00:00:00 GMT", 1582934400); // leap year allows feb 29
TestRfc1123IsTimeT("31 Mar 1970 00:00:00 GMT", 7689600);
TestRfc1123IsTimeT("30 Apr 1970 00:00:00 GMT", 10281600);
TestRfc1123IsTimeT("31 May 1970 00:00:00 GMT", 12960000);
TestRfc1123IsTimeT("30 Jun 1970 00:00:00 GMT", 15552000);
TestRfc1123IsTimeT("31 Jul 1970 00:00:00 GMT", 18230400);
TestRfc1123IsTimeT("31 Aug 1970 00:00:00 GMT", 20908800);
TestRfc1123IsTimeT("30 Sep 1970 00:00:00 GMT", 23500800);
TestRfc1123IsTimeT("31 Oct 1970 00:00:00 GMT", 26179200);
TestRfc1123IsTimeT("30 Nov 1970 00:00:00 GMT", 28771200);
TestRfc1123IsTimeT("31 Dec 1970 00:00:00 GMT", 31449600);
}
TEST(DateTime, ParseTimeRfc1123InvalidCases)
{
std::string const badStrings[] = {
"Ahu, 01 Jan 1970 00:00:00 GMT", // bad letters in each place
"TAu, 01 Jan 1970 00:00:00 GMT",
"ThA, 01 Jan 1970 00:00:00 GMT",
"ThuA 01 Jan 1970 00:00:00 GMT",
"Thu,A01 Jan 1970 00:00:00 GMT",
"Thu, A1 Jan 1970 00:00:00 GMT",
"Thu, 0A Jan 1970 00:00:00 GMT",
"Thu, 01AJan 1970 00:00:00 GMT",
"Thu, 01 Aan 1970 00:00:00 GMT",
"Thu, 01 JAn 1970 00:00:00 GMT",
"Thu, 01 JaA 1970 00:00:00 GMT",
"Thu, 01 JanA1970 00:00:00 GMT",
"Thu, 01 Jan A970 00:00:00 GMT",
"Thu, 01 Jan 1A70 00:00:00 GMT",
"Thu, 01 Jan 19A0 00:00:00 GMT",
"Thu, 01 Jan 197A 00:00:00 GMT",
"Thu, 01 Jan 1970A00:00:00 GMT",
"Thu, 01 Jan 1970 A0:00:00 GMT",
"Thu, 01 Jan 1970 0A:00:00 GMT",
"Thu, 01 Jan 1970 00A00:00 GMT",
"Thu, 01 Jan 1970 00:A0:00 GMT",
"Thu, 01 Jan 1970 00:0A:00 GMT",
"Thu, 01 Jan 1970 00:00A00 GMT",
"Thu, 01 Jan 1970 00:00:A0 GMT",
"Thu, 01 Jan 1970 00:00:0A GMT",
"Thu, 01 Jan 1970 00:00:00AGMT",
"Thu, 01 Jan 1970 00:00:00 AMT",
"Thu, 01 Jan 1970 00:00:00 GAT",
"Thu, 01 Jan 1970 00:00:00 GMA",
"", // truncation
"T",
"Th",
"Thu",
"Thu,",
"Thu, ",
"Thu, 0",
"Thu, 01",
"Thu, 01 ",
"Thu, 01 J",
"Thu, 01 Ja",
"Thu, 01 Jan",
"Thu, 01 Jan ",
"Thu, 01 Jan 1",
"Thu, 01 Jan 19",
"Thu, 01 Jan 197",
"Thu, 01 Jan 1970",
"Thu, 01 Jan 1970 ",
"Thu, 01 Jan 1970 0",
"Thu, 01 Jan 1970 00",
"Thu, 01 Jan 1970 00:",
"Thu, 01 Jan 1970 00:0",
"Thu, 01 Jan 1970 00:00",
"Thu, 01 Jan 1970 00:00:",
"Thu, 01 Jan 1970 00:00:0",
"Thu, 01 Jan 1970 00:00:00",
"Thu, 01 Jan 1970 00:00:00 ",
"Thu, 01 Jan 1970 00:00:00 G",
"Thu, 01 Jan 1970 00:00:00 GM",
"Fri, 01 Jan 1970 00:00:00 GMT", // wrong day
"01 Jan 1899 00:00:00 GMT", // year too small
"01 Xxx 1971 00:00:00 GMT", // month bad
"00 Jan 1971 00:00:00 GMT", // day too small
"32 Jan 1971 00:00:00 GMT", // day too big
"30 Feb 1971 00:00:00 GMT", // day too big for feb
"30 Feb 1971 00:00:00 GMT", // day too big for feb (non-leap year)
"32 Mar 1971 00:00:00 GMT", // other months
"31 Apr 1971 00:00:00 GMT",
"32 May 1971 00:00:00 GMT",
"31 Jun 1971 00:00:00 GMT",
"32 Jul 1971 00:00:00 GMT",
"32 Aug 1971 00:00:00 GMT",
"31 Sep 1971 00:00:00 GMT",
"32 Oct 1971 00:00:00 GMT",
"31 Nov 1971 00:00:00 GMT",
"32 Dec 1971 00:00:00 GMT",
"01 Jan 1971 70:00:00 GMT", // hour too big
"01 Jan 1971 24:00:00 GMT",
"01 Jan 1971 00:60:00 GMT", // minute too big
"01 Jan 1971 00:00:70 GMT", // second too big
"01 Jan 1971 00:00:61 GMT",
"01 Jan 1899 00:00:00 GMT", // underflow
"01 Jan 1969 00:00:00 CEST", // bad tz
"14 Jan 2019 23:16:21 G0100", // bad tzoffsets
"01 Jan 1970 00:00:00 +2400",
"01 Jan 1970 00:00:00 -3000",
"01 Jan 1970 00:00:00 +2160",
"01 Jan 1970 00:00:00 -2400",
"01 Jan 1970 00:00:00 -2160",
"00 Jan 1971 00:00:00 GMT", // zero month day
};
for (auto const& str : badStrings)
{
auto const dt = DateTime::FromString(str, DateTime::DateFormat::Rfc1123);
EXPECT_EQ(0, dt.ToInterval());
}
}
TEST(DateTime, ParseTimeIso8601BoundaryCases)
{
// boundary cases:
TestDateTimeRoundtrip("1970-01-01T00:00:00Z"); // epoch
TestDateTimeRoundtrip("2038-01-19T03:14:06+00:00", "2038-01-19T03:14:06Z"); // INT_MAX - 1
TestDateTimeRoundtrip(
"2038-01-19T03:13:07-00:01",
"2038-01-19T03:14:07Z"); // INT_MAX after subtacting 1
TestDateTimeRoundtrip("2038-01-19T03:14:07-00:00", "2038-01-19T03:14:07Z");
}
TEST(DateTime, ParseTimeIso8601UsesEachTimezoneDigit)
{
TestDateTimeRoundtrip("2019-01-14T23:16:21+00:00", "2019-01-14T23:16:21Z");
TestDateTimeRoundtrip("2019-01-14T23:16:21-00:01", "2019-01-14T23:17:21Z");
TestDateTimeRoundtrip("2019-01-14T23:16:21+00:01", "2019-01-14T23:15:21Z");
TestDateTimeRoundtrip("2019-01-14T23:16:21-01:00", "2019-01-15T00:16:21Z");
TestDateTimeRoundtrip("2019-01-14T23:16:21+01:00", "2019-01-14T22:16:21Z");
}
TEST(DateTime, ParseTimeIso8601UsesEachDigit)
{
TestDateTimeRoundtrip("1970-01-01T00:00:01Z");
TestDateTimeRoundtrip("1970-01-01T00:01:00Z");
TestDateTimeRoundtrip("1970-01-01T01:00:00Z");
TestDateTimeRoundtrip("1970-01-02T00:00:00Z");
TestDateTimeRoundtrip("1970-02-01T00:00:00Z");
TestDateTimeRoundtrip("1971-01-01T00:00:00Z");
TestDateTimeRoundtrip("1999-01-01T00:00:00Z");
TestDateTimeRoundtrip("1970-12-01T00:00:00Z");
TestDateTimeRoundtrip("1970-09-01T00:00:00Z");
TestDateTimeRoundtrip("1970-01-30T00:00:00Z");
TestDateTimeRoundtrip("1970-01-31T00:00:00Z");
TestDateTimeRoundtrip("1970-01-01T23:00:00Z");
TestDateTimeRoundtrip("1970-01-01T19:00:00Z");
TestDateTimeRoundtrip("1970-01-01T00:59:00Z");
TestDateTimeRoundtrip("1970-01-01T00:00:59Z");
TestDateTimeRoundtrip("1970-01-01T00:00:60Z", "1970-01-01T00:01:00Z"); // leap seconds
}
TEST(DateTime, ParseTimeIso8601AcceptsMonthMaxDays)
{
TestDateTimeRoundtrip("1970-01-31T00:00:00Z"); // jan
TestDateTimeRoundtrip("2019-02-28T00:00:00Z"); // non leap year allows feb 28
TestDateTimeRoundtrip("2020-02-29T00:00:00Z"); // leap year allows feb 29
TestDateTimeRoundtrip("1970-03-31T00:00:00Z"); // mar
TestDateTimeRoundtrip("1970-04-30T00:00:00Z"); // apr
TestDateTimeRoundtrip("1970-05-31T00:00:00Z"); // may
TestDateTimeRoundtrip("1970-06-30T00:00:00Z"); // jun
TestDateTimeRoundtrip("1970-07-31T00:00:00Z"); // jul
TestDateTimeRoundtrip("1970-08-31T00:00:00Z"); // aug
TestDateTimeRoundtrip("1970-09-30T00:00:00Z"); // sep
TestDateTimeRoundtrip("1970-10-31T00:00:00Z"); // oct
TestDateTimeRoundtrip("1970-11-30T00:00:00Z"); // nov
TestDateTimeRoundtrip("1970-12-31T00:00:00Z"); // dec
}
TEST(DateTime, ParseTimeIso8601AcceptsLowercaseTZ)
{
TestDateTimeRoundtrip("1970-01-01t00:00:00Z", "1970-01-01T00:00:00Z");
TestDateTimeRoundtrip("1970-01-01T00:00:00z", "1970-01-01T00:00:00Z");
}
TEST(DateTime, ParseTimeRoundtripAcceptsInvalidNoTrailingTimezone)
{
// No digits after the dot, or non-digits. This is not a valid input, but we should not choke on
// it, Simply ignore the bad fraction
std::string const badStrings[] = {"2013-11-19T14:30:59.Z", "2013-11-19T14:30:59.a12Z"};
std::string const strCorrected = "2013-11-19T14:30:59Z";
for (auto const& str : badStrings)
{
auto const dt = DateTime::FromString(str, DateTime::DateFormat::Iso8601);
auto const str2 = dt.ToString(DateTime::DateFormat::Iso8601);
EXPECT_EQ(str2, strCorrected);
}
}
TEST(DateTime, ParseTimeInvalid2)
{
// Various unsupported cases. In all cases, we have produce an empty date time
std::string const badStrings[] = {
"", // empty
".Z", // too short
".Zx", // no trailing Z
"3.14Z" // not a valid date
"a971-01-01T00:00:00Z", // any non digits or valid separators
"1a71-01-01T00:00:00Z",
"19a1-01-01T00:00:00Z",
"197a-01-01T00:00:00Z",
"1971a01-01T00:00:00Z",
"1971-a1-01T00:00:00Z",
"1971-0a-01T00:00:00Z",
"1971-01a01T00:00:00Z",
"1971-01-a1T00:00:00Z",
"1971-01-0aT00:00:00Z",
// "1971-01-01a00:00:00Z", parsed as complete date
"1971-01-01Ta0:00:00Z",
"1971-01-01T0a:00:00Z",
"1971-01-01T00a00:00Z",
"1971-01-01T00:a0:00Z",
"1971-01-01T00:0a:00Z",
"1971-01-01T00:00a00Z",
"1971-01-01T00:00:a0Z",
"1971-01-01T00:00:0aZ",
// "1971-01-01T00:00:00a", accepted as per invalid_no_trailing_timezone above
"1", // truncation
"19",
"197",
"1970",
"1970-",
"1970-0",
"1970-01",
"1970-01-",
"1970-01-0",
// "1970-01-01", complete date
"1970-01-01T",
"1970-01-01T0",
"1970-01-01T00",
"1970-01-01T00:",
"1970-01-01T00:0",
"1970-01-01T00:00",
"1970-01-01T00:00:",
"1970-01-01T00:00:0",
// "1970-01-01T00:00:00", // accepted as invalid timezone above
"1899-01-01T00:00:00Z", // year too small
"1971-00-01T00:00:00Z", // month too small
"1971-20-01T00:00:00Z", // month too big
"1971-13-01T00:00:00Z",
"1971-01-00T00:00:00Z", // day too small
"1971-01-32T00:00:00Z", // day too big
"1971-02-30T00:00:00Z", // day too big for feb
"1971-02-30T00:00:00Z", // day too big for feb (non-leap year)
"1971-03-32T00:00:00Z", // other months
"1971-04-31T00:00:00Z",
"1971-05-32T00:00:00Z",
"1971-06-31T00:00:00Z",
"1971-07-32T00:00:00Z",
"1971-08-32T00:00:00Z",
"1971-09-31T00:00:00Z",
"1971-10-32T00:00:00Z",
"1971-11-31T00:00:00Z",
"1971-12-32T00:00:00Z",
"1971-01-01T70:00:00Z", // hour too big
"1971-01-01T24:00:00Z",
"1971-01-01T00:60:00Z", // minute too big
"1971-01-01T00:00:70Z", // second too big
"1971-01-01T00:00:61Z",
"1899-01-01T00:00:00Z", // underflow
"1900-01-01T00:00:00+00:01", // time zone underflow
// "1970-01-01T00:00:00.Z", // accepted as invalid timezone above
"1970-01-01T00:00:00+24:00", // bad tzoffsets
"1970-01-01T00:00:00-30:00",
"1970-01-01T00:00:00+21:60",
"1970-01-01T00:00:00-24:00",
"1970-01-01T00:00:00-21:60",
"1971-01-00", // zero month day
};
for (auto const& str : badStrings)
{
auto const dt = DateTime::FromString(str, DateTime::DateFormat::Iso8601);
EXPECT_EQ(dt.ToInterval(), 0);
}
}