# # # patch "NEWS" # from [8ca47c752c2bec4b765cec89238f990c787b0b05] # to [cc1bc2470d38962ca0b3fdb79aa14def46d58955] # # patch "cmd_db.cc" # from [ddc1958a87b2e73ffb96f2d1774340d1d42810b1] # to [1ed3e071783afad66d6bb81ee3b8b30604f15c8f] # # patch "database.cc" # from [15e18738c0cd4d35f955d9079eed9282ee28ab05] # to [55adef1cdb48efd1fd0c0a2ff6f8f3b14265b80b] # # patch "database.hh" # from [40e06a31c4d18cf50a9a3f3f46bb2d14df06fe68] # to [31ba75343ca21813eb814b814d6576440f4f0cfc] # # patch "dates.cc" # from [f263c010c372d5a3de8b3c0c84d9382d85d5804e] # to [c663d2f5af45ca0f099f88860d733a958f805f37] # # patch "dates.hh" # from [a7f141455c581d29547f76c495cfbac204b0be0d] # to [69bde209cb015527e9716f9b52a3b56f25b6bd95] # # patch "options_list.hh" # from [f8e7d28b9c65b05918602919dfe526b7ef549fbe] # to [d8404aaabc072997bc8226f83cae955ee5851dcb] # # patch "rcs_import.cc" # from [295ce31bb7a349f1c0a6b534c2285017ee4cc805] # to [171d54de883512ffef7a85310126c19411313f0a] # ============================================================ --- NEWS 8ca47c752c2bec4b765cec89238f990c787b0b05 +++ NEWS cc1bc2470d38962ca0b3fdb79aa14def46d58955 @@ -1,3 +1,16 @@ +New stuff from nvm.dates* (written on top so as to avoid tedious merge +conflicts, move after landing on mainline): + + New features + + - Additional '--full' option for 'mtn db info' to display some + statistic analysis of the date certs in the database. + + Internal + + - Using 64 bit integer values to represent dates internally. This + has no user visible effect. + ??? ??? ?? ??:??:?? UTC 2009 0.43 release. ============================================================ --- cmd_db.cc ddc1958a87b2e73ffb96f2d1774340d1d42810b1 +++ cmd_db.cc 1ed3e071783afad66d6bb81ee3b8b30604f15c8f @@ -50,13 +50,13 @@ CMD(db_info, "info", "", CMD_REF(db), "" CMD(db_info, "info", "", CMD_REF(db), "", N_("Shows information about the database"), "", - options::opts::none) + options::opts::full) { N(args.size() == 0, F("no arguments needed")); database db(app); - db.info(cout); + db.info(cout, app.opts.full); } CMD(db_version, "version", "", CMD_REF(db), "", ============================================================ --- database.cc 15e18738c0cd4d35f955d9079eed9282ee28ab05 +++ database.cc 55adef1cdb48efd1fd0c0a2ff6f8f3b14265b80b @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include "vector.hh" @@ -79,6 +80,7 @@ using std::vector; using std::set; using std::string; using std::vector; +using std::accumulate; using boost::shared_ptr; using boost::shared_dynamic_cast; @@ -840,7 +842,7 @@ void void -database::info(ostream & out) +database::info(ostream & out, bool analyze) { // don't check the schema ensure_open_for_maintenance(); @@ -971,6 +973,154 @@ database::info(ostream & out) form = form % imp->cache_size(); out << form.str() << '\n'; // final newline is kept out of the translation + + // the following analyzation is only done for --full info + if (!analyze) + return; + + + typedef map rev_date; + rev_date rd; + vector certs; + + L(FL("fetching revision dates")); + imp->get_certs(date_cert_name, certs, "revision_certs"); + + L(FL("analyzing revision dates")); + rev_date::iterator d; + for (vector::iterator i = certs.begin(); i != certs.end(); ++i) + { + date_t cert_date; + try + { + cert_date = date_t(i->value()); + } + catch (informative_failure & e) + { + // simply skip dates we cannot parse + W(F("invalid date: %s for revision %s, skipped") + % i->value() % i->ident); + } + + if (cert_date.valid()) + { + if ((d = rd.find(i->ident)) == rd.end()) + rd.insert(make_pair(i->ident, cert_date)); + else + { + if (d->second > cert_date) + d->second = cert_date; + } + } + } + + L(FL("fetching ancestry map")); + typedef multimap::const_iterator gi; + rev_ancestry_map graph; + get_revision_ancestry(graph); + + L(FL("checking timestamps differences of related revisions")); + int correct = 0, + equal = 0, + incorrect = 0, + root_anc = 0, + missing = 0; + + vector diffs; + + for (gi i = graph.begin(); i != graph.end(); ++i) + { + revision_id anc_rid = i->first, + desc_rid = i->second; + + if (null_id(anc_rid)) + { + root_anc++; + continue; + } + I(!null_id(desc_rid)); + + date_t anc_date, + desc_date; + + map::iterator j; + if ((j = rd.find(anc_rid)) != rd.end()) + anc_date = j->second; + + if ((j = rd.find(desc_rid)) != rd.end()) + desc_date = j->second; + + if (anc_date.valid() && desc_date.valid()) + { + // we only need seconds precision here + s64 diff = (desc_date - anc_date) / 1000; + diffs.push_back(diff); + + if (anc_date < desc_date) + correct++; + else if (anc_date == desc_date) + equal++; + else + { + L(FL(" rev %s -> rev %s") % anc_rid % desc_rid); + L(FL(" but date %s ! -> %s") + % anc_date.as_iso_8601_extended() + % desc_date.as_iso_8601_extended()); + L(FL(" (difference: %d seconds)") + % (anc_date - desc_date)); + incorrect++; + } + } + else + missing++; + } + + form = + F("timestamp correctness between revisions\n" + " correct dates : %s edges\n" + " equal dates : %s edges\n" + " incorrect dates : %s edges\n" + " based on root : %s edges\n" + " missing date(s) : %s edges\n" + "\n" + "timestamp differences between revisions:\n" + " mean : %d sec\n" + " min : %d sec\n" + " max : %d sec\n" + "\n" + " 1st percentile : %s sec\n" + " 5th percentile : %s sec\n" + " 10th percentile : %s sec\n" + " 25th percentile : %s sec\n" + " 50th percentile : %s sec\n" + " 75th percentile : %s sec\n" + " 90th percentile : %s sec\n" + " 95th percentile : %s sec\n" + " 99th percentile : %s sec\n" + ); + + form = form % correct % equal % incorrect % root_anc % missing; + + // sort, so that we can get percentile values + sort(diffs.begin(), diffs.end()); + + // calculate mean time difference, output that, min and max + s64 mean = accumulate(diffs.begin(), diffs.end(), 0); + mean /= diffs.size(); + s64 median = *(diffs.begin() + diffs.size()/2); + form = form % mean % *diffs.begin() % *diffs.rbegin() + % *(diffs.begin() + int(diffs.size() * 0.01)) + % *(diffs.begin() + int(diffs.size() * 0.05)) + % *(diffs.begin() + int(diffs.size() * 0.10)) + % *(diffs.begin() + int(diffs.size() * 0.25)) + % *(diffs.begin() + int(diffs.size() * 0.50)) + % *(diffs.begin() + int(diffs.size() * 0.75)) + % *(diffs.begin() + int(diffs.size() * 0.90)) + % *(diffs.begin() + int(diffs.size() * 0.95)) + % *(diffs.begin() + int(diffs.size() * 0.99)); + + // output the string, with some newlines out of translation + out << '\n' << '\n' << form.str() << '\n'; } void ============================================================ --- database.hh 40e06a31c4d18cf50a9a3f3f46bb2d14df06fe68 +++ database.hh 31ba75343ca21813eb814b814d6576440f4f0cfc @@ -380,7 +380,7 @@ public: void debug(std::string const & sql, std::ostream & out); void dump(std::ostream &); void load(std::istream &); - void info(std::ostream &); + void info(std::ostream &, bool analyze); void version(std::ostream &); void migrate(key_store &); void test_migration_step(key_store &, std::string const &); ============================================================ --- dates.cc f263c010c372d5a3de8b3c0c84d9382d85d5804e +++ dates.cc c663d2f5af45ca0f099f88860d733a958f805f37 @@ -1,4 +1,5 @@ -// Copyright (C) 2007 Zack Weinberg +// Copyright (C) 2007, 2008 Zack Weinberg +// Markus Wanner // // This program is made available under the GNU GPL version 2.0 or // greater. See the accompanying file COPYING for details. @@ -9,77 +10,70 @@ #include "base.hh" #include "dates.hh" +#include "sanity.hh" #include #include +// Generic date handling routines for Monotone. +// +// The routines in this file substantively duplicate functionality of the +// standard C library, so one might wonder why they are needed. There are +// three fundamental portability problems which together force us to +// implement our own date handling: +// +// 1. We want millisecond precision in our dates, and, at the same time, the +// ability to represent dates far in the future. Support for dates far +// in the future (in particular, past 2038) is currently only common on +// 64-bit systems. Support for sub-second resolution is not available at +// all in the standard 'broken-down time' format (struct tm). +// +// 2. There is no standardized way to convert from 'struct tm' to 'time_t' +// without treating the 'struct tm' as local time. Some systems do +// provide a 'timegm' function but it is not widespread. +// +// 3. Some (rare, nowadays) systems do not use the Unix epoch as the epoch +// for 'time_t'. This is only a problem because we support reading +// CVS/RCS ,v files, which encode times as decimal seconds since the Unix +// epoch; so we must support that epoch regardless of what the system does. + 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 -// postpone checking it until runtime (date_t::from_unix_epoch), 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 +// 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 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 -const string & -date_t::as_iso_8601_extended() const -{ - I(this->valid()); - return d; -} +// 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) */ +}; -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(); -} - -date_t -date_t::now() -{ - using std::time_t; - using std::time; - using std::tm; - using std::gmtime; - using std::strftime; - - time_t t = time(0); - struct tm b = *gmtime(&t); - - // in CE 10000, you will need to increase the size of 'buf'. - I(b.tm_year <= 9999); - - char buf[20]; - strftime(buf, sizeof buf, "%Y-%m-%dT%H:%M:%S", &b); - return date_t(string(buf)); -} - // 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(). -// The algorithm below has been tested on one value from every day in the -// range [1970-01-01T00:00:00, 36812-02-20T00:36:16) -- that is, [0, 2**40). // // Unix time_t values are a linear count of seconds since the epoch, // and should be interpreted according to the Gregorian calendar: @@ -92,15 +86,32 @@ date_t::now() // - Years divisible by 400 have 366 days. // // The last two rules are the Gregorian correction to the Julian calendar. -// We make no attempt to handle leap seconds. +// Note that dates before 1582 are treated as if the Gregorian calendar had +// been in effect on that day in history (the 'proleptic' calendar). Also, +// we make no attempt to handle leap seconds. -unsigned int const MIN = 60; -unsigned int const HOUR = MIN * 60; -unsigned int const DAY = HOUR * 24; -unsigned int const YEAR = DAY * 365; -unsigned int const LEAP = DAY * 366; +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 @@ -115,195 +126,340 @@ unsigned char const MONTHS[] = { 31, // dec }; - inline bool -is_leap_year(unsigned int year) +is_leap_year(s32 year) { return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); } -inline u32 -secs_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; + } + +inline bool +valid_ms_count(s64 d) +{ + return (d >= EARLIEST_SUPPORTED_DATE && d <= LATEST_SUPPORTED_DATE); } -date_t -date_t::from_unix_epoch(u64 t) +static void +our_gmtime(s64 ts, broken_down_time & tm) { - // these types hint to the compiler that narrowing divides are safe - u64 yearbeg; - u32 year; - u32 month; - u32 day; - u32 secofday; - u16 hour; - u16 secofhour; - u8 min; - u8 sec; + // 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); - // validate our assumptions about which basic type is u64 (see above). - I(PROBABLE_U64_MAX == std::numeric_limits::max()); + I(valid_ms_count(ts)); - // time_t values after this point will overflow a signed 32-bit year - // counter. 'year' above is unsigned, but the system's struct tm almost - // certainly uses a signed tm_year; it is best to be consistent. - I(t <= u64_C(67767976233532799)); + // All subsequent calculations are easier if 't' is always positive, so we + // make zero be EARLIEST_SUPPORTED_DATE, which happens to be + // 0001-01-01T00:00:00 and is thus a convenient fixed point for leap year + // calculations. - // 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 / 31556952; + u64 t = u64(ts) - u64(EARLIEST_SUPPORTED_DATE); - // Given the above approximation, recalculate the _exact_ number of - // seconds to the beginning of that year. 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 seconds back to the 1970 epoch. - year += 369; + // sub-day components + u64 days = t / MILLISEC(DAY); + u32 ms_in_day = t % MILLISEC(DAY); - yearbeg = (widen(year)*365 + year/4 - year/100 + year/400)*DAY; - yearbeg -= (widen(369)*365 + 369/4 - 369/100 + 369/400)*DAY; + tm.millisec = ms_in_day % 1000; + ms_in_day /= 1000; - // *now* we want year to have its true value. - year += 1601; + tm.sec = ms_in_day % 60; + ms_in_day /= 60; - // Linear search for the range of seconds that really contains t. - // At most one of these loops should iterate, and only once. - while (yearbeg > t) - yearbeg -= secs_in_year(--year); - while (yearbeg + secs_in_year(year) <= t) - yearbeg += secs_in_year(year++); + tm.min = ms_in_day % 60; + tm.hour = ms_in_day / 60; - t -= yearbeg; + // This is the result of inverting the equation + // yb = y*365 + y/4 - y/100 + y/400 + // it approximates years since the epoch for any day count. + u32 year = (400*days / 146097); + // Compute the _exact_ number of days from the epoch to the beginning of + // the approximate year determined above. + u64 yearbeg; + yearbeg = widen(year)*365 + year/4 - year/100 + year/400; + + // Our epoch is year 1, not year 0 (there is no year 0). + year++; + + s64 delta = days - yearbeg; + // The approximation above occasionally guesses the year before the + // correct one, but never the year after, or any further off than that. + if (delta >= days_in_year(year)) + { + delta -= days_in_year(year); + year++; + } + I(0 <= delta && delta < days_in_year(year)); + + tm.year = year; + days = delta; + // Now, the months digit! - month = 0; + u32 month = 1; for (;;) { - unsigned int this_month = MONTHS[month] * DAY; - if (month == 1 && is_leap_year(year)) - this_month += DAY; - if (t < this_month) + u32 this_month = DAYS_PER_MONTH[month-1]; + if (month == 2 && is_leap_year(year)) + this_month += 1; + if (days < this_month) break; - t -= this_month; + days -= this_month; month++; - L(FL("from_unix_epoch: month >= %u, t now %llu") % month % t); - I(month < 12); + I(month <= 12); } + tm.month = month; + tm.day = days + 1; +} - // the rest is straightforward. - day = t / DAY; - secofday = t % DAY; +static s64 +our_timegm(broken_down_time const & tm) +{ + s64 d; - hour = secofday / HOUR; - secofhour = secofday % HOUR; + // 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); - min = secofhour / MIN; - sec = secofhour % MIN; + // 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; - // the widen<>s here are necessary because boost::format *ignores the - // format specification* and prints u8s as characters. - return date_t((FL("%u-%02u-%02uT%02u:%02u:%02u") - % year % (month + 1) % (day + 1) - % hour % widen(min) % widen(sec)).str()); + // months + for (int m = 1; m < tm.month; ++m) + { + d += DAYS_PER_MONTH[m-1] * DAY; + if (m == 2 && is_leap_year(tm.year)) + d += DAY; + } + + // days within month, and so on + d += (tm.day - 1) * DAY; + d += tm.hour * HOUR; + d += tm.min * MIN; + d += tm.sec * SEC; + + return MILLISEC(d) + tm.millisec; } +// 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; + + 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; +} + // We might want to consider teaching this routine more time formats. // gnulib has a rather nice date parser, except that it requires Bison // (not even just yacc). -date_t -date_t::from_string(string const & s) +date_t::date_t(string const & s) { try { - string d = s; - 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 (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 (s.at(i) == '.') + i--; + else + 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--; - else - d.insert(i+1, 1, ':'); // 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--; - else - d.insert(i+1, 1, ':'); // 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")); - // 'T' is required at this point; we also accept a space - N(d.at(i) == 'T' || d.at(i) == ' ', + // We accept 'T' as well as spaces between the date and the time + N(s.at(i) == 'T' || s.at(i) == ' ', F("unrecognized date (monotone only understands ISO 8601 format)")); - - if (d.at(i) == ' ') - d.at(i) = 'T'; 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--; - else - d.insert(i+1, 1, '-'); // 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--; - else - d.insert(i+1, 1, '-'); // year N(i >= 3, @@ -313,28 +469,39 @@ date_t::from_string(string const & s) // (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 >= 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(d); + 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) { @@ -343,26 +510,148 @@ date_t::from_string(string const & s) } } +date_t & +date_t::operator +=(s64 const other) +{ + // only operate on vaild dates, prevent turning an invalid + // date into a valid one. + I(valid()); + + d += other; + + I(valid()); + + return *this; +} + +date_t & +date_t::operator -=(s64 const other) +{ + // simply use the addition operator with inversed sign + return operator+=(-other); +} + +date_t +date_t::operator +(s64 const other) const +{ + date_t result(d); + result += other; + return result; +} + +date_t +date_t::operator -(s64 const other) const +{ + date_t result(d); + result += -other; + return result; +} + +s64 +date_t::operator -(date_t const & other) const +{ + return d - other.d; +} + #ifdef BUILD_UNIT_TESTS #include "unit_tests.hh" +s64 const FOUR_HUNDRED_YEARS = 400 * YEAR + (100 - 4 + 1) * DAY; + +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(s64_C(946684800)); + + // Make sure our_timegm works for years before 1970 as well. + t.year = 1960; + OK(-10 * YEAR - 3 * DAY); + + t.year = 1569; + OK(-FOUR_HUNDRED_YEARS - YEAR); + + t.year = 1570; + OK(-FOUR_HUNDRED_YEARS); + + t.year = 1571; + OK(-FOUR_HUNDRED_YEARS + YEAR); + + t.year = 1572; + OK(-FOUR_HUNDRED_YEARS + 2 * YEAR); + + t.year = 1573; + OK(-FOUR_HUNDRED_YEARS + 3 * YEAR + DAY); + + t.year = 1574; + OK(-FOUR_HUNDRED_YEARS + 4 * YEAR + DAY); + + t.year = 1170; + OK(-2 * FOUR_HUNDRED_YEARS); + + t.year = 770; + OK(-3 * FOUR_HUNDRED_YEARS); + + t.year = 370; + OK(-4 * FOUR_HUNDRED_YEARS); + + t.year = 1; /* year 1 anno Domini */ + OK(-1969 * YEAR - (492 - 19 + 4) * DAY); + + 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"); + OK("2007-03-01T00:41:13", "2007-03-01T00:41:13"); + OK("2007-03-01T01:41:13", "2007-03-01T01:41:13"); + OK("2007-03-01T23:41:13", "2007-03-01T23:41:13"); + + // test dates around leap years + OK("1999-12-31T12:00:00", "1999-12-31T12:00:00"); + OK("1999-12-31T23:59:59", "1999-12-31T23:59:59"); + OK("2000-01-01T00:00:00", "2000-01-01T00:00:00"); + OK("2000-01-01T12:00:00", "2000-01-01T12:00:00"); + OK("2003-12-31T12:00:00", "2003-12-31T12:00:00"); + OK("2003-12-31T23:59:59", "2003-12-31T23:59:59"); + OK("2004-01-01T00:00:00", "2004-01-01T00:00:00"); + OK("2004-01-01T12:00:00", "2004-01-01T12:00:00"); + + // test dates around the leap day in february + OK("2003-02-28T23:59:59", "2003-02-28T23:59:59"); + NO("2003-02-29T00:00:00"); + 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 OK("2007-03-01 18:41:13", "2007-03-01T18:41:13"); // squashed, space OK("20070301 184113", "2007-03-01T18:41:13"); + // more than four digits in the year 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"); @@ -403,9 +692,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"); @@ -446,130 +733,257 @@ UNIT_TEST(date, from_unix_epoch) UNIT_TEST(date, from_unix_epoch) { -#define OK(x,y) do { \ - string s_ = date_t::from_unix_epoch(x).as_iso_8601_extended(); \ - L(FL("from_unix_epoch: %lu -> %s") % (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"); - OK(2678399, "1970-01-31T23:59:59"); - OK(2678400, "1970-02-01T00:00:00"); - OK(5097599, "1970-02-28T23:59:59"); - OK(5097600, "1970-03-01T00:00:00"); - OK(7775999, "1970-03-31T23:59:59"); - OK(7776000, "1970-04-01T00:00:00"); - OK(10367999, "1970-04-30T23:59:59"); - OK(10368000, "1970-05-01T00:00:00"); - OK(13046399, "1970-05-31T23:59:59"); - OK(13046400, "1970-06-01T00:00:00"); - OK(15638399, "1970-06-30T23:59:59"); - OK(15638400, "1970-07-01T00:00:00"); - OK(18316799, "1970-07-31T23:59:59"); - OK(18316800, "1970-08-01T00:00:00"); - OK(20995199, "1970-08-31T23:59:59"); - OK(20995200, "1970-09-01T00:00:00"); - OK(23587199, "1970-09-30T23:59:59"); - OK(23587200, "1970-10-01T00:00:00"); - OK(26265599, "1970-10-31T23:59:59"); - OK(26265600, "1970-11-01T00:00:00"); - OK(28857599, "1970-11-30T23:59:59"); - OK(28857600, "1970-12-01T00:00:00"); - OK(31535999, "1970-12-31T23:59:59"); - OK(31536000, "1971-01-01T00:00:00"); + OK(0, "1970-01-01T00:00:00"); + OK(2678399000, "1970-01-31T23:59:59"); + OK(2678400000, "1970-02-01T00:00:00"); + OK(5097599000, "1970-02-28T23:59:59"); + OK(5097600000, "1970-03-01T00:00:00"); + OK(7775999000, "1970-03-31T23:59:59"); + OK(7776000000, "1970-04-01T00:00:00"); + OK(10367999000, "1970-04-30T23:59:59"); + OK(10368000000, "1970-05-01T00:00:00"); + OK(13046399000, "1970-05-31T23:59:59"); + OK(13046400000, "1970-06-01T00:00:00"); + OK(15638399000, "1970-06-30T23:59:59"); + OK(15638400000, "1970-07-01T00:00:00"); + OK(18316799000, "1970-07-31T23:59:59"); + OK(18316800000, "1970-08-01T00:00:00"); + OK(20995199000, "1970-08-31T23:59:59"); + OK(20995200000, "1970-09-01T00:00:00"); + OK(23587199000, "1970-09-30T23:59:59"); + OK(23587200000, "1970-10-01T00:00:00"); + OK(26265599000, "1970-10-31T23:59:59"); + OK(26265600000, "1970-11-01T00:00:00"); + OK(28857599000, "1970-11-30T23:59:59"); + OK(28857600000, "1970-12-01T00:00:00"); + OK(31535999000, "1970-12-31T23:59:59"); + OK(31536000000, "1971-01-01T00:00:00"); // every month boundary in 1972 (an ordinary leap year) - OK(63071999, "1971-12-31T23:59:59"); - OK(63072000, "1972-01-01T00:00:00"); - OK(65750399, "1972-01-31T23:59:59"); - OK(65750400, "1972-02-01T00:00:00"); - OK(68255999, "1972-02-29T23:59:59"); - OK(68256000, "1972-03-01T00:00:00"); - OK(70934399, "1972-03-31T23:59:59"); - OK(70934400, "1972-04-01T00:00:00"); - OK(73526399, "1972-04-30T23:59:59"); - OK(73526400, "1972-05-01T00:00:00"); - OK(76204799, "1972-05-31T23:59:59"); - OK(76204800, "1972-06-01T00:00:00"); - OK(78796799, "1972-06-30T23:59:59"); - OK(78796800, "1972-07-01T00:00:00"); - OK(81475199, "1972-07-31T23:59:59"); - OK(81475200, "1972-08-01T00:00:00"); - OK(84153599, "1972-08-31T23:59:59"); - OK(84153600, "1972-09-01T00:00:00"); - OK(86745599, "1972-09-30T23:59:59"); - OK(86745600, "1972-10-01T00:00:00"); - OK(89423999, "1972-10-31T23:59:59"); - OK(89424000, "1972-11-01T00:00:00"); - OK(92015999, "1972-11-30T23:59:59"); - OK(92016000, "1972-12-01T00:00:00"); - OK(94694399, "1972-12-31T23:59:59"); - OK(94694400, "1973-01-01T00:00:00"); + OK(63071999000, "1971-12-31T23:59:59"); + OK(63072000000, "1972-01-01T00:00:00"); + OK(65750399000, "1972-01-31T23:59:59"); + OK(65750400000, "1972-02-01T00:00:00"); + OK(68255999000, "1972-02-29T23:59:59"); + OK(68256000000, "1972-03-01T00:00:00"); + OK(70934399000, "1972-03-31T23:59:59"); + OK(70934400000, "1972-04-01T00:00:00"); + OK(73526399000, "1972-04-30T23:59:59"); + OK(73526400000, "1972-05-01T00:00:00"); + OK(76204799000, "1972-05-31T23:59:59"); + OK(76204800000, "1972-06-01T00:00:00"); + OK(78796799000, "1972-06-30T23:59:59"); + OK(78796800000, "1972-07-01T00:00:00"); + OK(81475199000, "1972-07-31T23:59:59"); + OK(81475200000, "1972-08-01T00:00:00"); + OK(84153599000, "1972-08-31T23:59:59"); + OK(84153600000, "1972-09-01T00:00:00"); + OK(86745599000, "1972-09-30T23:59:59"); + OK(86745600000, "1972-10-01T00:00:00"); + OK(89423999000, "1972-10-31T23:59:59"); + OK(89424000000, "1972-11-01T00:00:00"); + OK(92015999000, "1972-11-30T23:59:59"); + OK(92016000000, "1972-12-01T00:00:00"); + OK(94694399000, "1972-12-31T23:59:59"); + OK(94694400000, "1973-01-01T00:00:00"); // every month boundary in 2000 (a leap year per rule 5) - OK(946684799, "1999-12-31T23:59:59"); - OK(946684800, "2000-01-01T00:00:00"); - OK(949363199, "2000-01-31T23:59:59"); - OK(949363200, "2000-02-01T00:00:00"); - OK(951868799, "2000-02-29T23:59:59"); - OK(951868800, "2000-03-01T00:00:00"); - OK(954547199, "2000-03-31T23:59:59"); - OK(954547200, "2000-04-01T00:00:00"); - OK(957139199, "2000-04-30T23:59:59"); - OK(957139200, "2000-05-01T00:00:00"); - OK(959817599, "2000-05-31T23:59:59"); - OK(959817600, "2000-06-01T00:00:00"); - OK(962409599, "2000-06-30T23:59:59"); - OK(962409600, "2000-07-01T00:00:00"); - OK(965087999, "2000-07-31T23:59:59"); - OK(965088000, "2000-08-01T00:00:00"); - OK(967766399, "2000-08-31T23:59:59"); - OK(967766400, "2000-09-01T00:00:00"); - OK(970358399, "2000-09-30T23:59:59"); - OK(970358400, "2000-10-01T00:00:00"); - OK(973036799, "2000-10-31T23:59:59"); - OK(973036800, "2000-11-01T00:00:00"); - OK(975628799, "2000-11-30T23:59:59"); - OK(975628800, "2000-12-01T00:00:00"); - OK(978307199, "2000-12-31T23:59:59"); - OK(978307200, "2001-01-01T00:00:00"); + OK(946684799000, "1999-12-31T23:59:59"); + OK(946684800000, "2000-01-01T00:00:00"); + OK(949363199000, "2000-01-31T23:59:59"); + OK(949363200000, "2000-02-01T00:00:00"); + OK(951868799000, "2000-02-29T23:59:59"); + OK(951868800000, "2000-03-01T00:00:00"); + OK(954547199000, "2000-03-31T23:59:59"); + OK(954547200000, "2000-04-01T00:00:00"); + OK(957139199000, "2000-04-30T23:59:59"); + OK(957139200000, "2000-05-01T00:00:00"); + OK(959817599000, "2000-05-31T23:59:59"); + OK(959817600000, "2000-06-01T00:00:00"); + OK(962409599000, "2000-06-30T23:59:59"); + OK(962409600000, "2000-07-01T00:00:00"); + OK(965087999000, "2000-07-31T23:59:59"); + OK(965088000000, "2000-08-01T00:00:00"); + OK(967766399000, "2000-08-31T23:59:59"); + OK(967766400000, "2000-09-01T00:00:00"); + OK(970358399000, "2000-09-30T23:59:59"); + OK(970358400000, "2000-10-01T00:00:00"); + OK(973036799000, "2000-10-31T23:59:59"); + OK(973036800000, "2000-11-01T00:00:00"); + OK(975628799000, "2000-11-30T23:59:59"); + OK(975628800000, "2000-12-01T00:00:00"); + OK(978307199000, "2000-12-31T23:59:59"); + OK(978307200000, "2001-01-01T00:00:00"); // every month boundary in 2100 (a normal year per rule 4) - OK(u64_C(4102444800), "2100-01-01T00:00:00"); - OK(u64_C(4105123199), "2100-01-31T23:59:59"); - OK(u64_C(4105123200), "2100-02-01T00:00:00"); - OK(u64_C(4107542399), "2100-02-28T23:59:59"); - OK(u64_C(4107542400), "2100-03-01T00:00:00"); - OK(u64_C(4110220799), "2100-03-31T23:59:59"); - OK(u64_C(4110220800), "2100-04-01T00:00:00"); - OK(u64_C(4112812799), "2100-04-30T23:59:59"); - OK(u64_C(4112812800), "2100-05-01T00:00:00"); - OK(u64_C(4115491199), "2100-05-31T23:59:59"); - OK(u64_C(4115491200), "2100-06-01T00:00:00"); - OK(u64_C(4118083199), "2100-06-30T23:59:59"); - OK(u64_C(4118083200), "2100-07-01T00:00:00"); - OK(u64_C(4120761599), "2100-07-31T23:59:59"); - OK(u64_C(4120761600), "2100-08-01T00:00:00"); - OK(u64_C(4123439999), "2100-08-31T23:59:59"); - OK(u64_C(4123440000), "2100-09-01T00:00:00"); - OK(u64_C(4126031999), "2100-09-30T23:59:59"); - OK(u64_C(4126032000), "2100-10-01T00:00:00"); - OK(u64_C(4128710399), "2100-10-31T23:59:59"); - OK(u64_C(4128710400), "2100-11-01T00:00:00"); - OK(u64_C(4131302399), "2100-11-30T23:59:59"); - OK(u64_C(4131302400), "2100-12-01T00:00:00"); - OK(u64_C(4133980799), "2100-12-31T23:59:59"); + OK(4102444800000, "2100-01-01T00:00:00"); + OK(4105123199000, "2100-01-31T23:59:59"); + OK(4105123200000, "2100-02-01T00:00:00"); + OK(4107542399000, "2100-02-28T23:59:59"); + OK(4107542400000, "2100-03-01T00:00:00"); + OK(4110220799000, "2100-03-31T23:59:59"); + OK(4110220800000, "2100-04-01T00:00:00"); + OK(4112812799000, "2100-04-30T23:59:59"); + OK(4112812800000, "2100-05-01T00:00:00"); + OK(4115491199000, "2100-05-31T23:59:59"); + OK(4115491200000, "2100-06-01T00:00:00"); + OK(4118083199000, "2100-06-30T23:59:59"); + OK(4118083200000, "2100-07-01T00:00:00"); + OK(4120761599000, "2100-07-31T23:59:59"); + OK(4120761600000, "2100-08-01T00:00:00"); + OK(4123439999000, "2100-08-31T23:59:59"); + OK(4123440000000, "2100-09-01T00:00:00"); + OK(4126031999000, "2100-09-30T23:59:59"); + OK(4126032000000, "2100-10-01T00:00:00"); + OK(4128710399000, "2100-10-31T23:59:59"); + OK(4128710400000, "2100-11-01T00:00:00"); + OK(4131302399000, "2100-11-30T23:59:59"); + OK(4131302400000, "2100-12-01T00:00:00"); + OK(4133980799000, "2100-12-31T23:59:59"); - // limit of a (signed) 32-bit year counter - OK(u64_C(67767976233532799), "2147483647-12-31T23:59:59"); - UNIT_TEST_CHECK_THROW(date_t::from_unix_epoch(u64_C(67768036191676800)), - 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 +} -#undef OK +UNIT_TEST(date, comparisons) +{ + 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 + UNIT_TEST_CHECK(may < jun); + UNIT_TEST_CHECK(jun < jul); + UNIT_TEST_CHECK(may < jul); + + UNIT_TEST_CHECK(jul > may); + + 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 += MILLISEC(DAY * 31); + UNIT_TEST_CHECK(v == jun); + + v = jul; + v -= MILLISEC(DAY * 30); + UNIT_TEST_CHECK(v == jun); + + // check limits for subtractions + 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("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("2000-05-05T00:00:01") - + date_t("2000-05-05T00:00:00") + == 1000); + UNIT_TEST_CHECK(date_t("2000-05-05T00:00:01") - + date_t("2000-05-05T00:00:02") + == -1000); + UNIT_TEST_CHECK(date_t("2000-05-05T01:00:00") - + date_t("2000-05-05T00:00:00") + == 3600000); } +// This test takes a long time to run and can create an enormous logfile +// (if there are a lot of failures) so it is disabled by default. If you +// make substantial changes to our_gmtime or our_timegm you should run it. +#if 0 +static void +roundtrip_1(s64 t) +{ + if (!valid_ms_count(t)) + return; + + broken_down_time tm; + our_gmtime(t, tm); + s64 t1 = our_timegm(tm); + if (t != t1) + { + L(FL("%d -> %04u-%02u-%02uT%02u:%02u:%02u.%03u -> %d error %+d") + % t + % tm.year % tm.month % tm.day % tm.hour % tm.min % tm.sec % tm.millisec + % t1 + % (t - t1)); + UNIT_TEST_CHECK(t == t1); + } + + // if possible check against the system gmtime() as well + if (std::numeric_limits::max() >= std::numeric_limits::max()) + { + time_t tsys = ((t - tm.millisec) / 1000) - get_epoch_offset(); + std::tm tmsys = *std::gmtime(&tsys); + broken_down_time tmo; + tmo.millisec = 0; + tmo.sec = tmsys.tm_sec; + tmo.min = tmsys.tm_min; + tmo.hour = tmsys.tm_hour; + tmo.day = tmsys.tm_mday; + tmo.month = tmsys.tm_mon + 1; + tmo.year = tmsys.tm_year + 1900; + + bool sys_match = (tm.year == tmo.year + && tm.month == tmo.month + && tm.day == tmo.day + && tm.hour == tmo.hour + && tm.min == tmo.min + && tm.sec == tmo.sec); + if (!sys_match) + { + L(FL("ours: %04u-%02u-%02uT%02u:%02u:%02u.%03u") + % tm.year % tm.month % tm.day % tm.hour % tm.min + % tm.sec % tm.millisec); + L(FL("system: %04u-%02u-%02uT%02u:%02u:%02u") + % tmo.year % tmo.month % tmo.day % tmo.hour % tmo.min % tmo.sec); + UNIT_TEST_CHECK(sys_match); + } + } +} + +UNIT_TEST(date, roundtrip_all_year_boundaries) +{ + s64 t = EARLIEST_SUPPORTED_DATE; + u32 year = 1; + + while (t < LATEST_SUPPORTED_DATE) + { + roundtrip_1(t-1); + roundtrip_1(t); + + t += MILLISEC(DAY * days_in_year(year)); + year ++; + } +} #endif +#endif // Local Variables: // mode: C++ ============================================================ --- dates.hh a7f141455c581d29547f76c495cfbac204b0be0d +++ dates.hh 69bde209cb015527e9716f9b52a3b56f25b6bd95 @@ -15,35 +15,61 @@ // user's time zone. #include "numeric_vocab.hh" -#include "sanity.hh" struct date_t { - // For the benefit of the --date option. - date_t() : d("") {} - bool valid() const { return d != ""; } + // initialize to an invalid date + date_t(); - // Return the local system's idea of the current date. + // initialize from milliseconds since the unix epoch + date_t(s64 d); + + // 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); + + // initialize from a string; presently recognizes only + // ISO 8601 "basic" and "extended" time formats. + date_t(std::string const & s); + + // initialize to the current date and time static date_t now(); - // Return the date corresponding to an unsigned 64-bit count of seconds - // since the Unix epoch (1970-01-01T00:00:00). - static date_t from_unix_epoch(u64); + bool valid() const; - // 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 &); + // Retrieve the date as a string. + std::string as_iso_8601_extended() const; - // Write out date as a string. - std::string const & as_iso_8601_extended() const; + // Retrieve the internal milliseconds count since the Unix epoch. + s64 as_millisecs_since_unix_epoch() const; + // Date comparison operators + bool operator <(date_t const & other) const + { return d < other.d; }; + bool operator <=(date_t const & other) const + { return d <= other.d; }; + bool operator >(date_t const & other) const + { return d > other.d; }; + bool operator >=(date_t const & other) const + { return d >= other.d; }; + bool operator ==(date_t const & other) const + { return d == other.d; }; + bool operator !=(date_t const & other) const + { return d != other.d; }; + + // Addition and subtraction of millisecond amounts + date_t & operator +=(s64 const other); + date_t & operator -=(s64 const other); + date_t operator +(s64 const other) const; + date_t operator -(s64 const other) const; + + // Difference between two dates in milliseconds + s64 operator -(date_t const & other) const; + private: - // For what we do with dates, it is most convenient to store them as - // strings in the ISO 8601 extended time format. - std::string d; - - // used by the above factory functions - date_t(std::string const & s) : d(s) {}; + // The date as a signed 64-bit count of milliseconds since + // the Unix epoch (1970-01-01T00:00:00.000). + 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) { ============================================================ --- rcs_import.cc 295ce31bb7a349f1c0a6b534c2285017ee4cc805 +++ rcs_import.cc 171d54de883512ffef7a85310126c19411313f0a @@ -1368,7 +1368,7 @@ cluster_consumer::store_auxiliary_certs( project.put_standard_certs(keys, p.rid, branch_name(branchname), utf8(cvs.changelog_interner.lookup(p.changelog)), - date_t::from_unix_epoch(p.time), + date_t(p.time), cvs.author_interner.lookup(p.author)); }