Skip to content

Commit a3da3e9

Browse files
committed
Added the ability to parse years back to 1601 to support the NT epoch.
1 parent faa72d6 commit a3da3e9

File tree

3 files changed

+104
-57
lines changed

3 files changed

+104
-57
lines changed

Release/include/cpprest/asyncrt_utils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,8 @@ class datetime
628628
/// </summary>
629629
interval_type to_interval() const { return m_interval; }
630630

631+
static datetime from_interval(interval_type interval) { return datetime(interval); }
632+
631633
datetime operator-(interval_type value) const { return datetime(m_interval - value); }
632634

633635
datetime operator+(interval_type value) const { return datetime(m_interval + value); }

Release/src/utilities/asyncrt_utils.cpp

Lines changed: 56 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -620,14 +620,12 @@ utf16string __cdecl conversions::to_utf16string(const std::string& value) { retu
620620

621621
static const int64_t NtToUnixOffsetSeconds = 11644473600; // diff between windows and unix epochs (seconds)
622622

623-
static bool year_is_leap_year_1601(int year)
623+
static bool year_is_leap_year_1601(int yearsSince1601)
624624
{
625-
int decimal_year = year + 1601;
626-
return (decimal_year % 4 == 0 && (decimal_year % 100 != 0 || decimal_year % 400 == 0));
625+
int decimalYear = yearsSince1601 + 1601;
626+
return (decimalYear % 4 == 0 && (decimalYear % 100 != 0 || decimalYear % 400 == 0));
627627
}
628628

629-
static bool year_is_leap_year(int year) { return year_is_leap_year_1601(year + 299); }
630-
631629
static const int SecondsInMinute = 60;
632630
static const int SecondsInHour = SecondsInMinute * 60;
633631
static const int SecondsInDay = SecondsInHour * 24;
@@ -641,9 +639,6 @@ static const int SecondsInYear = SecondsInDay * DaysInYear;
641639
static const int SecondsIn4Years = SecondsInDay * DaysIn4Years;
642640
static const int64_t SecondsIn100Years = static_cast<int64_t>(SecondsInDay) * DaysIn100Years;
643641
static const int64_t SecondsIn400Years = static_cast<int64_t>(SecondsInDay) * DaysIn400Years;
644-
static const int64_t SecondsFrom1900To2001 = INT64_C(3187296000);
645-
646-
static const int64_t NtTo1900OffsetInterval = INT64_C(0x014F373BFDE04000);
647642

648643
static int count_leap_years_1601(int yearsSince1601)
649644
{
@@ -660,12 +655,6 @@ static int count_leap_years_1601(int yearsSince1601)
660655
return result;
661656
}
662657

663-
static int count_leap_years(const int yearsSince1900)
664-
{
665-
// shift into 1601, the first 400 year cycle including 1900, then subtract leap years from 1601->1900
666-
return count_leap_years_1601(yearsSince1900 + 299) - 72;
667-
}
668-
669658
// The following table assumes no leap year; leap year is added separately
670659
static const unsigned short cumulative_days_to_month[12] = {
671660
0, // Jan
@@ -749,23 +738,37 @@ static compute_year_result compute_year_1601(int64_t secondsSince1601)
749738
return {year400 * 400 + year100 * 100 + year4 * 4 + year1, secondsInt};
750739
}
751740

752-
static const int64_t secondsFrom1601To1900 = INT64_C(9435484800);
753-
static compute_year_result compute_year(int64_t secondsSince1900)
754-
{
755-
// shift to start of this 400 year cycle
756-
auto partialResult = compute_year_1601(secondsSince1900 + secondsFrom1601To1900);
757-
// shift back to 1900
758-
return {partialResult.year - 299, partialResult.secondsLeftThisYear};
759-
}
741+
// The constant below was calculated by running the following test program on a Windows machine:
742+
// #include <windows.h>
743+
// #include <stdio.h>
744+
745+
// int main() {
746+
// SYSTEMTIME st;
747+
// st.wYear = 9999;
748+
// st.wMonth = 12;
749+
// st.wDayOfWeek = 5;
750+
// st.wDay = 31;
751+
// st.wHour = 23;
752+
// st.wMinute = 59;
753+
// st.wSecond = 59;
754+
// st.wMilliseconds = 999;
755+
756+
// unsigned long long ft;
757+
// if (SystemTimeToFileTime(&st, reinterpret_cast<FILETIME*>(&ft))) {
758+
// printf("0x%016llX\n", ft);
759+
// } else {
760+
// puts("failed!");
761+
// }
762+
// }
760763

761764
utility::string_t datetime::to_string(date_format format) const
762765
{
763-
if (m_interval > INT64_C(2650467743990000000))
766+
const int64_t interval = static_cast<int64_t>(m_interval);
767+
if (interval > INT64_C(0x24C85A5ED1C018F0))
764768
{
765769
throw std::out_of_range("The requested year exceeds the year 9999.");
766770
}
767771

768-
const int64_t interval = static_cast<int64_t>(m_interval);
769772
const int64_t secondsSince1601 = interval / _secondTicks; // convert to seconds
770773
const int fracSec = static_cast<int>(interval % _secondTicks);
771774

@@ -899,12 +902,12 @@ static const unsigned char max_days_in_month[12] = {
899902
31 // Dec
900903
};
901904

902-
static bool validate_day_month(int day, int month, int year)
905+
static bool validate_day_month_1601(int day, int month, int year)
903906
{
904907
int maxDaysThisMonth;
905908
if (month == 1)
906909
{ // Feb needs leap year testing
907-
maxDaysThisMonth = 28 + year_is_leap_year(year);
910+
maxDaysThisMonth = 28 + year_is_leap_year_1601(year);
908911
}
909912
else
910913
{
@@ -914,9 +917,9 @@ static bool validate_day_month(int day, int month, int year)
914917
return day >= 1 && day <= maxDaysThisMonth;
915918
}
916919

917-
static int get_year_day(int month, int monthDay, int year)
920+
static int get_year_day_1601(int month, int monthDay, int year)
918921
{
919-
return cumulative_days_to_month[month] + monthDay + (year_is_leap_year(year) && month > 1) - 1;
922+
return cumulative_days_to_month[month] + monthDay + (year_is_leap_year_1601(year) && month > 1) - 1;
920923
}
921924

922925
template<class CharT>
@@ -1009,7 +1012,7 @@ datetime __cdecl datetime::from_string(const utility::string_t& dateString, date
10091012
datetime __cdecl datetime::from_string_maximum_error(const utility::string_t& dateString, date_format format)
10101013
{
10111014
datetime result = datetime::maximum();
1012-
int64_t secondsSince1900;
1015+
int64_t secondsSince1601;
10131016
uint64_t fracSec = 0;
10141017
auto str = dateString.c_str();
10151018
if (format == RFC_1123)
@@ -1076,19 +1079,21 @@ datetime __cdecl datetime::from_string_maximum_error(const utility::string_t& da
10761079

10771080
int year = (str[0] - _XPLATSTR('0')) * 1000 + (str[1] - _XPLATSTR('0')) * 100 + (str[2] - _XPLATSTR('0')) * 10 +
10781081
(str[3] - _XPLATSTR('0'));
1079-
if (year < 1900)
1082+
if (year < 1601)
10801083
{
10811084
return result;
10821085
}
10831086

1087+
year -= 1601;
1088+
10841089
// days in month validity check
1085-
if (!validate_day_month(monthDay, month, year))
1090+
if (!validate_day_month_1601(monthDay, month, year))
10861091
{
10871092
return result;
10881093
}
10891094

10901095
str += 5; // parsed year
1091-
const int yearDay = get_year_day(month, monthDay, year);
1096+
const int yearDay = get_year_day_1601(month, monthDay, year);
10921097

10931098
if (!ascii_isdigit2(str[0]) || !ascii_isdigit(str[1]) || str[2] != _XPLATSTR(':') || !ascii_isdigit5(str[3]) ||
10941099
!ascii_isdigit(str[4]))
@@ -1132,21 +1137,20 @@ datetime __cdecl datetime::from_string_maximum_error(const utility::string_t& da
11321137
return result;
11331138
}
11341139

1135-
year -= 1900;
1136-
int daysSince1900 = year * DaysInYear + count_leap_years(year) + yearDay;
1140+
int daysSince1601 = year * DaysInYear + count_leap_years_1601(year) + yearDay;
11371141

11381142
if (parsedWeekday != 7)
11391143
{
1140-
const int actualWeekday = (daysSince1900 + 1) % 7;
1144+
const int actualWeekday = (daysSince1601 + 1) % 7;
11411145

11421146
if (parsedWeekday != actualWeekday)
11431147
{
11441148
return result;
11451149
}
11461150
}
11471151

1148-
secondsSince1900 =
1149-
static_cast<int64_t>(daysSince1900) * SecondsInDay + hour * SecondsInHour + minute * SecondsInMinute + sec;
1152+
secondsSince1601 =
1153+
static_cast<int64_t>(daysSince1601) * SecondsInDay + hour * SecondsInHour + minute * SecondsInMinute + sec;
11501154

11511155
if (!string_starts_with(str, "GMT") && !string_starts_with(str, "UT"))
11521156
{
@@ -1186,8 +1190,8 @@ datetime __cdecl datetime::from_string_maximum_error(const utility::string_t& da
11861190
return result;
11871191
}
11881192

1189-
secondsSince1900 = timezone_adjust(secondsSince1900, static_cast<unsigned char>(tzCh), tzHours, tzMinutes);
1190-
if (secondsSince1900 < 0)
1193+
secondsSince1601 = timezone_adjust(secondsSince1601, static_cast<unsigned char>(tzCh), tzHours, tzMinutes);
1194+
if (secondsSince1601 < 0)
11911195
{
11921196
return result;
11931197
}
@@ -1203,11 +1207,13 @@ datetime __cdecl datetime::from_string_maximum_error(const utility::string_t& da
12031207

12041208
int year = (str[0] - _XPLATSTR('0')) * 1000 + (str[1] - _XPLATSTR('0')) * 100 + (str[2] - _XPLATSTR('0')) * 10 +
12051209
(str[3] - _XPLATSTR('0'));
1206-
if (year < 1900)
1210+
if (year < 1601)
12071211
{
12081212
return result;
12091213
}
12101214

1215+
year -= 1601;
1216+
12111217
str += 4;
12121218
if (*str == _XPLATSTR('-'))
12131219
{
@@ -1241,24 +1247,22 @@ datetime __cdecl datetime::from_string_maximum_error(const utility::string_t& da
12411247
}
12421248

12431249
int monthDay = atoi2(str);
1244-
if (!validate_day_month(monthDay, month, year))
1250+
if (!validate_day_month_1601(monthDay, month, year))
12451251
{
12461252
return result;
12471253
}
12481254

1249-
const int yearDay = get_year_day(month, monthDay, year);
1255+
const int yearDay = get_year_day_1601(month, monthDay, year);
12501256

12511257
str += 2;
1252-
year -= 1900;
1253-
int daysSince1900 = year * DaysInYear + count_leap_years(year) + yearDay;
1258+
int daysSince1601 = year * DaysInYear + count_leap_years_1601(year) + yearDay;
12541259

12551260
if (str[0] != _XPLATSTR('T') && str[0] != _XPLATSTR('t'))
12561261
{
12571262
// No time
1258-
secondsSince1900 = static_cast<int64_t>(daysSince1900) * SecondsInDay;
1263+
secondsSince1601 = static_cast<int64_t>(daysSince1601) * SecondsInDay;
12591264

1260-
result.m_interval =
1261-
static_cast<interval_type>(secondsSince1900 * _secondTicks + fracSec + NtTo1900OffsetInterval);
1265+
result.m_interval = static_cast<interval_type>(secondsSince1601 * _secondTicks + fracSec);
12621266
return result;
12631267
}
12641268

@@ -1347,8 +1351,8 @@ datetime __cdecl datetime::from_string_maximum_error(const utility::string_t& da
13471351
}
13481352
}
13491353

1350-
secondsSince1900 =
1351-
static_cast<int64_t>(daysSince1900) * SecondsInDay + hour * SecondsInHour + minute * SecondsInMinute + sec;
1354+
secondsSince1601 =
1355+
static_cast<int64_t>(daysSince1601) * SecondsInDay + hour * SecondsInHour + minute * SecondsInMinute + sec;
13521356

13531357
if (str[0] == _XPLATSTR('Z') || str[0] == _XPLATSTR('z'))
13541358
{
@@ -1363,8 +1367,8 @@ datetime __cdecl datetime::from_string_maximum_error(const utility::string_t& da
13631367
return result;
13641368
}
13651369

1366-
secondsSince1900 = timezone_adjust(secondsSince1900, offsetDirection, atoi2(str + 1), atoi2(str + 4));
1367-
if (secondsSince1900 < 0)
1370+
secondsSince1601 = timezone_adjust(secondsSince1601, offsetDirection, atoi2(str + 1), atoi2(str + 4));
1371+
if (secondsSince1601 < 0)
13681372
{
13691373
return result;
13701374
}
@@ -1379,7 +1383,7 @@ datetime __cdecl datetime::from_string_maximum_error(const utility::string_t& da
13791383
throw std::invalid_argument("unrecognized date format");
13801384
}
13811385

1382-
result.m_interval = static_cast<interval_type>(secondsSince1900 * _secondTicks + fracSec + NtTo1900OffsetInterval);
1386+
result.m_interval = static_cast<interval_type>(secondsSince1601 * _secondTicks + fracSec);
13831387
return result;
13841388
}
13851389

Release/tests/functional/utils/datetime.cpp

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,14 @@ SUITE(datetime)
138138

139139
TEST(parsing_time_roundtrip_year_2021) { TestDateTimeRoundtrip(_XPLATSTR("2021-01-01T20:59:59Z")); }
140140

141+
TEST(parsing_time_roundtrip_year_1601) { TestDateTimeRoundtrip(_XPLATSTR("1601-01-01T00:00:00Z")); }
142+
143+
TEST(parsing_time_roundtrip_year_1602) { TestDateTimeRoundtrip(_XPLATSTR("1602-01-01T00:00:00Z")); }
144+
145+
TEST(parsing_time_roundtrip_year_1603) { TestDateTimeRoundtrip(_XPLATSTR("1603-01-01T00:00:00Z")); }
146+
147+
TEST(parsing_time_roundtrip_year_1604) { TestDateTimeRoundtrip(_XPLATSTR("1604-01-01T00:00:00Z")); }
148+
141149
TEST(emitting_time_correct_day)
142150
{
143151
const auto test = utility::datetime() + UINT64_C(132004507640000000); // 2019-04-22T23:52:44 is a Monday
@@ -287,7 +295,7 @@ SUITE(datetime)
287295
_XPLATSTR("Thu, 01 Jan 1970 00:00:00 G"),
288296
_XPLATSTR("Thu, 01 Jan 1970 00:00:00 GM"),
289297
_XPLATSTR("Fri, 01 Jan 1970 00:00:00 GMT"), // wrong day
290-
_XPLATSTR("01 Jan 1899 00:00:00 GMT"), // year too small
298+
_XPLATSTR("01 Jan 1600 00:00:00 GMT"), // year too small
291299
_XPLATSTR("01 Xxx 1971 00:00:00 GMT"), // month bad
292300
_XPLATSTR("00 Jan 1971 00:00:00 GMT"), // day too small
293301
_XPLATSTR("32 Jan 1971 00:00:00 GMT"), // day too big
@@ -308,7 +316,7 @@ SUITE(datetime)
308316
_XPLATSTR("01 Jan 1971 00:60:00 GMT"), // minute too big
309317
_XPLATSTR("01 Jan 1971 00:00:70 GMT"), // second too big
310318
_XPLATSTR("01 Jan 1971 00:00:61 GMT"),
311-
_XPLATSTR("01 Jan 1899 00:00:00 GMT"), // underflow
319+
_XPLATSTR("01 Jan 1600 00:00:00 GMT"), // underflow
312320
_XPLATSTR("01 Jan 1969 00:00:00 CEST"), // bad tz
313321
_XPLATSTR("14 Jan 2019 23:16:21 G0100"), // bad tzoffsets
314322
_XPLATSTR("01 Jan 1970 00:00:00 +2400"),
@@ -454,7 +462,7 @@ SUITE(datetime)
454462
_XPLATSTR("1970-01-01T00:00:"),
455463
_XPLATSTR("1970-01-01T00:00:0"),
456464
// _XPLATSTR("1970-01-01T00:00:00"), // accepted as invalid timezone above
457-
_XPLATSTR("1899-01-01T00:00:00Z"), // year too small
465+
_XPLATSTR("1600-01-01T00:00:00Z"), // year too small
458466
_XPLATSTR("1971-00-01T00:00:00Z"), // month too small
459467
_XPLATSTR("1971-20-01T00:00:00Z"), // month too big
460468
_XPLATSTR("1971-13-01T00:00:00Z"),
@@ -477,8 +485,8 @@ SUITE(datetime)
477485
_XPLATSTR("1971-01-01T00:60:00Z"), // minute too big
478486
_XPLATSTR("1971-01-01T00:00:70Z"), // second too big
479487
_XPLATSTR("1971-01-01T00:00:61Z"),
480-
_XPLATSTR("1899-01-01T00:00:00Z"), // underflow
481-
_XPLATSTR("1900-01-01T00:00:00+00:01"), // time zone underflow
488+
_XPLATSTR("1600-01-01T00:00:00Z"), // underflow
489+
_XPLATSTR("1601-01-01T00:00:00+00:01"), // time zone underflow
482490
// _XPLATSTR("1970-01-01T00:00:00.Z"), // accepted as invalid timezone above
483491
_XPLATSTR("1970-01-01T00:00:00+24:00"), // bad tzoffsets
484492
_XPLATSTR("1970-01-01T00:00:00-30:00"),
@@ -508,6 +516,39 @@ SUITE(datetime)
508516
auto result = utility::datetime {}.to_string(utility::datetime::ISO_8601);
509517
VERIFY_ARE_EQUAL(_XPLATSTR("1601-01-01T00:00:00Z"), result);
510518
}
519+
520+
TEST(can_emit_year_9999_rfc_1123)
521+
{
522+
auto result =
523+
utility::datetime::from_interval(INT64_C(0x24C85A5ED1C018F0)).to_string(utility::datetime::RFC_1123);
524+
VERIFY_ARE_EQUAL(_XPLATSTR("Fri, 31 Dec 9999 23:59:59 GMT"), result);
525+
}
526+
527+
TEST(can_emit_year_9999_iso_8601)
528+
{
529+
auto result =
530+
utility::datetime::from_interval(INT64_C(0x24C85A5ED1C018F0)).to_string(utility::datetime::ISO_8601);
531+
VERIFY_ARE_EQUAL(_XPLATSTR("9999-12-31T23:59:59.999Z"), result);
532+
}
533+
534+
TEST(can_parse_nt_epoch_zero_rfc_1123)
535+
{
536+
auto dt =
537+
utility::datetime::from_string(_XPLATSTR("Mon, 01 Jan 1601 00:00:00 GMT"), utility::datetime::RFC_1123);
538+
VERIFY_ARE_EQUAL(0U, dt.to_interval());
539+
auto dt_me = utility::datetime::from_string_maximum_error(_XPLATSTR("Mon, 01 Jan 1601 00:00:00 GMT"),
540+
utility::datetime::RFC_1123);
541+
VERIFY_ARE_EQUAL(0U, dt_me.to_interval());
542+
}
543+
544+
TEST(can_parse_nt_epoch_zero_iso_8601)
545+
{
546+
auto dt = utility::datetime::from_string(_XPLATSTR("1601-01-01T00:00:00Z"), utility::datetime::ISO_8601);
547+
VERIFY_ARE_EQUAL(0U, dt.to_interval());
548+
auto dt_me = utility::datetime::from_string_maximum_error(_XPLATSTR("1601-01-01T00:00:00Z"),
549+
utility::datetime::ISO_8601);
550+
VERIFY_ARE_EQUAL(0U, dt_me.to_interval());
551+
}
511552
} // SUITE(datetime)
512553

513554
} // namespace utils_tests

0 commit comments

Comments
 (0)