From 0b19be208f91e70c683d60c22011cad14a9fa78c Mon Sep 17 00:00:00 2001 From: Shaun Verch Date: Fri, 11 Oct 2013 18:10:35 -0400 Subject: SERVER-6058 Factor out date to UTC handling code and use in mongoexport --- jstests/tool/csvexport_dates.js | 65 ++++++++++++++++++++++++++++++++++++ src/mongo/db/pipeline/expression.cpp | 4 +-- src/mongo/db/pipeline/value.cpp | 17 +--------- src/mongo/util/time_support.cpp | 29 ++++++++++++++-- src/mongo/util/time_support.h | 12 +++++++ src/mongo/util/time_support_test.cpp | 46 +++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 21 deletions(-) create mode 100644 jstests/tool/csvexport_dates.js diff --git a/jstests/tool/csvexport_dates.js b/jstests/tool/csvexport_dates.js new file mode 100644 index 00000000000..7d1c776baa5 --- /dev/null +++ b/jstests/tool/csvexport_dates.js @@ -0,0 +1,65 @@ +// Test that we can handle various edge cases of a date type in a csv export + +t = new ToolTest("csvexport_date_before_epoch") + +c = t.startDB("foo"); + +function test(date) { + print("testing " + date); + + c.drop(); + + assert.eq(0, c.count(), "initial collection not empty"); + + c.insert({ _id : 1, date : date }); + + assert.eq(1, c.count(), "failed to insert document into collection"); + + t.runTool("export", "--out", t.extFile, "-d", t.baseName, "-c", "foo", "--csv", "-f", + "_id,date") + + c.drop() + + assert.eq(0, c.count(), "failed to drop collection") + + t.runTool("import", "--file", t.extFile, "-d", t.baseName, "-c", "foo", "--type", "csv", + "--headerline"); + + assert.soon(1 + " == c.count()", "after import"); + + // Note: Exporting and Importing to/from CSV is not designed to be round-trippable + var expected = { "date" : date.toISOString() }; + + var actual = c.findOne(); + + delete actual._id + assert.eq(expected, actual, "imported doc did not match expected"); +} + +// Basic test +test(ISODate('1960-01-02 03:04:05.006Z')); + +// Testing special rounding rules for seconds +test(ISODate('1960-01-02 03:04:04.999Z')); // second = 4 +test(ISODate('1960-01-02 03:04:05.000Z')); // second = 5 +test(ISODate('1960-01-02 03:04:05.001Z')); // second = 5 +test(ISODate('1960-01-02 03:04:05.999Z')); // second = 5 + +// Test date before 1900 (negative tm_year values from gmtime) +test(ISODate('1860-01-02 03:04:05.006Z')); + +// Test with time_t == -1 and 0 +test(new Date(-1000)); +test(new Date(0)); + +// Testing dates between 1970 and 2000 +test(ISODate('1970-01-01 00:00:00.000Z')); +test(ISODate('1970-01-01 00:00:00.999Z')); +test(ISODate('1980-05-20 12:53:64.834Z')); +test(ISODate('1999-12-31 00:00:00.000Z')); +test(ISODate('1999-12-31 23:59:59.999Z')); + +// Test date > 2000 for completeness (using now) +test(new Date()); + +t.stop(); diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index 8fb62a388a8..cf9eaca53c3 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -1437,9 +1437,7 @@ namespace { Value ExpressionMillisecond::evaluateInternal(const Variables& vars) const { Value date(vpOperand[0]->evaluateInternal(vars)); - const int ms = date.coerceToDate() % 1000LL; - // adding 1000 since dates before 1970 would have negative ms - return Value(ms >= 0 ? ms : 1000 + ms); + return Value(extractMillisPortion(date.coerceToDate())); } REGISTER_EXPRESSION("$millisecond", ExpressionMillisecond::parse); diff --git a/src/mongo/db/pipeline/value.cpp b/src/mongo/db/pipeline/value.cpp index 9139f0cdbed..26f01e85d2e 100644 --- a/src/mongo/db/pipeline/value.cpp +++ b/src/mongo/db/pipeline/value.cpp @@ -426,22 +426,7 @@ namespace mongo { } time_t Value::coerceToTimeT() const { - long long millis = coerceToDate(); - if (millis < 0) { - // We want the division below to truncate toward -inf rather than 0 - // eg Dec 31, 1969 23:59:58.001 should be -2 seconds rather than -1 - // This is needed to get the correct values from coerceToTM - if ( -1999 / 1000 != -2) { // this is implementation defined - millis -= 1000-1; - } - } - const long long seconds = millis / 1000; - - uassert(16421, "Can't handle date values outside of time_t range", - seconds >= std::numeric_limits::min() && - seconds <= std::numeric_limits::max()); - - return static_cast(seconds); + return millisToTimeT(coerceToDate()); } tm Value::coerceToTm() const { // See implementation in Date_t. diff --git a/src/mongo/util/time_support.cpp b/src/mongo/util/time_support.cpp index 76190501e14..fc7b71eef1f 100644 --- a/src/mongo/util/time_support.cpp +++ b/src/mongo/util/time_support.cpp @@ -112,12 +112,13 @@ namespace mongo { const int bufSize = 32; char buf[bufSize]; struct tm t; - time_t_to_Struct(date.toTimeT(), &t, local); + time_t_to_Struct(millisToTimeT(static_cast(date.millis)), &t, local); int pos = strftime(buf, bufSize, MONGO_ISO_DATE_FMT_NO_TZ, &t); fassert(16981, 0 < pos); char* cur = buf + pos; int bufRemaining = bufSize - pos; - pos = snprintf(cur, bufRemaining, ".%03d", static_cast(date.asInt64() % 1000)); + pos = snprintf(cur, bufRemaining, ".%03d", + extractMillisPortion(static_cast(date.millis))); fassert(16982, bufRemaining > pos && pos > 0); cur += pos; bufRemaining -= pos; @@ -182,6 +183,30 @@ namespace mongo { return millis / 1000; } + time_t millisToTimeT(long long millis) { + if (millis < 0) { + // We want the division below to truncate toward -inf rather than 0 + // eg Dec 31, 1969 23:59:58.001 should be -2 seconds rather than -1 + // This is needed to get the correct values from coerceToTM + if ( -1999 / 1000 != -2) { // this is implementation defined + millis -= 1000-1; + } + } + const long long seconds = millis / 1000; + + uassert(16421, "Can't handle date values outside of time_t range", + seconds >= std::numeric_limits::min() && + seconds <= std::numeric_limits::max()); + + return static_cast(seconds); + } + + int extractMillisPortion(long long millisSinceEpoch) { + const int ms = millisSinceEpoch % 1000LL; + // adding 1000 since dates before 1970 would have negative ms + return ms >= 0 ? ms : 1000 + ms; + } + std::string dateToCtimeString(Date_t date) { time_t t = date.toTimeT(); char buf[64]; diff --git a/src/mongo/util/time_support.h b/src/mongo/util/time_support.h index 46cee85e97a..1043a658142 100644 --- a/src/mongo/util/time_support.h +++ b/src/mongo/util/time_support.h @@ -78,6 +78,18 @@ namespace mongo { */ std::string dateToCtimeString(Date_t date); + /** + * Converts millis to time_t, doing correct division for negative millis, and uasserting that + * the result falls within the valid range of a time_t. + */ + time_t millisToTimeT(long long millis); + + /** + * Returns the millis since the last whole second of the given millis since epoch, and correctly + * handles dates before epoch. + */ + int extractMillisPortion(long long millisSinceEpoch); + boost::gregorian::date currentDate(); // parses time of day in "hh:mm" format assuming 'hh' is 00-23 diff --git a/src/mongo/util/time_support_test.cpp b/src/mongo/util/time_support_test.cpp index c459c124645..51848153133 100644 --- a/src/mongo/util/time_support_test.cpp +++ b/src/mongo/util/time_support_test.cpp @@ -63,6 +63,52 @@ namespace { dateToISOStringUTC(Date_t(2781455351100ULL))); ASSERT_EQUALS(std::string("2013-02-20T18:29:11.100Z"), dateToISOStringUTC(Date_t(1361384951100ULL))); + + // Basic test +#ifndef _WIN32 // Negative Dates don't currently work on Windows + ASSERT_EQUALS(std::string("1960-01-02T03:04:05.006Z"), + dateToISOStringUTC(Date_t(-315521754994LL))); +#endif + + // Testing special rounding rules for seconds +#ifndef _WIN32 // Negative Dates don't currently work on Windows + ASSERT_EQUALS(std::string("1960-01-02T03:04:04.999Z"), + dateToISOStringUTC(Date_t(-315521755001LL))); // second = 4 + ASSERT_EQUALS(std::string("1960-01-02T03:04:05.000Z"), + dateToISOStringUTC(Date_t(-315521755000LL))); // second = 5 + ASSERT_EQUALS(std::string("1960-01-02T03:04:05.001Z"), + dateToISOStringUTC(Date_t(-315521754999LL))); // second = 5 + ASSERT_EQUALS(std::string("1960-01-02T03:04:05.999Z"), + dateToISOStringUTC(Date_t(-315521754001LL))); // second = 5 +#endif + + // Test date before 1900 (negative tm_year values from gmtime) +#ifndef _WIN32 // Negative Dates don't currently work on Windows + ASSERT_EQUALS(std::string("1860-01-02T03:04:05.006Z"), + dateToISOStringUTC(Date_t(-3471195354994LL))); +#endif + + // Test with time_t == -1 +#ifndef _WIN32 // Negative Dates don't currently work on Windows + ASSERT_EQUALS(std::string("1969-12-31T23:59:59.000Z"), + dateToISOStringUTC(Date_t(-1000LL))); +#endif + + // Testing dates between 1970 and 2000 + ASSERT_EQUALS(std::string("1970-01-01T00:00:00.000Z"), + dateToISOStringUTC(Date_t(0ULL))); + ASSERT_EQUALS(std::string("1970-01-01T00:00:00.999Z"), + dateToISOStringUTC(Date_t(999ULL))); + ASSERT_EQUALS(std::string("1980-05-20T12:54:04.834Z"), + dateToISOStringUTC(Date_t(327675244834ULL))); + ASSERT_EQUALS(std::string("1999-12-31T00:00:00.000Z"), + dateToISOStringUTC(Date_t(946598400000ULL))); + ASSERT_EQUALS(std::string("1999-12-31T23:59:59.999Z"), + dateToISOStringUTC(Date_t(946684799999ULL))); + + // Test date > 2000 for completeness (using now) + ASSERT_EQUALS(std::string("2013-10-11T23:20:12.072Z"), + dateToISOStringUTC(Date_t(1381533612072ULL))); } TEST(TimeFormatting, DateAsISO8601Local) { -- cgit v1.2.1