DateTime: Allow dates from 1601 to 1900 (#799)

Closes #769.
This commit is contained in:
Anton Kolesnyk 2020-10-16 19:05:05 -07:00 committed by GitHub
parent d79d95e5b7
commit a1385518a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 51 additions and 52 deletions

View File

@ -56,10 +56,8 @@ constexpr int SecondsInDay = 24 * SecondsInHour;
constexpr int DaysInYear = 365;
constexpr ComputeYearResult ComputeYear(int64_t secondsSince1900)
constexpr ComputeYearResult ComputeYear(int64_t secondsSince1601)
{
constexpr int64_t SecondsFrom1601To1900 = 9435484800LL;
constexpr int DaysIn4Years = DaysInYear * 4 + 1;
constexpr int DaysIn100Years = DaysIn4Years * 25 - 1;
@ -71,8 +69,7 @@ constexpr ComputeYearResult ComputeYear(int64_t secondsSince1900)
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
int64_t secondsLeft = secondsSince1601;
int year400 = static_cast<int>(secondsLeft / SecondsIn400Years);
secondsLeft -= year400 * SecondsIn400Years;
@ -86,8 +83,7 @@ constexpr ComputeYearResult ComputeYear(int64_t secondsSince1900)
int year1 = secondsInt / SecondsInYear;
secondsInt -= year1 * SecondsInYear;
// shift back to 1900 base from 1601:
return {year400 * 400 + year100 * 100 + year4 * 4 + year1 - 299, secondsInt};
return {year400 * 400 + year100 * 100 + year4 * 4 + year1, secondsInt};
}
constexpr bool IsLeapYear(int year)
@ -129,22 +125,21 @@ constexpr unsigned short const CumulativeDaysToMonthLeap[12] = {
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)
if (m_interval > 2650467743999999999LL)
{
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
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);
auto const yearData = ComputeYear(secondsSince1900);
int const year = yearData.Year;
auto const yearData = ComputeYear(secondsSince1601);
int const year = yearData.Year + 1601;
int const yearDay = yearData.SecondsLeftThisYear / SecondsInDay;
int leftover = yearData.SecondsLeftThisYear % SecondsInDay;
@ -163,7 +158,7 @@ std::string DateTime::ToString(DateFormat format) const
}
auto const monthDay = yearDay - monthTable[month] + 1;
auto const weekday = static_cast<int>((secondsSince1900 / SecondsInDay + 1) % 7);
auto const weekday = static_cast<int>((secondsSince1601 / SecondsInDay + 1) % 7);
char outBuffer[38]{}; // Thu, 01 Jan 1970 00:00:00 GMT\0
// 1970-01-01T00:00:00.1234567Z\0
@ -185,7 +180,7 @@ std::string DateTime::ToString(DateFormat format) const
dayNames + 4ULL * static_cast<uint64_t>(weekday),
monthDay,
monthNames + 4ULL * static_cast<uint64_t>(month),
year + 1900,
year,
hour,
minute,
leftover);
@ -206,7 +201,7 @@ std::string DateTime::ToString(DateFormat format) const
20,
#endif
"%04d-%02d-%02dT%02d:%02d:%02d",
year + 1900,
year,
month + 1,
monthDay,
hour,
@ -305,9 +300,9 @@ constexpr int GetYearDay(int month, int monthDay, int year)
return CumulativeDaysToMonth[month] + monthDay + (IsLeapYear(year) && month > 1) - 1;
}
constexpr int CountLeapYears(int yearsSince1900)
constexpr int CountLeapYears(int yearsSince1601)
{
int tmpYears = yearsSince1900 + 299; // shift into 1601, the first 400 year cycle including 1900
int tmpYears = yearsSince1601;
int const year400 = tmpYears / 400;
tmpYears -= year400 * 400;
@ -320,9 +315,6 @@ constexpr int CountLeapYears(int yearsSince1900)
result += tmpYears / 4;
// subtract off leap years from 1601
result -= 72;
return result;
}
@ -405,7 +397,7 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format)
{
DateTime result = {};
int64_t secondsSince1900 = 0;
int64_t secondsSince1601 = 0;
uint64_t fracSec = 0;
auto str = dateString.c_str();
@ -472,7 +464,7 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format)
}
int year = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0');
if (year < 1900)
if (year < 1601)
{
return result;
}
@ -527,19 +519,19 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format)
return result;
}
year -= 1900;
int const daysSince1900 = year * DaysInYear + CountLeapYears(year) + yearDay;
year -= 1601;
int const daysSince1601 = year * DaysInYear + CountLeapYears(year) + yearDay;
if (parsedWeekday != 7)
{
int const actualWeekday = (daysSince1900 + 1) % 7;
int const actualWeekday = (daysSince1601 + 1) % 7;
if (parsedWeekday != actualWeekday)
{
return result;
}
}
secondsSince1900 = static_cast<IntervalType>(daysSince1900) * SecondsInDay
secondsSince1601 = static_cast<IntervalType>(daysSince1601) * SecondsInDay
+ static_cast<IntervalType>(hour) * SecondsInHour
+ static_cast<IntervalType>(minute) * SecondsInMinute + sec;
@ -583,10 +575,10 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format)
return result;
}
secondsSince1900
= AdjustTimezone(secondsSince1900, static_cast<unsigned char>(tzCh), tzHours, tzMinutes);
secondsSince1601
= AdjustTimezone(secondsSince1601, static_cast<unsigned char>(tzCh), tzHours, tzMinutes);
if (secondsSince1900 < 0)
if (secondsSince1601 < 0)
{
return result;
}
@ -601,7 +593,7 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format)
}
int year = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0');
if (year < 1900)
if (year < 1601)
{
return result;
}
@ -647,17 +639,14 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format)
int const yearDay = GetYearDay(month, monthDay, year);
str += 2;
year -= 1900;
int daysSince1900 = year * DaysInYear + CountLeapYears(year) + yearDay;
year -= 1601;
int daysSince1601 = 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);
secondsSince1601 = static_cast<int64_t>(daysSince1601) * SecondsInDay;
result.m_interval = static_cast<IntervalType>(secondsSince1601 * TicksPerSecond + fracSec);
return result;
}
@ -748,7 +737,7 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format)
}
}
secondsSince1900 = static_cast<int64_t>(daysSince1900) * SecondsInDay
secondsSince1601 = static_cast<int64_t>(daysSince1601) * SecondsInDay
+ static_cast<int64_t>(hour) * SecondsInHour
+ static_cast<int64_t>(minute) * SecondsInMinute + sec;
@ -765,12 +754,12 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format)
return result;
}
secondsSince1900 = AdjustTimezone(
secondsSince1900,
secondsSince1601 = AdjustTimezone(
secondsSince1601,
offsetDirection,
StringToDoubleDigitInt(str + 1),
StringToDoubleDigitInt(str + 4));
if (secondsSince1900 < 0)
if (secondsSince1601 < 0)
{
return result;
}
@ -785,8 +774,6 @@ DateTime DateTime::FromString(std::string const& dateString, DateFormat format)
throw DateTimeException("Unrecognized date format.");
}
result.m_interval = static_cast<IntervalType>(
secondsSince1900 * TicksPerSecond + fracSec + TicksFromWindowsEpochTo1900);
result.m_interval = static_cast<IntervalType>(secondsSince1601 * TicksPerSecond + fracSec);
return result;
}

