# # # patch "dates.cc" # from [a8bef57375a94c4180618efc70bda5ecd9b1d226] # to [75292f5f0c9d407d5356066f8f2592586b6fbfa9] # # patch "dates.hh" # from [2b1dcada20482787b3637aa113f9a20aa96c8016] # to [69bde209cb015527e9716f9b52a3b56f25b6bd95] # # patch "options_list.hh" # from [f8e7d28b9c65b05918602919dfe526b7ef549fbe] # to [d8404aaabc072997bc8226f83cae955ee5851dcb] # ============================================================ --- dates.cc a8bef57375a94c4180618efc70bda5ecd9b1d226 +++ dates.cc 75292f5f0c9d407d5356066f8f2592586b6fbfa9 @@ -9,6 +9,7 @@ #include "base.hh" #include "dates.hh" +#include "sanity.hh" #include #include @@ -16,83 +17,37 @@ using std::string; using std::string; // Writing a 64-bit constant is tricky. We cannot use the macros that -// provides in C99 (UINT64_C, or even UINT64_MAX) because those -// macros are not in C++'s version of . std::numeric_limits +// provides in C99 (INT64_C, or even INT64_MAX) because those +// macros are not in C++'s version of . std::numeric_limits // cannot be used directly, so we have to resort to #ifdef chains on the old // skool C limits macros. BOOST_STATIC_ASSERT is defined in a way that -// doesn't let us use std::numeric_limits::max(), so we have to +// doesn't let us use std::numeric_limits::max(), so we have to // postpone checking it until runtime (our_gmtime), bleah. However, the check // will be optimized out, and the unit tests exercise it. -#if defined ULONG_MAX && ULONG_MAX > UINT_MAX - #define PROBABLE_U64_MAX ULONG_MAX - #define u64_C(x) x##UL -#elif defined ULLONG_MAX && ULLONG_MAX > UINT_MAX - #define PROBABLE_U64_MAX ULLONG_MAX - #define u64_C(x) x##ULL -#elif defined ULONG_LONG_MAX && ULONG_LONG_MAX > UINT_MAX - #define PROBABLE_U64_MAX ULONG_LONG_MAX - #define u64_C(x) x##ULL +#if defined LONG_MAX && LONG_MAX > UINT_MAX + #define PROBABLE_S64_MAX LONG_MAX + #define s64_C(x) x##L +#elif defined LLONG_MAX && LLONG_MAX > UINT_MAX + #define PROBABLE_S64_MAX LLONG_MAX + #define s64_C(x) x##LL +#elif defined LONG_LONG_MAX && LONG_LONG_MAX > UINT_MAX + #define PROBABLE_S64_MAX LONG_LONG_MAX + #define s64_C(x) x##LL #else - #error "How do I write a constant of type u64?" + #error "How do I write a constant of type s64?" #endif -// Forward declarations required so as to not have to shuffle around code. -static s64 our_mktime(broken_down_time const & tm); -static void our_gmtime(const s64 d, broken_down_time & tm); +// Our own "struct tm"-like struct to represent broken-down times +struct broken_down_time { + int millisec; /* milliseconds (0 - 999) */ + int sec; /* seconds (0 - 59) */ + int min; /* minutes (0 - 59) */ + int hour; /* hours (0 - 23) */ + int day; /* day of the month (1 - 31) */ + int month; /* month (1 - 12) */ + int year; /* years (anno Domini, i.e. 1999) */ +}; -date_t::date_t(u64 d) - : d(d) -{ - // When initialized from a millisecods since Unix epoch value, we require - // that to be in a valid range. Use the constructor without any argument - // to generate an invalid date. - I(valid()); -} - -date_t::date_t(int year, int month, int day, - int hour, int min, int sec, int millisec) -{ - // general validity checks - I((year >= 1970) && (year <= 9999)); - I((month >= 1) && (month <= 12)); - I((day >= 1) && (day <= 31)); - I((hour >= 0) && (hour < 24)); - I((min >= 0) && (min < 60)); - I((sec >= 0) && (sec < 60)); - I((millisec >= 0) && (millisec < 1000)); - - broken_down_time t; - t.millisec = millisec; - t.sec = sec; - t.min = min; - t.hour = hour; - t.day = day; - t.month = month; - t.year = year; - - d = our_mktime(t); - I(valid()); -} - -bool -date_t::valid() const -{ - // year 10000 limit - return d < u64_C(253402300800000); -} - -std::ostream & -operator<< (std::ostream & o, date_t const & d) -{ - return o << d.as_iso_8601_extended(); -} - -template <> void -dump(date_t const & d, std::string & s) -{ - s = d.as_iso_8601_extended(); -} - // The Unix epoch is 1970-01-01T00:00:00 (in UTC). As we cannot safely // assume that the system's epoch is the Unix epoch, we implement the // conversion to broken-down time by hand instead of relying on gmtime(). @@ -112,15 +67,28 @@ dump(date_t const & d, std::string & s) // The last two rules are the Gregorian correction to the Julian calendar. // We make no attempt to handle leap seconds. -u64 const SEC = u64_C(1000); -u64 const MIN = 60 * SEC; -u64 const HOUR = 60 * MIN; -u64 const DAY = 24 * HOUR; -u64 const YEAR = 365 * DAY; -u64 const LEAP = 366 * DAY; -u64 const FOUR_HUNDRED_YEARS = 400 * YEAR + (100 - 4 + 1) * DAY; +s64 const INVALID = PROBABLE_S64_MAX; -unsigned char const MONTHS[] = { +// This is the date 292278994-01-01T00:00:00.000. The year 292,278,994 +// overflows a signed 64-bit millisecond counter somewhere in August, so +// we've rounded down to the last whole year that fits. +s64 const LATEST_SUPPORTED_DATE = s64_C(9223372017129600000); + +// This is the date 0001-01-01T00:00:00.000. There is no year zero in the +// Gregorian calendar, and what are you doing using monotone to version +// data from before the common era, anyway? +s64 const EARLIEST_SUPPORTED_DATE = s64_C(-62135596800000); + +// These constants are all in seconds. +u32 const SEC = 1; +u32 const MIN = 60*SEC; +u32 const HOUR = 60*MIN; +u64 const DAY = 24*HOUR; +u64 const YEAR = 365*DAY; + +inline s64 MILLISEC(s64 n) { return n * 1000; } + +unsigned char const DAYS_PER_MONTH[] = { 31, // jan 28, // feb (non-leap) 31, // mar @@ -135,135 +103,107 @@ unsigned char const MONTHS[] = { 31, // dec }; -static s64 -get_epoch_offset() -{ - static s64 epoch_offset; - static bool know_epoch_offset = false; - broken_down_time our_t; - - if (know_epoch_offset) - return epoch_offset; - - std::time_t epoch = 0; - std::tm t = *std::gmtime(&epoch); - - our_t.millisec = 0; - our_t.sec = t.tm_sec; - our_t.min = t.tm_min; - our_t.hour = t.tm_hour; - our_t.day = t.tm_mday; - our_t.month = t.tm_mon + 1; - our_t.year = t.tm_year + 1900; - - epoch_offset = our_mktime(our_t); - - know_epoch_offset = true; - return epoch_offset; -} - -date_t -date_t::now() -{ - std::time_t t = std::time(0); - date_t d; - d.d = u64(t) * 1000 + get_epoch_offset(); - return d; -} - inline bool -is_leap_year(unsigned int year) +is_leap_year(s32 year) { return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); } -inline u64 -millisecs_in_year(unsigned int year) +inline s32 +days_in_year(s32 year) { - return is_leap_year(year) ? LEAP : YEAR; -} + return is_leap_year(year) ? 366 : 365; + } -string -date_t::as_iso_8601_extended() const +inline bool +valid_ms_count(s64 d) { - broken_down_time tm; - I(valid()); - our_gmtime(d, tm); - return (FL("%04u-%02u-%02uT%02u:%02u:%02u") - % tm.year % tm.month % tm.day - % tm.hour % tm.min % tm.sec).str(); + return (d >= EARLIEST_SUPPORTED_DATE && d <= LATEST_SUPPORTED_DATE); } -u64 -date_t::millisecs_since_unix_epoch() const +static void +our_gmtime(s64 t, broken_down_time & tm) { - return d; -} + // validate our assumptions about which basic type is u64 (see above). + I(PROBABLE_S64_MAX == std::numeric_limits::max()); + I(LATEST_SUPPORTED_DATE < PROBABLE_S64_MAX); -static void -our_gmtime(const s64 d, broken_down_time & tm) -{ - // these types hint to the compiler that narrowing divides are safe - u64 yearbeg; - u16 year; - u32 month; - u32 day; - u32 msofday; - u16 hour; - u32 msofhour; - u8 min; - u16 msofmin; - u8 sec; - u16 msec; + I(valid_ms_count(t)); - // the temp variable to calculate with - u64 t = d; + // There are exactly 31556952 seconds (365d 5h 43m 12s) in the average + // Gregorian year. This will therefore approximate the correct year + // (minus 1970). + s32 year = t / MILLISEC(s64_C(31556592)); - // validate our assumptions about which basic type is u64 (see above). - I(PROBABLE_U64_MAX == std::numeric_limits::max()); + s32 ms_in_day; + if (t >= 0) + { + ms_in_day = t % MILLISEC(DAY); + t /= MILLISEC(DAY); + } + else + { + // this is '+' because t%MS(DAY) is negative + ms_in_day = MILLISEC(DAY) + (t % MILLISEC(DAY)); + t = t/MILLISEC(DAY) - 1; + } - // enforce a limit of year 9999 so that we remain within the range of a - // four digit year. - I(t < u64_C(253402300800000)); + // t is now a day count relative to the epoch, and ms_in_day is a positive + // count of milliseconds since the beginning of day 't'. - // There are 31556952 seconds (365d 5h 43m 12s) in the average Gregorian - // year. This will therefore approximate the correct year (minus 1970). - // It may be off in either direction, but by no more than one year - // (empirically tested for every year from 1970 to 2**32 - 1). - year = t / u64_C(31556592000); + tm.millisec = ms_in_day % 1000; + ms_in_day /= 1000; - // Given the above approximation, recalculate the _exact_ number of - // milliseconds to the beginning of that year. For this to work correctly + tm.sec = ms_in_day % 60; + ms_in_day /= 60; + + tm.min = ms_in_day % 60; + tm.hour = ms_in_day / 60; + + // edge case, only happens for negative input + if (tm.hour == 24) + { + tm.hour = 0; + t += 1; + } + + // t is now a day count relative to the epoch. + // Compute the _exact_ number of days from the epoch to the beginning of + // the approximate year determined above. For this to work correctly // (i.e. for the year/4, year/100, year/400 terms to increment exactly // when they ought to) it is necessary to count years from 1601 (as if the // Gregorian calendar had been in effect at that time) and then correct - // the final number of milliseconds back to the 1970 epoch. + // the final number of days back to the 1970 epoch. + s64 yearbeg; + year += 369; + yearbeg = widen(year)*365 + year/4 - year/100 + year/400; + yearbeg -= 369*365 + 369/4 - 369/100 + 369/400; - yearbeg = (widen(year)*365 + year/4 - year/100 + year/400) * DAY; - yearbeg -= (widen(369)*365 + 369/4 - 369/100 + 369/400) * DAY; - // *now* we want year to have its true value. year += 1601; - // Linear search for the range of milliseconds that really contains t. - // Due to the above approximation it's sufficient to correct only once - // in one or the other direction. - if (yearbeg > t) - yearbeg -= millisecs_in_year(--year); - else if (yearbeg + millisecs_in_year(year) <= t) - yearbeg += millisecs_in_year(year++); - I((yearbeg <= t) && (yearbeg + millisecs_in_year(year) > t)); + L(FL("guess year %d, delta %d") % year % (t - yearbeg)); + // Linear search for the actual year containing 't'. + // At most one of these loops should iterate. + while (yearbeg > t) + yearbeg -= days_in_year(--year); + while (yearbeg + days_in_year(year) <= t) + yearbeg += days_in_year(year++); + + tm.year = year; t -= yearbeg; + L(FL("year %d yday %d") % year % t); + // Now, the months digit! - month = 1; + s32 month = 1; for (;;) { - u64 this_month = MONTHS[month-1] * DAY; + s32 this_month = DAYS_PER_MONTH[month-1]; if (month == 2 && is_leap_year(year)) - this_month += DAY; + this_month += 1; if (t < this_month) break; @@ -271,61 +211,160 @@ our_gmtime(const s64 d, broken_down_time month++; I(month <= 12); } - - // the rest is straightforward. - day = t / DAY + 1; - msofday = t % DAY; - - hour = msofday / HOUR; - msofhour = msofday % HOUR; - - min = msofhour / MIN; - msofmin = msofhour % MIN; - - sec = msofmin / SEC; - msec = msofmin % SEC; - - // fill in the result - tm.millisec = msec; - tm.sec = sec; - tm.min = min; - tm.hour = hour; - tm.day = day; tm.month = month; - tm.year = year; + tm.day = t + 1; } static s64 -our_mktime(broken_down_time const & tm) +our_timegm(broken_down_time const & tm) { s64 d; - d = tm.millisec; - d += tm.sec * SEC; - d += tm.min * MIN; - d += tm.hour * HOUR; - d += (tm.day - 1) * DAY; + // range checks + I(tm.year > 0 && tm.year <= 292278994); + I(tm.month >= 1 && tm.month <= 12); + I(tm.day >= 1 && tm.day <= 31); + I(tm.hour >= 0 && tm.hour <= 23); + I(tm.min >= 0 && tm.min <= 59); + I(tm.sec >= 0 && tm.sec <= 60); + I(tm.millisec >= 0 && tm.millisec <= 999); - // add months + // years (since 1970) + d = YEAR * (tm.year - 1970); + // leap days to add (or subtract) + int add_leap_days = (tm.year - 1) / 4 - 492; + add_leap_days -= (tm.year - 1) / 100 - 19; + add_leap_days += (tm.year - 1) / 400 - 4; + d += add_leap_days * DAY; + + // months for (int m = 1; m < tm.month; ++m) { - d += MONTHS[m-1] * DAY; - if ((m == 2) && (is_leap_year(tm.year))) + d += DAYS_PER_MONTH[m-1] * DAY; + if (m == 2 && is_leap_year(tm.year)) d += DAY; } - I(tm.year >= 0); + // days within month, and so on + d += (tm.day - 1) * DAY; + d += tm.hour * HOUR; + d += tm.min * MIN; + d += tm.sec * SEC; - // add years (since 1970) - d += YEAR * (tm.year - 1970); + return MILLISEC(d) + tm.millisec; +} - // calculate leap days to add (or subtract) - int add_leap_days = (tm.year - 1) / 4 - 492; - add_leap_days -= (tm.year - 1) / 100 - 19; - add_leap_days += (tm.year - 1) / 400 - 4; +// In a few places we need to know the offset between the Unix epoch and the +// system epoch. +static s64 +get_epoch_offset() +{ + static s64 epoch_offset; + static bool know_epoch_offset = false; + broken_down_time our_t; - d += DAY * add_leap_days; + if (know_epoch_offset) + return epoch_offset; + std::time_t epoch = 0; + std::tm t = *std::gmtime(&epoch); + + our_t.millisec = 0; + our_t.sec = t.tm_sec; + our_t.min = t.tm_min; + our_t.hour = t.tm_hour; + our_t.day = t.tm_mday; + our_t.month = t.tm_mon + 1; + our_t.year = t.tm_year + 1900; + + epoch_offset = our_timegm(our_t); + + L(FL("time epoch offset is %d\n") % epoch_offset); + + know_epoch_offset = true; + return epoch_offset; +} + + +// +// date_t methods +// +bool +date_t::valid() const +{ + return valid_ms_count(d); +} + +// initialize to an invalid date +date_t::date_t() + : d(INVALID) +{ + I(!valid()); +} + +date_t::date_t(s64 d) + : d(d) +{ + // When initialized from a millisecods since Unix epoch value, we require + // it to be in a valid range. Use the constructor without any argument to + // generate an invalid date. + I(valid()); +} + +date_t::date_t(int year, int month, int day, + int hour, int min, int sec, int millisec) +{ + broken_down_time t; + t.millisec = millisec; + t.sec = sec; + t.min = min; + t.hour = hour; + t.day = day; + t.month = month; + t.year = year; + + d = our_timegm(t); + I(valid()); +} + +date_t +date_t::now() +{ + std::time_t t = std::time(0); + s64 tu = t * 1000 + get_epoch_offset(); + E(valid_ms_count(tu), + F("current date '%s' is outside usable range\n" + "(your system clock may not be set correctly)") + % std::ctime(&t)); + return date_t(tu); +} + +string +date_t::as_iso_8601_extended() const +{ + broken_down_time tm; + I(valid()); + our_gmtime(d, tm); + return (FL("%04u-%02u-%02uT%02u:%02u:%02u") + % tm.year % tm.month % tm.day + % tm.hour % tm.min % tm.sec).str(); +} + +std::ostream & +operator<< (std::ostream & o, date_t const & d) +{ + return o << d.as_iso_8601_extended(); +} + +template <> void +dump(date_t const & d, std::string & s) +{ + s = d.as_iso_8601_extended(); +} + +s64 +date_t::as_millisecs_since_unix_epoch() const +{ return d; } @@ -333,92 +372,91 @@ our_mktime(broken_down_time const & tm) // gnulib has a rather nice date parser, except that it requires Bison // (not even just yacc). -date_t -date_t::from_string(string const & d) +date_t::date_t(string const & s) { try { - size_t i = d.size() - 1; // last character of the array + size_t i = s.size() - 1; // last character of the array // check the first character which is not a digit - while (d.at(i) >= '0' && d.at(i) <= '9') + while (s.at(i) >= '0' && s.at(i) <= '9') i--; // ignore fractional seconds, if present, or go back to the end of the // date string to parse the digits for seconds. - if (d.at(i) == '.') + if (s.at(i) == '.') i--; else - i = d.size() - 1; + i = s.size() - 1; // seconds u8 sec; - N(d.at(i) >= '0' && d.at(i) <= '9' - && d.at(i-1) >= '0' && d.at(i-1) <= '5', + N(s.at(i) >= '0' && s.at(i) <= '9' + && s.at(i-1) >= '0' && s.at(i-1) <= '5', F("unrecognized date (monotone only understands ISO 8601 format)")); - sec = (d.at(i-1) - '0')*10 + (d.at(i) - '0'); + sec = (s.at(i-1) - '0')*10 + (s.at(i) - '0'); i -= 2; - N(sec < 60, + N(sec <= 60, F("seconds out of range")); // optional colon - if (d.at(i) == ':') + if (s.at(i) == ':') i--; // minutes u8 min; - N(d.at(i) >= '0' && d.at(i) <= '9' - && d.at(i-1) >= '0' && d.at(i-1) <= '5', + N(s.at(i) >= '0' && s.at(i) <= '9' + && s.at(i-1) >= '0' && s.at(i-1) <= '5', F("unrecognized date (monotone only understands ISO 8601 format)")); - min = (d.at(i-1) - '0')*10 + (d.at(i) - '0'); + min = (s.at(i-1) - '0')*10 + (s.at(i) - '0'); i -= 2; N(min < 60, F("minutes out of range")); // optional colon - if (d.at(i) == ':') + if (s.at(i) == ':') i--; // hours u8 hour; - N((d.at(i-1) >= '0' && d.at(i-1) <= '1' - && d.at(i) >= '0' && d.at(i) <= '9') - || (d.at(i-1) == '2' && d.at(i) >= '0' && d.at(i) <= '3'), + N((s.at(i-1) >= '0' && s.at(i-1) <= '1' + && s.at(i) >= '0' && s.at(i) <= '9') + || (s.at(i-1) == '2' && s.at(i) >= '0' && s.at(i) <= '3'), F("unrecognized date (monotone only understands ISO 8601 format)")); - hour = (d.at(i-1) - '0')*10 + (d.at(i) - '0'); + hour = (s.at(i-1) - '0')*10 + (s.at(i) - '0'); i -= 2; N(hour < 24, F("hour out of range")); // We accept 'T' as well as spaces between the date and the time - N(d.at(i) == 'T' || d.at(i) == ' ', + N(s.at(i) == 'T' || s.at(i) == ' ', F("unrecognized date (monotone only understands ISO 8601 format)")); i--; // day u8 day; - N(d.at(i-1) >= '0' && d.at(i-1) <= '3' - && d.at(i) >= '0' && d.at(i) <= '9', + N(s.at(i-1) >= '0' && s.at(i-1) <= '3' + && s.at(i) >= '0' && s.at(i) <= '9', F("unrecognized date (monotone only understands ISO 8601 format)")); - day = (d.at(i-1) - '0')*10 + (d.at(i) - '0'); + day = (s.at(i-1) - '0')*10 + (s.at(i) - '0'); i -= 2; // optional dash - if (d.at(i) == '-') + if (s.at(i) == '-') i--; // month u8 month; - N(d.at(i-1) >= '0' && d.at(i-1) <= '1' - && d.at(i) >= '0' && d.at(i) <= '9', + N(s.at(i-1) >= '0' && s.at(i-1) <= '1' + && s.at(i) >= '0' && s.at(i) <= '9', F("unrecognized date (monotone only understands ISO 8601 format)")); - month = (d.at(i-1) - '0')*10 + (d.at(i) - '0'); + month = (s.at(i-1) - '0')*10 + (s.at(i) - '0'); N(month >= 1 && month <= 12, - F("month out of range in '%s'") % d); + F("month out of range in '%s'") % s); i -= 2; // optional dash - if (d.at(i) == '-') + if (s.at(i) == '-') i--; // year @@ -429,30 +467,39 @@ date_t::from_string(string const & d) // (size_t being unsigned) u32 year = 0; u32 digit = 1; - while (i < d.size()) + while (i < s.size()) { - N(d.at(i) >= '0' && d.at(i) <= '9', + N(s.at(i) >= '0' && s.at(i) <= '9', F("unrecognized date (monotone only understands ISO 8601 format)")); - year += (d.at(i) - '0')*digit; + year += (s.at(i) - '0')*digit; i--; digit *= 10; } - N(year >= 1970, - F("date too early (monotone only goes back to 1970-01-01T00:00:00)")); - N(year <= 9999, - F("date too late (monotone only goes forward to year 9999)")); + N(year >= 1, + F("date too early (monotone only goes back to 0001-01-01T00:00:00)")); + N(year <= 292278994, + F("date too late (monotone only goes forward to year 292,278,993)")); u8 mdays; if (month == 2 && is_leap_year(year)) - mdays = MONTHS[month-1] + 1; + mdays = DAYS_PER_MONTH[month-1] + 1; else - mdays = MONTHS[month-1]; + mdays = DAYS_PER_MONTH[month-1]; N(day >= 1 && day <= mdays, - F("day out of range for its month in '%s'") % d); + F("day out of range for its month in '%s'") % s); - return date_t(year, month, day, hour, min, sec); + broken_down_time t; + t.millisec = 0; + t.sec = sec; + t.min = min; + t.hour = hour; + t.day = day; + t.month = month; + t.year = year; + + d = our_timegm(t); } catch (std::out_of_range) { @@ -470,7 +517,6 @@ date_t::operator +=(s64 const other) d += other; - // make sure we are still before year 10,000 I(valid()); return *this; @@ -508,18 +554,19 @@ date_t::operator -(date_t const & other) #ifdef BUILD_UNIT_TESTS #include "unit_tests.hh" -UNIT_TEST(date, our_mktime) -{ +s64 const FOUR_HUNDRED_YEARS = 400 * YEAR + (100 - 4 + 1) * DAY; -#define OK(x) UNIT_TEST_CHECK(our_mktime(t) == (x)) +UNIT_TEST(date, our_timegm) +{ +#define OK(x) UNIT_TEST_CHECK(our_timegm(t) == MILLISEC(x)) broken_down_time t = {0, 0, 0, 0, 1, 1, 1970}; OK(0); t.year = 2000; - OK(u64_C(946684800000)); + OK(s64_C(946684800)); - // Make sure our_mktime works for years before 1970 as well. + // Make sure our_timegm works for years before 1970 as well. t.year = 1960; OK(-10 * YEAR - 3 * DAY); @@ -550,20 +597,23 @@ UNIT_TEST(date, our_mktime) t.year = 370; OK(-4 * FOUR_HUNDRED_YEARS); - t.year = 0; /* year 0 anno Domini */ - OK(-1970 * YEAR - (492 - 19 + 4) * DAY); + t.year = 1; /* year 1 anno Domini */ + OK(-1969 * YEAR - (492 - 19 + 4) * DAY); - t.year = -1; /* year 1 BC */ - UNIT_TEST_CHECK_THROW(our_mktime(t), std::logic_error); + t.year = 0; /* no such year */ + UNIT_TEST_CHECK_THROW(our_timegm(t), std::logic_error); #undef OK } UNIT_TEST(date, from_string) { -#define OK(x,y) UNIT_TEST_CHECK(date_t::from_string(x).as_iso_8601_extended() \ - == (y)) -#define NO(x) UNIT_TEST_CHECK_THROW(date_t::from_string(x), informative_failure) +#define OK(x,y) do { \ + string s_ = date_t(x).as_iso_8601_extended(); \ + L(FL("date_t: %s -> %s") % (x) % s_); \ + UNIT_TEST_CHECK(s_ == (y)); \ + } while (0) +#define NO(x) UNIT_TEST_CHECK_THROW(date_t(x), informative_failure) // canonical format OK("2007-03-01T18:41:13", "2007-03-01T18:41:13"); @@ -587,7 +637,6 @@ UNIT_TEST(date, from_string) OK("2004-02-28T23:59:59", "2004-02-28T23:59:59"); OK("2004-02-29T00:00:00", "2004-02-29T00:00:00"); - // squashed format OK("20070301T184113", "2007-03-01T18:41:13"); // space between date and time @@ -596,8 +645,11 @@ UNIT_TEST(date, from_string) OK("20070301 184113", "2007-03-01T18:41:13"); // more than four digits in the year - NO("120070301T184113"); // equals 12007-03-01T18:41:13 + OK("120070301T184113", "12007-03-01T18:41:13"); + // before the epoch + OK("1969-03-01T18:41:13", "1969-03-01T18:41:13"); + // inappropriate character at every possible position NO("x007-03-01T18:41:13"); NO("2x07-03-01T18:41:13"); @@ -638,9 +690,7 @@ UNIT_TEST(date, from_string) // two digit years are not accepted NO("07-03-01T18:41:13"); - // components out of range - NO("1969-03-01T18:41:13"); - + // components (other than year) out of range NO("2007-00-01T18:41:13"); NO("2007-13-01T18:41:13"); @@ -681,11 +731,12 @@ UNIT_TEST(date, from_unix_epoch) UNIT_TEST(date, from_unix_epoch) { -#define OK(x,y) do { \ - string s_ = date_t(u64_C(x)).as_iso_8601_extended(); \ - L(FL("date_t: %lu -> %s") % u64_C(x) % s_); \ - UNIT_TEST_CHECK(s_ == (y)); \ +#define OK_(x,y) do { \ + string s_ = date_t(x).as_iso_8601_extended(); \ + L(FL("date_t: %lu -> %s") % (x) % s_); \ + UNIT_TEST_CHECK(s_ == (y)); \ } while (0) +#define OK(x,y) OK_(s64_C(x),y) // every month boundary in 1970 OK(0, "1970-01-01T00:00:00"); @@ -796,18 +847,22 @@ UNIT_TEST(date, from_unix_epoch) OK(4131302400000, "2100-12-01T00:00:00"); OK(4133980799000, "2100-12-31T23:59:59"); - // year 9999 limit - OK(253402300799000, "9999-12-31T23:59:59"); - UNIT_TEST_CHECK_THROW(date_t(u64_C(253402300800000)), std::logic_error); - + // limit of valid dates + OK_(LATEST_SUPPORTED_DATE, "292278994-01-01T00:00:00"); + UNIT_TEST_CHECK_THROW(date_t(LATEST_SUPPORTED_DATE+1), + std::logic_error); + OK_(EARLIEST_SUPPORTED_DATE, "0001-01-01T00:00:00"); + UNIT_TEST_CHECK_THROW(date_t(EARLIEST_SUPPORTED_DATE-1), + std::logic_error); + #undef OK } UNIT_TEST(date, comparisons) { - date_t may = date_t::from_string("2000-05-01T00:00:00"), - jun = date_t::from_string("2000-06-01T00:00:00"), - jul = date_t::from_string("2000-07-01T00:00:00"), + date_t may = date_t("2000-05-01T00:00:00"), + jun = date_t("2000-06-01T00:00:00"), + jul = date_t("2000-07-01T00:00:00"), v; // testing all comparisons operators @@ -817,40 +872,42 @@ UNIT_TEST(date, comparisons) UNIT_TEST_CHECK(jul > may); - UNIT_TEST_CHECK(may == date_t::from_string("2000-05-01T00:00:00")); - UNIT_TEST_CHECK(may != date_t::from_string("2000-05-01T00:00:01")); - UNIT_TEST_CHECK(may != date_t::from_string("2000-09-01T00:00:00")); - UNIT_TEST_CHECK(may != date_t::from_string("1999-05-01T00:00:00")); + UNIT_TEST_CHECK(may == date_t("2000-05-01T00:00:00")); + UNIT_TEST_CHECK(may != date_t("2000-05-01T00:00:01")); + UNIT_TEST_CHECK(may != date_t("2000-09-01T00:00:00")); + UNIT_TEST_CHECK(may != date_t("1999-05-01T00:00:00")); v = may; - v += DAY * 31; + v += MILLISEC(DAY * 31); UNIT_TEST_CHECK(v == jun); v = jul; - v -= DAY * 30; + v -= MILLISEC(DAY * 30); UNIT_TEST_CHECK(v == jun); // check limits for subtractions - v = date_t(12345000); - v -= 12345000; - UNIT_TEST_CHECK(v == date_t::from_string("1970-01-01T00:00:00")); + v = date_t("0001-01-01T00:00:01"); + v -= 1000; + UNIT_TEST_CHECK(v == date_t("0001-01-01T00:00:00")); UNIT_TEST_CHECK_THROW(v -= 1, std::logic_error); // check limits for additions - v = date_t::from_string("9999-12-31T23:59:00"); - v += 59000; - UNIT_TEST_CHECK(v == date_t::from_string("9999-12-31T23:59:59")); - UNIT_TEST_CHECK_THROW(v += 1000, std::logic_error); + v = date_t("292278993-12-31T23:59:59"); + v += 1000; + UNIT_TEST_CHECK(v == date_t("292278994-01-01T00:00:00")); + L(FL("v off by %ld") + % (v.as_millisecs_since_unix_epoch() - LATEST_SUPPORTED_DATE)); + UNIT_TEST_CHECK_THROW(v += 1, std::logic_error); // check date differences - UNIT_TEST_CHECK(date_t::from_string("2000-05-05T00:00:01") - - date_t::from_string("2000-05-05T00:00:00") + UNIT_TEST_CHECK(date_t("2000-05-05T00:00:01") - + date_t("2000-05-05T00:00:00") == 1000); - UNIT_TEST_CHECK(date_t::from_string("2000-05-05T00:00:01") - - date_t::from_string("2000-05-05T00:00:02") + UNIT_TEST_CHECK(date_t("2000-05-05T00:00:01") - + date_t("2000-05-05T00:00:02") == -1000); - UNIT_TEST_CHECK(date_t::from_string("2000-05-05T01:00:00") - - date_t::from_string("2000-05-05T00:00:00") + UNIT_TEST_CHECK(date_t("2000-05-05T01:00:00") - + date_t("2000-05-05T00:00:00") == 3600000); } ============================================================ --- dates.hh 2b1dcada20482787b3637aa113f9a20aa96c8016 +++ dates.hh 69bde209cb015527e9716f9b52a3b56f25b6bd95 @@ -15,45 +15,33 @@ // user's time zone. #include "numeric_vocab.hh" -#include "sanity.hh" -// Our own "struct tm"-like struct to represent broken-down times -struct broken_down_time { - int millisec; /* milliseconds (0 - 999) */ - int sec; /* seconds (0 - 59) */ - int min; /* minutes (0 - 59) */ - int hour; /* hours (0 - 23) */ - int day; /* day of the month (1 - 31) */ - int month; /* month (1 - 12) */ - int year; /* years (anno Domini, i.e. 1999) */ -}; - struct date_t { // initialize to an invalid date - date_t() : d(-1) {} + date_t(); - // initialize from a unix timestamp - date_t(u64 d); + // initialize from milliseconds since the unix epoch + date_t(s64 d); - // Initialize from broken-down time + // initialize from broken-down time date_t(int year, int month, int day, int hour=0, int min=0, int sec=0, int millisec=0); - bool valid() const; + // initialize from a string; presently recognizes only + // ISO 8601 "basic" and "extended" time formats. + date_t(std::string const & s); - // Return the local system's idea of the current date. + // initialize to the current date and time static date_t now(); - // Return the date corresponding to a string. Presently this recognizes - // only ISO 8601 "basic" and "extended" time formats. - static date_t from_string(std::string const &); + bool valid() const; - // Write out date as a string. + // Retrieve the date as a string. std::string as_iso_8601_extended() const; // Retrieve the internal milliseconds count since the Unix epoch. - u64 millisecs_since_unix_epoch() const; + s64 as_millisecs_since_unix_epoch() const; // Date comparison operators bool operator <(date_t const & other) const @@ -79,9 +67,9 @@ private: s64 operator -(date_t const & other) const; private: - // The date as an unsigned 64-bit count of milliseconds since + // The date as a signed 64-bit count of milliseconds since // the Unix epoch (1970-01-01T00:00:00.000). - u64 d; + s64 d; }; std::ostream & operator<< (std::ostream & o, date_t const & d); ============================================================ --- options_list.hh f8e7d28b9c65b05918602919dfe526b7ef549fbe +++ options_list.hh d8404aaabc072997bc8226f83cae955ee5851dcb @@ -158,7 +158,7 @@ OPT(date, "date", date_t, , { try { - date = date_t::from_string(arg); + date = date_t(arg); } catch (std::exception &e) {