View File

@ -97,9 +97,9 @@ TEST(DateTime, ParseTimeRoundrip6)
TestDateTimeRoundtrip("2013-11-19T14:30:59.5Z");
}
TEST(DateTime, ParseTimeRoundripYear1900) { TestDateTimeRoundtrip("1900-01-01T00:00:00Z"); }
TEST(DateTime, ParseTimeRoundripYear1601) { TestDateTimeRoundtrip("1601-01-01T00:00:00Z"); }
TEST(DateTime, ParseTimeRoundripYear9999) { TestDateTimeRoundtrip("9999-12-31T23:59:59Z"); }
TEST(DateTime, ParseTimeRoundripYear9999) { TestDateTimeRoundtrip("9999-12-31T23:59:59.9999999Z"); }
TEST(DateTime, EmittingTimeCorrectDay)
{
@ -252,7 +252,7 @@ TEST(DateTime, ParseTimeRfc1123InvalidCases)
"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 Jan 1600 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
@ -273,7 +273,6 @@ TEST(DateTime, ParseTimeRfc1123InvalidCases)
"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",
@ -417,7 +416,7 @@ TEST(DateTime, ParseTimeInvalid2)
"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
"1600-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",
@ -440,8 +439,7 @@ TEST(DateTime, ParseTimeInvalid2)
"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
"1601-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",
@ -457,3 +455,17 @@ TEST(DateTime, ParseTimeInvalid2)
EXPECT_EQ(dt.ToInterval(), 0);
}
}
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());
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());
}