summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Zolnierz <nicholas.zolnierz@mongodb.com>2018-01-19 11:36:11 -0500
committerNick Zolnierz <nicholas.zolnierz@mongodb.com>2018-02-05 16:48:17 -0500
commitb721d0f20ad3dc1b1e9c20c63592c7da15846935 (patch)
tree291ca9bda643d1c92d0559e5e8abaa75412cd9ab
parent0f2cc83cdb0320563f6de507885e9c7b17313fa7 (diff)
downloadmongo-b721d0f20ad3dc1b1e9c20c63592c7da15846935.tar.gz
SERVER-32771: Add format specifier for $dateFromString expression
-rw-r--r--jstests/aggregation/expressions/date_from_string.js490
-rw-r--r--jstests/aggregation/expressions/date_to_string.js28
-rw-r--r--jstests/aggregation/extras/utils.js13
-rw-r--r--src/mongo/db/pipeline/expression.cpp56
-rw-r--r--src/mongo/db/pipeline/expression.h4
-rw-r--r--src/mongo/db/pipeline/expression_test.cpp172
-rw-r--r--src/mongo/db/query/datetime/date_time_support.cpp134
-rw-r--r--src/mongo/db/query/datetime/date_time_support.h16
-rw-r--r--src/mongo/db/query/datetime/date_time_support_test.cpp130
9 files changed, 879 insertions, 164 deletions
diff --git a/jstests/aggregation/expressions/date_from_string.js b/jstests/aggregation/expressions/date_from_string.js
index 78d6b815b78..9dfe4a355b4 100644
--- a/jstests/aggregation/expressions/date_from_string.js
+++ b/jstests/aggregation/expressions/date_from_string.js
@@ -1,4 +1,4 @@
-load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
+load("jstests/aggregation/extras/utils.js"); // For assertErrorCode and assertErrMsgContains.
(function() {
"use strict";
@@ -12,20 +12,63 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
assert.writeOK(coll.insert({_id: 0}));
let testCases = [
- {expect: "2017-07-04T11:56:02Z", inputString: "2017-07-04T11:56:02Z"},
- {expect: "2017-07-04T11:56:02.813Z", inputString: "2017-07-04T11:56:02.813Z"},
- {expect: "2017-07-04T11:56:02.810Z", inputString: "2017-07-04T11:56:02.81Z"},
- {expect: "2017-07-04T11:56:02.800Z", inputString: "2017-07-04T11:56:02.8Z"},
- {expect: "2017-07-04T11:56:02Z", inputString: "2017-07-04T11:56.02"},
- {expect: "2017-07-04T11:56:02.813Z", inputString: "2017-07-04T11:56.02.813"},
- {expect: "2017-07-04T11:56:02.810Z", inputString: "2017-07-04T11:56.02.81"},
- {expect: "2017-07-04T11:56:02.800Z", inputString: "2017-07-04T11:56.02.8"},
+ {
+ expect: "2017-07-04T11:56:02Z",
+ inputString: "2017-07-04T11:56:02Z",
+ format: "%Y-%m-%dT%H:%M:%SZ"
+ },
+ {
+ expect: "2017-07-04T11:56:02.813Z",
+ inputString: "2017-07-04T11:56:02.813Z",
+ format: "%Y-%m-%dT%H:%M:%S.%LZ"
+ },
+ {
+ expect: "2017-07-04T11:56:02.810Z",
+ inputString: "2017-07-04T11:56:02.81Z",
+ format: "%Y-%m-%dT%H:%M:%S.%LZ"
+ },
+ {
+ expect: "2017-07-04T11:56:02.800Z",
+ inputString: "2017-07-04T11:56:02.8Z",
+ format: "%Y-%m-%dT%H:%M:%S.%LZ"
+ },
+ {
+ expect: "2017-07-04T11:56:02Z",
+ inputString: "2017-07-04T11:56.02",
+ format: "%Y-%m-%dT%H:%M.%S"
+ },
+ {
+ expect: "2017-07-04T11:56:02.813Z",
+ inputString: "2017-07-04T11:56.02.813",
+ format: "%Y-%m-%dT%H:%M.%S.%L"
+ },
+ {
+ expect: "2017-07-04T11:56:02.810Z",
+ inputString: "2017-07-04T11:56.02.81",
+ format: "%Y-%m-%dT%H:%M.%S.%L"
+ },
+ {
+ expect: "2017-07-04T11:56:02.800Z",
+ inputString: "2017-07-04T11:56.02.8",
+ format: "%Y-%m-%dT%H:%M.%S.%L"
+ },
];
testCases.forEach(function(testCase) {
+ assert.eq([{_id: 0, date: ISODate(testCase.expect)}],
+ coll.aggregate(
+ {$project: {date: {$dateFromString: {dateString: testCase.inputString}}}})
+ .toArray(),
+ tojson(testCase));
assert.eq(
[{_id: 0, date: ISODate(testCase.expect)}],
- coll.aggregate(
- {$project: {date: {'$dateFromString': {"dateString": testCase.inputString}}}})
+ coll.aggregate({
+ $project: {
+ date: {
+ $dateFromString:
+ {dateString: testCase.inputString, format: testCase.format}
+ }
+ }
+ })
.toArray(),
tojson(testCase));
});
@@ -37,10 +80,26 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
assert.writeOK(coll.insert({_id: 0}));
testCases = [
- {expect: "2017-07-04T10:56:02Z", inputString: "2017-07-04T11:56.02"},
- {expect: "2017-07-04T10:56:02.813Z", inputString: "2017-07-04T11:56.02.813"},
- {expect: "2017-07-04T10:56:02.810Z", inputString: "2017-07-04T11:56.02.81"},
- {expect: "2017-07-04T10:56:02.800Z", inputString: "2017-07-04T11:56.02.8"},
+ {
+ expect: "2017-07-04T10:56:02Z",
+ inputString: "2017-07-04T11:56.02",
+ format: "%Y-%m-%dT%H:%M.%S"
+ },
+ {
+ expect: "2017-07-04T10:56:02.813Z",
+ inputString: "2017-07-04T11:56.02.813",
+ format: "%Y-%m-%dT%H:%M.%S.%L"
+ },
+ {
+ expect: "2017-07-04T10:56:02.810Z",
+ inputString: "2017-07-04T11:56.02.81",
+ format: "%Y-%m-%dT%H:%M.%S.%L"
+ },
+ {
+ expect: "2017-07-04T10:56:02.800Z",
+ inputString: "2017-07-04T11:56.02.8",
+ format: "%Y-%m-%dT%H:%M.%S.%L"
+ },
];
testCases.forEach(function(testCase) {
assert.eq(
@@ -55,6 +114,20 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
})
.toArray(),
tojson(testCase));
+ assert.eq([{_id: 0, date: ISODate(testCase.expect)}],
+ coll.aggregate({
+ $project: {
+ date: {
+ $dateFromString: {
+ dateString: testCase.inputString,
+ timezone: "Europe/London",
+ format: testCase.format
+ }
+ }
+ }
+ })
+ .toArray(),
+ tojson(testCase));
});
/* --------------------------------------------------------------------------------------- */
@@ -64,10 +137,26 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
assert.writeOK(coll.insert({_id: 0}));
testCases = [
- {expect: "2017-07-04T10:56:02Z", inputString: "2017-07-04T11:56.02"},
- {expect: "2017-07-04T10:56:02.813Z", inputString: "2017-07-04T11:56.02.813"},
- {expect: "2017-07-04T10:56:02.810Z", inputString: "2017-07-04T11:56.02.81"},
- {expect: "2017-07-04T10:56:02.800Z", inputString: "2017-07-04T11:56.02.8"},
+ {
+ expect: "2017-07-04T10:56:02Z",
+ inputString: "2017-07-04T11:56.02",
+ format: "%Y-%m-%dT%H:%M.%S"
+ },
+ {
+ expect: "2017-07-04T10:56:02.813Z",
+ inputString: "2017-07-04T11:56.02.813",
+ format: "%Y-%m-%dT%H:%M.%S.%L"
+ },
+ {
+ expect: "2017-07-04T10:56:02.810Z",
+ inputString: "2017-07-04T11:56.02.81",
+ format: "%Y-%m-%dT%H:%M.%S.%L"
+ },
+ {
+ expect: "2017-07-04T10:56:02.800Z",
+ inputString: "2017-07-04T11:56.02.8",
+ format: "%Y-%m-%dT%H:%M.%S.%L"
+ },
];
testCases.forEach(function(testCase) {
assert.eq([{_id: 0, date: ISODate(testCase.expect)}],
@@ -81,6 +170,20 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
})
.toArray(),
tojson(testCase));
+ assert.eq([{_id: 0, date: ISODate(testCase.expect)}],
+ coll.aggregate({
+ $project: {
+ date: {
+ $dateFromString: {
+ dateString: testCase.inputString,
+ timezone: "+01:00",
+ format: testCase.format
+ }
+ }
+ }
+ })
+ .toArray(),
+ tojson(testCase));
});
/* --------------------------------------------------------------------------------------- */
@@ -88,40 +191,64 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
coll.drop();
assert.writeOK(coll.insert([
- {_id: 0, dateString: "2017-07-06T12:35:37Z"},
- {_id: 1, dateString: "2017-07-06T12:35:37.513Z"},
- {_id: 2, dateString: "2017-07-06T12:35:37"},
- {_id: 3, dateString: "2017-07-06T12:35:37.513"},
- {_id: 4, dateString: "1960-07-10T12:10:37.448"},
+ {_id: 0, dateString: "2017-07-06T12:35:37Z", format: "%Y-%m-%dT%H:%M:%SZ"},
+ {_id: 1, dateString: "2017-07-06T12:35:37.513Z", format: "%Y-%m-%dT%H:%M:%S.%LZ"},
+ {_id: 2, dateString: "2017-07-06T12:35:37", format: "%Y-%m-%dT%H:%M:%S"},
+ {_id: 3, dateString: "2017-07-06T12:35:37.513", format: "%Y-%m-%dT%H:%M:%S.%L"},
+ {_id: 4, dateString: "1960-07-10T12:10:37.448", format: "%Y-%m-%dT%H:%M:%S.%L"},
]));
+ let expectedResults = [
+ {"_id": 0, "date": ISODate("2017-07-06T12:35:37Z")},
+ {"_id": 1, "date": ISODate("2017-07-06T12:35:37.513Z")},
+ {"_id": 2, "date": ISODate("2017-07-06T12:35:37Z")},
+ {"_id": 3, "date": ISODate("2017-07-06T12:35:37.513Z")},
+ {"_id": 4, "date": ISODate("1960-07-10T12:10:37.448Z")},
+ ];
+ assert.eq(expectedResults,
+ coll.aggregate([
+ {
+ $project: {date: {$dateFromString: {dateString: "$dateString"}}},
+ },
+ {$sort: {_id: 1}}
+ ])
+ .toArray());
+
+ // Repeat the test with an explicit format specifier string.
assert.eq(
- [
- {"_id": 0, "date": ISODate("2017-07-06T12:35:37Z")},
- {"_id": 1, "date": ISODate("2017-07-06T12:35:37.513Z")},
- {"_id": 2, "date": ISODate("2017-07-06T12:35:37Z")},
- {"_id": 3, "date": ISODate("2017-07-06T12:35:37.513Z")},
- {"_id": 4, "date": ISODate("1960-07-10T12:10:37.448Z")},
- ],
+ expectedResults,
coll.aggregate([
{
- $project: {date: {'$dateFromString': {dateString: "$dateString"}}},
+ $project:
+ {date: {$dateFromString: {dateString: "$dateString", format: "$format"}}},
},
{$sort: {_id: 1}}
])
.toArray());
+ expectedResults = [
+ {"_id": 0, "date": new Date(1499344537000)},
+ {"_id": 1, "date": new Date(1499344537513)},
+ {"_id": 2, "date": new Date(1499344537000)},
+ {"_id": 3, "date": new Date(1499344537513)},
+ {"_id": 4, "date": new Date(-299072962552)},
+ ];
+ assert.eq(expectedResults,
+ coll.aggregate([
+ {
+ $project: {date: {$dateFromString: {dateString: "$dateString"}}},
+ },
+ {$sort: {_id: 1}}
+ ])
+ .toArray());
+
+ // Repeat the test with an explicit format specifier string.
assert.eq(
- [
- {"_id": 0, "date": new Date(1499344537000)},
- {"_id": 1, "date": new Date(1499344537513)},
- {"_id": 2, "date": new Date(1499344537000)},
- {"_id": 3, "date": new Date(1499344537513)},
- {"_id": 4, "date": new Date(-299072962552)},
- ],
+ expectedResults,
coll.aggregate([
{
- $project: {date: {'$dateFromString': {dateString: "$dateString"}}},
+ $project:
+ {date: {$dateFromString: {dateString: "$dateString", format: "$format"}}},
},
{$sort: {_id: 1}}
])
@@ -141,27 +268,45 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
{_id: 6, dateString: "2017-07-06T12:35:37.513", timezone: "+04:00"},
]));
+ expectedResults = [
+ {"_id": 0, "date": ISODate("2017-07-06T12:35:37.513Z")},
+ {"_id": 1, "date": ISODate("2017-07-06T12:35:37.513Z")},
+ {"_id": 2, "date": ISODate("1960-07-10T16:35:37.513Z")},
+ {"_id": 3, "date": ISODate("1960-07-10T11:35:37.513Z")},
+ {"_id": 4, "date": ISODate("2017-07-06T19:35:37.513Z")},
+ {"_id": 5, "date": ISODate("2017-07-06T10:35:37.513Z")},
+ {"_id": 6, "date": ISODate("2017-07-06T08:35:37.513Z")},
+ ];
+
assert.eq(
- [
- {"_id": 0, "date": ISODate("2017-07-06T12:35:37.513Z")},
- {"_id": 1, "date": ISODate("2017-07-06T12:35:37.513Z")},
- {"_id": 2, "date": ISODate("1960-07-10T16:35:37.513Z")},
- {"_id": 3, "date": ISODate("1960-07-10T11:35:37.513Z")},
- {"_id": 4, "date": ISODate("2017-07-06T19:35:37.513Z")},
- {"_id": 5, "date": ISODate("2017-07-06T10:35:37.513Z")},
- {"_id": 6, "date": ISODate("2017-07-06T08:35:37.513Z")},
- ],
+ expectedResults,
coll.aggregate([
{
- $project: {
- date:
- {'$dateFromString': {dateString: "$dateString", timezone: "$timezone"}}
- },
+ $project:
+ {date: {$dateFromString: {dateString: "$dateString", timezone: "$timezone"}}},
},
{$sort: {_id: 1}}
])
.toArray());
+ // Repeat the test with an explicit format specifier string.
+ assert.eq(expectedResults,
+ coll.aggregate([
+ {
+ $project: {
+ date: {
+ $dateFromString: {
+ dateString: "$dateString",
+ timezone: "$timezone",
+ format: "%Y-%m-%dT%H:%M:%S.%L"
+ }
+ }
+ },
+ },
+ {$sort: {_id: 1}}
+ ])
+ .toArray());
+
/* --------------------------------------------------------------------------------------- */
/* dateString from data with timezone as constant */
@@ -177,8 +322,7 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
coll.aggregate([
{
$project: {
- date:
- {'$dateFromString': {dateString: "$dateString", timezone: "Asia/Tokyo"}}
+ date: {$dateFromString: {dateString: "$dateString", timezone: "Asia/Tokyo"}}
},
},
{$sort: {_id: 1}}
@@ -205,7 +349,7 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
{
$project: {
date: {
- '$dateFromString':
+ $dateFromString:
{dateString: "2017-07-19T18:52:35.199", timezone: "$timezone"}
}
},
@@ -223,31 +367,31 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
let pipelines = [
{
expect: "2017-01-01T00:00:00Z",
- pipeline: {$project: {date: {'$dateFromString': {"dateString": "2017-01-01 00:00:00"}}}}
+ pipeline: {$project: {date: {$dateFromString: {dateString: "2017-01-01 00:00:00"}}}}
},
{
expect: "2017-07-01T00:00:00Z",
- pipeline: {$project: {date: {'$dateFromString': {"dateString": "2017-07-01 00:00:00"}}}}
+ pipeline: {$project: {date: {$dateFromString: {dateString: "2017-07-01 00:00:00"}}}}
},
{
expect: "2017-07-06T00:00:00Z",
- pipeline: {$project: {date: {'$dateFromString': {"dateString": "2017-07-06"}}}}
+ pipeline: {$project: {date: {$dateFromString: {dateString: "2017-07-06"}}}}
},
{
expect: "2017-07-06T00:00:00Z",
- pipeline: {$project: {date: {'$dateFromString': {"dateString": "2017-07-06 00:00:00"}}}}
+ pipeline: {$project: {date: {$dateFromString: {dateString: "2017-07-06 00:00:00"}}}}
},
{
expect: "2017-07-06T11:00:00Z",
- pipeline: {$project: {date: {'$dateFromString': {"dateString": "2017-07-06 11:00:00"}}}}
+ pipeline: {$project: {date: {$dateFromString: {dateString: "2017-07-06 11:00:00"}}}}
},
{
expect: "2017-07-06T11:36:00Z",
- pipeline: {$project: {date: {'$dateFromString': {"dateString": "2017-07-06 11:36:00"}}}}
+ pipeline: {$project: {date: {$dateFromString: {dateString: "2017-07-06 11:36:00"}}}}
},
{
expect: "2017-07-06T11:36:54Z",
- pipeline: {$project: {date: {'$dateFromString': {"dateString": "2017-07-06 11:36:54"}}}}
+ pipeline: {$project: {date: {$dateFromString: {dateString: "2017-07-06 11:36:54"}}}}
},
];
pipelines.forEach(function(pipeline) {
@@ -285,12 +429,24 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
{expect: "2017-07-14T12:02:44.771Z", inputString: "2017-07-14T12:02:44.771 Z"},
];
testCases.forEach(function(testCase) {
- assert.eq(
- [{_id: 0, date: ISODate(testCase.expect)}],
- coll.aggregate(
- {$project: {date: {'$dateFromString': {"dateString": testCase.inputString}}}})
- .toArray(),
- tojson(testCase));
+ assert.eq([{_id: 0, date: ISODate(testCase.expect)}],
+ coll.aggregate(
+ {$project: {date: {$dateFromString: {dateString: testCase.inputString}}}})
+ .toArray(),
+ tojson(testCase));
+ assert.eq([{_id: 0, date: ISODate(testCase.expect)}],
+ coll.aggregate({
+ $project: {
+ date: {
+ $dateFromString: {
+ dateString: testCase.inputString,
+ format: "%Y-%m-%dT%H:%M:%S.%L%z"
+ }
+ }
+ }
+ })
+ .toArray(),
+ tojson(testCase));
});
/* --------------------------------------------------------------------------------------- */
@@ -319,7 +475,7 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
],
coll.aggregate([
{
- $project: {date: {'$dateFromString': {dateString: "$dateString"}}},
+ $project: {date: {$dateFromString: {dateString: "$dateString"}}},
},
{$sort: {_id: 1}}
])
@@ -355,13 +511,74 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
],
coll.aggregate([
{
- $project: {date: {'$dateFromString': {dateString: "$dateString"}}},
+ $project: {date: {$dateFromString: {dateString: "$dateString"}}},
},
{$sort: {_id: 1}}
])
.toArray());
/* --------------------------------------------------------------------------------------- */
+ /* Tests formats that aren't supported with the normal $dateFromString parser. */
+
+ coll.drop();
+ assert.writeOK(coll.insert({_id: 0}));
+
+ testCases = [
+ {inputString: "05 12 1988", format: "%d %m %Y", expect: "1988-12-05T00:00:00Z"},
+ {inputString: "1992 04 26", format: "%Y %m %d", expect: "1992-04-26T00:00:00Z"},
+ {inputString: "05*12*1988", format: "%d*%m*%Y", expect: "1988-12-05T00:00:00Z"},
+ {inputString: "1992/04/26", format: "%Y/%m/%d", expect: "1992-04-26T00:00:00Z"},
+ {inputString: "1992 % 04 % 26", format: "%Y %% %m %% %d", expect: "1992-04-26T00:00:00Z"},
+ {
+ inputString: "Day: 05 Month: 12 Year: 1988",
+ format: "Day: %d Month: %m Year: %Y",
+ expect: "1988-12-05T00:00:00Z"
+ },
+ {inputString: "Date: 1992/04/26", format: "Date: %Y/%m/%d", expect: "1992-04-26T00:00:00Z"},
+ {inputString: "4/26/1992:+0445", format: "%m/%d/%Y:%z", expect: "1992-04-25T19:15:00Z"},
+ {inputString: "4/26/1992:+285", format: "%m/%d/%Y:%Z", expect: "1992-04-25T19:15:00Z"},
+ ];
+ testCases.forEach(function(testCase) {
+ assert.eq(
+ [{_id: 0, date: ISODate(testCase.expect)}],
+ coll.aggregate({
+ $project: {
+ date: {
+ $dateFromString:
+ {dateString: testCase.inputString, format: testCase.format}
+ }
+ }
+ })
+ .toArray(),
+ tojson(testCase));
+ });
+
+ /* --------------------------------------------------------------------------------------- */
+ /* Tests for ISO year, week of year, and day of the week. */
+
+ testCases = [
+ {inputString: "2017", format: "%G", expect: "2017-01-02T00:00:00Z"},
+ {inputString: "2017, Week 53", format: "%G, Week %V", expect: "2018-01-01T00:00:00Z"},
+ {inputString: "2017, Day 5", format: "%G, Day %u", expect: "2017-01-06T00:00:00Z"},
+ {inputString: "53.7.2017", format: "%V.%u.%G", expect: "2018-01-07T00:00:00Z"},
+ {inputString: "1.1.1", format: "%V.%u.%G", expect: "0001-01-01T00:00:00Z"},
+ ];
+ testCases.forEach(function(testCase) {
+ assert.eq(
+ [{_id: 0, date: ISODate(testCase.expect)}],
+ coll.aggregate({
+ $project: {
+ date: {
+ $dateFromString:
+ {dateString: testCase.inputString, format: testCase.format}
+ }
+ }
+ })
+ .toArray(),
+ tojson(testCase));
+ });
+
+ /* --------------------------------------------------------------------------------------- */
/* Testing whether it throws the right assert for missing elements of a date/time string. */
coll.drop();
@@ -371,12 +588,13 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
]));
pipelines = [
- [{'$project': {date: {'$dateFromString': {dateString: "July 4th"}}}}],
- [{'$project': {date: {'$dateFromString': {dateString: "12:50:53"}}}}],
+ [{'$project': {date: {$dateFromString: {dateString: "July 4th"}}}}],
+ [{'$project': {date: {$dateFromString: {dateString: "12:50:53"}}}}],
];
pipelines.forEach(function(pipeline) {
- assertErrorCode(coll, pipeline, 40545, tojson(pipeline));
+ assertErrMsgContains(
+ coll, pipeline, 40545, "an incomplete date/time string has been found");
});
/* --------------------------------------------------------------------------------------- */
@@ -389,12 +607,12 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
]));
pipelines = [
- [{'$project': {date: {'$dateFromString': {dateString: "2017, 12:50:53"}}}}],
- [{'$project': {date: {'$dateFromString': {dateString: "60.Monday1770/06:59"}}}}],
+ [{'$project': {date: {$dateFromString: {dateString: "2017, 12:50:53"}}}}],
+ [{'$project': {date: {$dateFromString: {dateString: "60.Monday1770/06:59"}}}}],
];
pipelines.forEach(function(pipeline) {
- assertErrorCode(coll, pipeline, 40553, tojson(pipeline));
+ assertErrMsgContains(coll, pipeline, 40553, "Error parsing date string");
});
/* --------------------------------------------------------------------------------------- */
@@ -408,14 +626,11 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
]));
pipelines = [
- [{$project: {date: {'$dateFromString': {"dateString": "$tz"}}}}, {$sort: {_id: 1}}],
+ [{$project: {date: {$dateFromString: {dateString: "$tz"}}}}, {$sort: {_id: 1}}],
[
{
- $project: {
- date: {
- '$dateFromString': {"dateString": "2017-07-11T17:05:19Z", "timezone": "$tz"}
- }
- }
+ $project:
+ {date: {$dateFromString: {dateString: "2017-07-11T17:05:19Z", timezone: "$tz"}}}
},
{$sort: {_id: 1}}
],
@@ -426,17 +641,37 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
tojson(pipeline));
});
+ coll.drop();
+ assert.writeOK(coll.insert([
+ {_id: 0},
+ {_id: 1, format: null},
+ {_id: 2, format: undefined},
+ ]));
+
+ assert.eq(
+ [{_id: 0, date: null}, {_id: 1, date: null}, {_id: 2, date: null}],
+ coll.aggregate({
+ $project: {
+ date: {
+ $dateFromString: {dateString: "2017-07-11T17:05:19Z", format: "$format"}
+ }
+ }
+ })
+ .toArray());
+
/* --------------------------------------------------------------------------------------- */
/* Parse errors. */
- let pipeline = {$project: {date: {'$dateFromString': "no-object"}}};
- assertErrorCode(coll, pipeline, 40540);
+ let pipeline = [{$project: {date: {$dateFromString: "no-object"}}}];
+ assertErrMsgContains(
+ coll, pipeline, 40540, "$dateFromString only supports an object as an argument");
- pipeline = {$project: {date: {'$dateFromString': {"unknown": "$tz"}}}};
- assertErrorCode(coll, pipeline, 40541);
+ pipeline = [{$project: {date: {$dateFromString: {"unknown": "$tz"}}}}];
+ assertErrMsgContains(coll, pipeline, 40541, "Unrecognized argument");
- pipeline = {$project: {date: {'$dateFromString': {"dateString": 5}}}};
- assertErrorCode(coll, pipeline, 40543);
+ pipeline = [{$project: {date: {$dateFromString: {dateString: 5}}}}];
+ assertErrMsgContains(
+ coll, pipeline, 40543, "$dateFromString requires that 'dateString' be a string");
/* --------------------------------------------------------------------------------------- */
/* Passing in time zone with date/time string. */
@@ -444,7 +679,7 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
pipeline = {
$project: {
date: {
- '$dateFromString':
+ $dateFromString:
{dateString: "2017-07-12T22:23:55 GMT+02:00", timezone: "Europe/Amsterdam"}
}
}
@@ -454,7 +689,7 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
pipeline = {
$project: {
date: {
- '$dateFromString':
+ $dateFromString:
{dateString: "2017-07-12T22:23:55Z", timezone: "Europe/Amsterdam"}
}
}
@@ -464,7 +699,7 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
pipeline = {
$project: {
date: {
- '$dateFromString': {
+ $dateFromString: {
dateString: "2017-07-12T22:23:55 America/New_York",
timezone: "Europe/Amsterdam"
}
@@ -474,9 +709,76 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
assertErrorCode(coll, pipeline, 40553);
pipeline = {
- $project:
- {date: {'$dateFromString': {dateString: "2017-07-12T22:23:55 Europe/Amsterdam"}}}
+ $project: {date: {$dateFromString: {dateString: "2017-07-12T22:23:55 Europe/Amsterdam"}}}
};
assertErrorCode(coll, pipeline, 40553);
+ /* --------------------------------------------------------------------------------------- */
+ /* Error cases for $dateFromString with format specifier string. */
+
+ // Test umatched format specifier string.
+ pipeline = [{$project: {date: {$dateFromString: {dateString: "2018-01", format: "%Y-%m-%d"}}}}];
+ assertErrMsgContains(coll, pipeline, 40553, "Data missing");
+
+ pipeline = [{$project: {date: {$dateFromString: {dateString: "2018-01", format: "%Y"}}}}];
+ assertErrMsgContains(coll, pipeline, 40553, "Trailing data");
+
+ // Test missing specifier prefix '%'.
+ pipeline = [{$project: {date: {$dateFromString: {dateString: "1992-26-04", format: "Y-d-m"}}}}];
+ assertErrMsgContains(coll, pipeline, 40553, "Format literal not found");
+
+ pipeline = [{$project: {date: {$dateFromString: {dateString: "1992", format: "%n"}}}}];
+ assertErrMsgContains(coll, pipeline, 18536, "Invalid format character");
+
+ pipeline = [{
+ $project: {
+ date: {
+ $dateFromString:
+ {dateString: "4/26/1992:+0445", format: "%m/%d/%Y:%z", timezone: "+0500"}
+ }
+ }
+ }];
+ assertErrMsgContains(
+ coll,
+ pipeline,
+ 40554,
+ "you cannot pass in a date/time string with GMT offset together with a timezone argument");
+
+ pipeline = [{$project: {date: {$dateFromString: {dateString: "4/26/1992", format: 5}}}}];
+ assertErrMsgContains(
+ coll, pipeline, 40684, "$dateFromString requires that 'format' be a string");
+
+ pipeline = [{$project: {date: {$dateFromString: {dateString: "4/26/1992", format: {}}}}}];
+ assertErrMsgContains(
+ coll, pipeline, 40684, "$dateFromString requires that 'format' be a string");
+
+ pipeline =
+ [{$project: {date: {$dateFromString: {dateString: "ISO Day 6", format: "ISO Day %u"}}}}];
+ assertErrMsgContains(coll, pipeline, 40553, "The parsed date was invalid");
+
+ pipeline =
+ [{$project: {date: {$dateFromString: {dateString: "ISO Week 52", format: "ISO Week %V"}}}}];
+ assertErrMsgContains(coll, pipeline, 40553, "The parsed date was invalid");
+
+ pipeline = [{
+ $project: {
+ date: {$dateFromString: {dateString: "ISO Week 1, 2018", format: "ISO Week %V, %Y"}}
+ }
+ }];
+ assertErrMsgContains(
+ coll, pipeline, 40553, "Mixing of ISO dates with natural dates is not allowed");
+
+ pipeline =
+ [{$project: {date: {$dateFromString: {dateString: "12/31/2018", format: "%m/%d/%G"}}}}];
+ assertErrMsgContains(
+ coll, pipeline, 40553, "Mixing of ISO dates with natural dates is not allowed");
+
+ // Test embedded null bytes in the 'dateString' and 'format' fields.
+ pipeline =
+ [{$project: {date: {$dateFromString: {dateString: "12/31\0/2018", format: "%m/%d/%Y"}}}}];
+ assertErrMsgContains(coll, pipeline, 40553, "Data missing");
+
+ pipeline =
+ [{$project: {date: {$dateFromString: {dateString: "12/31/2018", format: "%m/%d\0/%Y"}}}}];
+ assertErrMsgContains(coll, pipeline, 40553, "Trailing data");
})();
diff --git a/jstests/aggregation/expressions/date_to_string.js b/jstests/aggregation/expressions/date_to_string.js
index f15d566fabd..2022edc459b 100644
--- a/jstests/aggregation/expressions/date_to_string.js
+++ b/jstests/aggregation/expressions/date_to_string.js
@@ -104,6 +104,34 @@ load("jstests/aggregation/extras/utils.js"); // For assertErrorCode
.toArray());
/* --------------------------------------------------------------------------------------- */
+ coll.drop();
+
+ assert.writeOK(coll.insert([
+ {_id: 0, date: new ISODate("2017-01-01T15:08:51.911Z")},
+ {_id: 1, date: new ISODate("2017-07-04T15:09:12.911Z")},
+ {_id: 2, date: new ISODate("2017-12-04T15:09:14.911Z")},
+ ]));
+
+ assert.eq(
+ [
+ {_id: 0, date: "Natural: 2017-W1-01, ISO: 2016-W7-52"},
+ {_id: 1, date: "Natural: 2017-W3-27, ISO: 2017-W2-27"},
+ {_id: 2, date: "Natural: 2017-W2-49, ISO: 2017-W1-49"},
+ ],
+ coll.aggregate([
+ {
+ $project: {
+ date: {
+ $dateToString:
+ {format: "Natural: %Y-W%w-%U, ISO: %G-W%u-%V", date: "$date"}
+ }
+ }
+ },
+ {$sort: {_id: 1}}
+ ])
+ .toArray());
+
+ /* --------------------------------------------------------------------------------------- */
/* Test that missing expressions, turn into BSON null values */
coll.drop();
diff --git a/jstests/aggregation/extras/utils.js b/jstests/aggregation/extras/utils.js
index d664aedb9f1..2c6151ffe73 100644
--- a/jstests/aggregation/extras/utils.js
+++ b/jstests/aggregation/extras/utils.js
@@ -277,3 +277,16 @@ function assertErrorCode(coll, pipe, code, errmsg) {
assert.eq(cursorRes.code, code, tojson(cursorRes));
}
}
+
+/**
+ * Assert that an aggregation fails with a specific code and the error message contains the given
+ * string.
+ */
+function assertErrMsgContains(coll, pipe, code, expectedMessage) {
+ const response = assert.commandFailedWithCode(
+ coll.getDB().runCommand({aggregate: coll.getName(), pipeline: pipe, cursor: {}}), code);
+ assert.neq(
+ -1,
+ response.errmsg.indexOf(expectedMessage),
+ "Error message did not contain '" + expectedMessage + "', found:\n" + tojson(response));
+}
diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp
index 3c3156b3435..40e499291ff 100644
--- a/src/mongo/db/pipeline/expression.cpp
+++ b/src/mongo/db/pipeline/expression.cpp
@@ -1299,12 +1299,15 @@ intrusive_ptr<Expression> ExpressionDateFromString::parse(
BSONElement dateStringElem;
BSONElement timeZoneElem;
+ BSONElement formatElem;
const BSONObj args = expr.embeddedObject();
for (auto&& arg : args) {
auto field = arg.fieldNameStringData();
- if (field == "dateString"_sd) {
+ if (field == "format"_sd) {
+ formatElem = arg;
+ } else if (field == "dateString"_sd) {
dateStringElem = arg;
} else if (field == "timezone"_sd) {
timeZoneElem = arg;
@@ -1317,25 +1320,33 @@ intrusive_ptr<Expression> ExpressionDateFromString::parse(
uassert(40542, "Missing 'dateString' parameter to $dateFromString", dateStringElem);
- return new ExpressionDateFromString(expCtx,
- parseOperand(expCtx, dateStringElem, vps),
- timeZoneElem ? parseOperand(expCtx, timeZoneElem, vps)
- : nullptr);
+ return new ExpressionDateFromString(
+ expCtx,
+ parseOperand(expCtx, dateStringElem, vps),
+ timeZoneElem ? parseOperand(expCtx, timeZoneElem, vps) : nullptr,
+ formatElem ? parseOperand(expCtx, formatElem, vps) : nullptr);
}
ExpressionDateFromString::ExpressionDateFromString(
const boost::intrusive_ptr<ExpressionContext>& expCtx,
intrusive_ptr<Expression> dateString,
- intrusive_ptr<Expression> timeZone)
- : Expression(expCtx), _dateString(std::move(dateString)), _timeZone(std::move(timeZone)) {}
+ intrusive_ptr<Expression> timeZone,
+ intrusive_ptr<Expression> format)
+ : Expression(expCtx),
+ _dateString(std::move(dateString)),
+ _timeZone(std::move(timeZone)),
+ _format(std::move(format)) {}
intrusive_ptr<Expression> ExpressionDateFromString::optimize() {
_dateString = _dateString->optimize();
if (_timeZone) {
_timeZone = _timeZone->optimize();
}
+ if (_format) {
+ _format = _format->optimize();
+ }
- if (ExpressionConstant::allNullOrConstant({_dateString, _timeZone})) {
+ if (ExpressionConstant::allNullOrConstant({_dateString, _timeZone, _format})) {
// Everything is a constant, so we can turn into a constant.
return ExpressionConstant::create(getExpressionContext(), evaluate(Document{}));
}
@@ -1346,7 +1357,8 @@ Value ExpressionDateFromString::serialize(bool explain) const {
return Value(
Document{{"$dateFromString",
Document{{"dateString", _dateString->serialize(explain)},
- {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()}}}});
+ {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()},
+ {"format", _format ? _format->serialize(explain) : Value()}}}});
}
Value ExpressionDateFromString::evaluate(const Document& root) const {
@@ -1366,6 +1378,27 @@ Value ExpressionDateFromString::evaluate(const Document& root) const {
dateString.getType() == BSONType::String);
const std::string& dateTimeString = dateString.getString();
+ if (_format) {
+ const Value format = _format->evaluate(root);
+
+ if (format.nullish()) {
+ return Value(BSONNULL);
+ }
+
+ uassert(40684,
+ str::stream() << "$dateFromString requires that 'format' be a string, found: "
+ << typeName(format.getType())
+ << " with value "
+ << format.toString(),
+ format.getType() == BSONType::String);
+ const std::string& formatString = format.getString();
+
+ TimeZone::validateFromStringFormat(formatString);
+
+ return Value(getExpressionContext()->timeZoneDatabase->fromString(
+ dateTimeString, timeZone, StringData(formatString)));
+ }
+
return Value(getExpressionContext()->timeZoneDatabase->fromString(dateTimeString, timeZone));
}
@@ -1374,6 +1407,9 @@ void ExpressionDateFromString::_doAddDependencies(DepsTracker* deps) const {
if (_timeZone) {
_timeZone->addDependencies(deps);
}
+ if (_format) {
+ _format->addDependencies(deps);
+ }
}
/* ---------------------- ExpressionDateToParts ----------------------- */
@@ -1560,7 +1596,7 @@ intrusive_ptr<Expression> ExpressionDateToString::parse(
const string format = formatElem.str();
- TimeZone::validateFormat(format);
+ TimeZone::validateToStringFormat(format);
return new ExpressionDateToString(expCtx,
format,
diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h
index 045614133d3..2dd3b1cc42c 100644
--- a/src/mongo/db/pipeline/expression.h
+++ b/src/mongo/db/pipeline/expression.h
@@ -873,10 +873,12 @@ protected:
private:
ExpressionDateFromString(const boost::intrusive_ptr<ExpressionContext>& expCtx,
boost::intrusive_ptr<Expression> dateString,
- boost::intrusive_ptr<Expression> timeZone);
+ boost::intrusive_ptr<Expression> timeZone,
+ boost::intrusive_ptr<Expression> format);
boost::intrusive_ptr<Expression> _dateString;
boost::intrusive_ptr<Expression> _timeZone;
+ boost::intrusive_ptr<Expression> _format;
};
class ExpressionDateFromParts final : public Expression {
diff --git a/src/mongo/db/pipeline/expression_test.cpp b/src/mongo/db/pipeline/expression_test.cpp
index b893229d6f0..6e41e9e75fc 100644
--- a/src/mongo/db/pipeline/expression_test.cpp
+++ b/src/mongo/db/pipeline/expression_test.cpp
@@ -5191,6 +5191,22 @@ TEST_F(ExpressionDateFromStringTest, SerializesToObjectSyntax) {
ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization);
ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization);
+
+ spec = BSON("$dateFromString" << BSON("dateString"
+ << "2017-07-04T13:06:44Z"
+ << "timezone"
+ << "Europe/London"
+ << "format"
+ << "%Y-%d-%mT%H:%M:%S"));
+ dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ expectedSerialization =
+ Value(Document{{"$dateFromString",
+ Document{{"dateString", Document{{"$const", "2017-07-04T13:06:44Z"_sd}}},
+ {"timezone", Document{{"$const", "Europe/London"_sd}}},
+ {"format", Document{{"$const", "%Y-%d-%mT%H:%M:%S"_sd}}}}}});
+
+ ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization);
+ ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization);
}
TEST_F(ExpressionDateFromStringTest, OptimizesToConstantIfAllInputsAreConstant) {
@@ -5212,6 +5228,16 @@ TEST_F(ExpressionDateFromStringTest, OptimizesToConstantIfAllInputsAreConstant)
dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get()));
+ // Test that it becomes a constant with the dateString, timezone, and format being a constant.
+ spec = BSON("$dateFromString" << BSON("dateString"
+ << "2017-07-04T13:09:57"
+ << "timezone"
+ << "Europe/London"
+ << "format"
+ << "%Y-%m-%dT%H:%M:%S"));
+ dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get()));
+
dateVal = Date_t::fromMillisSinceEpoch(1499170197000);
ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{}));
@@ -5228,6 +5254,16 @@ TEST_F(ExpressionDateFromStringTest, OptimizesToConstantIfAllInputsAreConstant)
<< "$tz"));
dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get()));
+
+ // Test that it does *not* become a constant if format is not a constant.
+ spec = BSON("$dateFromString" << BSON("dateString"
+ << "2017-07-04T13:09:57Z"
+ << "timezone"
+ << "Europe/London"
+ << "format"
+ << "$format"));
+ dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get()));
}
TEST_F(ExpressionDateFromStringTest, RejectsUnparsableString) {
@@ -5289,6 +5325,79 @@ TEST_F(ExpressionDateFromStringTest, RejectsTimeZoneInStringAndArgument) {
ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40554);
}
+TEST_F(ExpressionDateFromStringTest, RejectsNonStringFormat) {
+ auto expCtx = getExpCtx();
+
+ auto spec = BSON("$dateFromString" << BSON("dateString"
+ << "2017-07-13T10:02:57"
+ << "format"
+ << 2));
+ auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40684);
+
+ spec = BSON("$dateFromString" << BSON("dateString"
+ << "July 4, 2017"
+ << "format"
+ << true));
+ dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40684);
+}
+
+TEST_F(ExpressionDateFromStringTest, RejectsStringsThatDoNotMatchFormat) {
+ auto expCtx = getExpCtx();
+
+ auto spec = BSON("$dateFromString" << BSON("dateString"
+ << "2017-07"
+ << "format"
+ << "%Y-%m-%d"));
+ auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553);
+
+ spec = BSON("$dateFromString" << BSON("dateString"
+ << "2017-07"
+ << "format"
+ << "%m-%Y"));
+ dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_THROWS_CODE(dateExp->evaluate({}), AssertionException, 40553);
+}
+
+TEST_F(ExpressionDateFromStringTest, EscapeCharacterAllowsPrefixUsage) {
+ auto expCtx = getExpCtx();
+
+ auto spec = BSON("$dateFromString" << BSON("dateString"
+ << "2017 % 01 % 01"
+ << "format"
+ << "%Y %% %m %% %d"));
+ auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_EQ("2017-01-01T00:00:00.000Z", dateExp->evaluate(Document{}).toString());
+}
+
+
+TEST_F(ExpressionDateFromStringTest, EvaluatesToNullIfFormatIsNullish) {
+ auto expCtx = getExpCtx();
+
+ auto spec = BSON("$dateFromString" << BSON("dateString"
+ << "1/1/2017"
+ << "format"
+ << BSONNULL));
+ auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{}));
+
+ spec = BSON("$dateFromString" << BSON("dateString"
+ << "1/1/2017"
+ << "format"
+ << "$missing"));
+ dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{}));
+
+ spec = BSON("$dateFromString" << BSON("dateString"
+ << "1/1/2017"
+ << "format"
+ << BSONUndefined));
+ dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(Document{}));
+}
+
TEST_F(ExpressionDateFromStringTest, ReadWithUTCOffset) {
auto expCtx = getExpCtx();
@@ -5297,40 +5406,83 @@ TEST_F(ExpressionDateFromStringTest, ReadWithUTCOffset) {
<< "timezone"
<< "-01:00"));
auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
- auto dateVal = Date_t::fromMillisSinceEpoch(1501242472912);
- ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{}));
+ ASSERT_EQ("2017-07-28T11:47:52.912Z", dateExp->evaluate(Document{}).toString());
spec = BSON("$dateFromString" << BSON("dateString"
<< "2017-07-28T10:47:52.912"
<< "timezone"
<< "+01:00"));
dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
- dateVal = Date_t::fromMillisSinceEpoch(1501235272912);
- ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{}));
+ ASSERT_EQ("2017-07-28T09:47:52.912Z", dateExp->evaluate(Document{}).toString());
spec = BSON("$dateFromString" << BSON("dateString"
<< "2017-07-28T10:47:52.912"
<< "timezone"
<< "+0445"));
dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
- dateVal = Date_t::fromMillisSinceEpoch(1501221772912);
- ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{}));
+ ASSERT_EQ("2017-07-28T06:02:52.912Z", dateExp->evaluate(Document{}).toString());
spec = BSON("$dateFromString" << BSON("dateString"
<< "2017-07-28T10:47:52.912"
<< "timezone"
<< "+10:45"));
dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
- dateVal = Date_t::fromMillisSinceEpoch(1501200172912);
- ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{}));
+ ASSERT_EQ("2017-07-28T00:02:52.912Z", dateExp->evaluate(Document{}).toString());
spec = BSON("$dateFromString" << BSON("dateString"
<< "1945-07-28T10:47:52.912"
<< "timezone"
<< "-08:00"));
dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
- dateVal = Date_t::fromMillisSinceEpoch(-770879527088);
- ASSERT_VALUE_EQ(Value(dateVal), dateExp->evaluate(Document{}));
+ ASSERT_EQ("1945-07-28T18:47:52.912Z", dateExp->evaluate(Document{}).toString());
+}
+
+TEST_F(ExpressionDateFromStringTest, ConvertStringWithUTCOffsetAndFormat) {
+ auto expCtx = getExpCtx();
+
+ auto spec = BSON("$dateFromString" << BSON("dateString"
+ << "10:47:52.912 on 7/28/2017"
+ << "timezone"
+ << "-01:00"
+ << "format"
+ << "%H:%M:%S.%L on %m/%d/%Y"));
+ auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_EQ("2017-07-28T11:47:52.912Z", dateExp->evaluate(Document{}).toString());
+
+ spec = BSON("$dateFromString" << BSON("dateString"
+ << "10:47:52.912 on 7/28/2017"
+ << "timezone"
+ << "+01:00"
+ << "format"
+ << "%H:%M:%S.%L on %m/%d/%Y"));
+ dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_EQ("2017-07-28T09:47:52.912Z", dateExp->evaluate(Document{}).toString());
+}
+
+TEST_F(ExpressionDateFromStringTest, ConvertStringWithISODateFormat) {
+ auto expCtx = getExpCtx();
+
+ auto spec = BSON("$dateFromString" << BSON("dateString"
+ << "Day 7 Week 53 Year 2017"
+ << "format"
+ << "Day %u Week %V Year %G"));
+ auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_EQ("2018-01-07T00:00:00.000Z", dateExp->evaluate(Document{}).toString());
+
+ // Week and day of week default to '1' if not specified.
+ spec = BSON("$dateFromString" << BSON("dateString"
+ << "Week 53 Year 2017"
+ << "format"
+ << "Week %V Year %G"));
+ dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_EQ("2018-01-01T00:00:00.000Z", dateExp->evaluate(Document{}).toString());
+
+ spec = BSON("$dateFromString" << BSON("dateString"
+ << "Day 7 Year 2017"
+ << "format"
+ << "Day %u Year %G"));
+ dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState);
+ ASSERT_EQ("2017-01-08T00:00:00.000Z", dateExp->evaluate(Document{}).toString());
}
} // namespace ExpressionDateFromStringTest
diff --git a/src/mongo/db/query/datetime/date_time_support.cpp b/src/mongo/db/query/datetime/date_time_support.cpp
index 2b6cb8ec078..c41df5b28d6 100644
--- a/src/mongo/db/query/datetime/date_time_support.cpp
+++ b/src/mongo/db/query/datetime/date_time_support.cpp
@@ -65,6 +65,67 @@ long long seconds(Date_t date) {
return durationCount<Seconds>(Milliseconds(millis));
}
+//
+// Format specifier map when parsing a date from a string with a required format.
+//
+const std::vector<timelib_format_specifier> kDateFromStringFormatMap = {
+ {'d', TIMELIB_FORMAT_DAY_TWO_DIGIT},
+ {'G', TIMELIB_FORMAT_YEAR_ISO},
+ {'H', TIMELIB_FORMAT_HOUR_TWO_DIGIT_24_MAX},
+ {'L', TIMELIB_FORMAT_MILLISECOND_THREE_DIGIT},
+ {'m', TIMELIB_FORMAT_MONTH_TWO_DIGIT},
+ {'M', TIMELIB_FORMAT_MINUTE_TWO_DIGIT},
+ {'S', TIMELIB_FORMAT_SECOND_TWO_DIGIT},
+ {'u', TIMELIB_FORMAT_DAY_OF_WEEK_ISO},
+ {'V', TIMELIB_FORMAT_WEEK_OF_YEAR_ISO},
+ {'Y', TIMELIB_FORMAT_YEAR_FOUR_DIGIT},
+ {'z', TIMELIB_FORMAT_TIMEZONE_OFFSET},
+ {'Z', TIMELIB_FORMAT_TIMEZONE_OFFSET_MINUTES},
+ {'\0', TIMELIB_FORMAT_END}};
+
+//
+// Format specifier map when converting a date to a string.
+//
+const std::vector<timelib_format_specifier> kDateToStringFormatMap = {
+ {'d', TIMELIB_FORMAT_DAY_TWO_DIGIT},
+ {'G', TIMELIB_FORMAT_YEAR_ISO},
+ {'H', TIMELIB_FORMAT_HOUR_TWO_DIGIT_24_MAX},
+ {'j', TIMELIB_FORMAT_DAY_OF_YEAR},
+ {'L', TIMELIB_FORMAT_MILLISECOND_THREE_DIGIT},
+ {'m', TIMELIB_FORMAT_MONTH_TWO_DIGIT},
+ {'M', TIMELIB_FORMAT_MINUTE_TWO_DIGIT},
+ {'S', TIMELIB_FORMAT_SECOND_TWO_DIGIT},
+ {'w', TIMELIB_FORMAT_DAY_OF_WEEK},
+ {'u', TIMELIB_FORMAT_DAY_OF_WEEK_ISO},
+ {'U', TIMELIB_FORMAT_WEEK_OF_YEAR},
+ {'V', TIMELIB_FORMAT_WEEK_OF_YEAR_ISO},
+ {'Y', TIMELIB_FORMAT_YEAR_FOUR_DIGIT},
+ {'z', TIMELIB_FORMAT_TIMEZONE_OFFSET},
+ {'Z', TIMELIB_FORMAT_TIMEZONE_OFFSET_MINUTES}};
+
+
+// Verifies that any '%' is followed by a valid format character as indicated by 'allowedFormats',
+// and that the 'format' string ends with an even number of '%' symbols.
+void validateFormat(StringData format,
+ const std::vector<timelib_format_specifier>& allowedFormats) {
+ for (auto it = format.begin(); it != format.end(); ++it) {
+ if (*it != '%') {
+ continue;
+ }
+
+ ++it; // next character must be format modifier
+ uassert(18535, "Unmatched '%' at end of format string", it != format.end());
+
+ const bool validSpecifier = (*it == '%') ||
+ std::find_if(allowedFormats.begin(), allowedFormats.end(), [=](const auto& format) {
+ return format.specifier == *it;
+ }) != allowedFormats.end();
+ uassert(18536,
+ str::stream() << "Invalid format character '%" << *it << "' in format string",
+ validSpecifier);
+ }
+}
+
} // namespace
const TimeZoneDatabase* TimeZoneDatabase::get(ServiceContext* serviceContext) {
@@ -133,17 +194,37 @@ static timelib_tzinfo* timezonedatabase_gettzinfowrapper(char* tz_id,
return nullptr;
}
-Date_t TimeZoneDatabase::fromString(StringData dateString, boost::optional<TimeZone> tz) const {
+Date_t TimeZoneDatabase::fromString(StringData dateString,
+ boost::optional<TimeZone> tz,
+ boost::optional<StringData> format) const {
std::unique_ptr<timelib_error_container, TimeZoneDatabase::TimelibErrorContainerDeleter>
errors{};
timelib_error_container* rawErrors;
- std::unique_ptr<timelib_time, TimeZone::TimelibTimeDeleter> parsedTime(
- timelib_strtotime(const_cast<char*>(dateString.toString().c_str()),
- dateString.size(),
- &rawErrors,
- _timeZoneDatabase.get(),
- timezonedatabase_gettzinfowrapper));
+ timelib_time* rawTime;
+ if (!format) {
+ // Without a format, timelib will attempt to parse a string as best as it can, accepting a
+ // variety of formats.
+ rawTime = timelib_strtotime(const_cast<char*>(dateString.rawData()),
+ dateString.size(),
+ &rawErrors,
+ _timeZoneDatabase.get(),
+ timezonedatabase_gettzinfowrapper);
+ } else {
+ const timelib_format_config dateFormatConfig = {
+ &kDateFromStringFormatMap[0],
+ // Format specifiers must be prefixed by '%'.
+ '%'};
+ rawTime = timelib_parse_from_format_with_map(const_cast<char*>(format->rawData()),
+ const_cast<char*>(dateString.rawData()),
+ dateString.size(),
+ &rawErrors,
+ _timeZoneDatabase.get(),
+ timezonedatabase_gettzinfowrapper,
+ &dateFormatConfig);
+ }
+ std::unique_ptr<timelib_time, TimeZone::TimelibTimeDeleter> parsedTime(rawTime);
+
errors.reset(rawErrors);
// If the parsed string has a warning or error, throw an error.
@@ -458,41 +539,12 @@ Seconds TimeZone::utcOffset(Date_t date) const {
return Seconds(time->z);
}
-void TimeZone::validateFormat(StringData format) {
- for (auto it = format.begin(); it != format.end(); ++it) {
- if (*it != '%') {
- continue;
- }
+void TimeZone::validateToStringFormat(StringData format) {
+ return validateFormat(format, kDateToStringFormatMap);
+}
- ++it; // next character must be format modifier
- uassert(18535, "Unmatched '%' at end of $dateToString format string", it != format.end());
-
-
- switch (*it) {
- // all of these fall through intentionally
- case '%':
- case 'Y':
- case 'm':
- case 'd':
- case 'H':
- case 'M':
- case 'S':
- case 'L':
- case 'j':
- case 'w':
- case 'U':
- case 'G':
- case 'V':
- case 'u':
- case 'z':
- case 'Z':
- break;
- default:
- uasserted(18536,
- str::stream() << "Invalid format character '%" << *it
- << "' in $dateToString format string");
- }
- }
+void TimeZone::validateFromStringFormat(StringData format) {
+ return validateFormat(format, kDateFromStringFormatMap);
}
std::string TimeZone::formatDate(StringData format, Date_t date) const {
diff --git a/src/mongo/db/query/datetime/date_time_support.h b/src/mongo/db/query/datetime/date_time_support.h
index 9d5b3188abc..36125268532 100644
--- a/src/mongo/db/query/datetime/date_time_support.h
+++ b/src/mongo/db/query/datetime/date_time_support.h
@@ -271,9 +271,10 @@ public:
/**
* Verifies that any '%' is followed by a valid format character, and that 'format' string
- * ends with an even number of '%' symbols
+ * ends with an even number of '%' symbols.
*/
- static void validateFormat(StringData format);
+ static void validateToStringFormat(StringData format);
+ static void validateFromStringFormat(StringData format);
private:
std::unique_ptr<_timelib_time, TimelibTimeDeleter> getTimelibTime(Date_t) const;
@@ -358,11 +359,15 @@ public:
std::unique_ptr<TimeZoneDatabase> timeZoneDatabase);
/**
- * Constructs a Date_t from a string description of a date.
+ * Constructs a Date_t from a string description of a date, with an optional format specifier
+ * string.
*
* 'dateString' may contain time zone information if the information is simply an offset from
* UTC, in which case the returned Date_t will be adjusted accordingly.
*
+ * The supported format specifiers for the 'format' string are listed in
+ * kDateFromStringFormatMap.
+ *
* Throws a AssertionException if any of the following occur:
* * The string cannot be parsed into a date.
* * The string specifies a time zone that is not simply an offset from UTC, like
@@ -371,8 +376,11 @@ public:
* string '2017-07-04T00:00:00Z'.
* * 'tz' is provided, but 'dateString' specifies an offset from UTC, like '-0400'
* in the string '2017-07-04 -0400'.
+ * * The string does not match the 'format' specifier.
*/
- Date_t fromString(StringData dateString, boost::optional<TimeZone> tz) const;
+ Date_t fromString(StringData dateString,
+ boost::optional<TimeZone> tz,
+ boost::optional<StringData> format = boost::none) const;
/**
* Returns a TimeZone object representing the UTC time zone.
diff --git a/src/mongo/db/query/datetime/date_time_support_test.cpp b/src/mongo/db/query/datetime/date_time_support_test.cpp
index 96d1327c773..aeccf4030fa 100644
--- a/src/mongo/db/query/datetime/date_time_support_test.cpp
+++ b/src/mongo/db/query/datetime/date_time_support_test.cpp
@@ -37,6 +37,7 @@ namespace mongo {
namespace {
const TimeZoneDatabase kDefaultTimeZoneDatabase{};
+const TimeZone kDefaultTimeZone = TimeZoneDatabase::utcZone();
TEST(GetTimeZone, DoesReturnKnownTimeZone) {
// Just asserting that these do not throw exceptions.
@@ -939,15 +940,28 @@ TEST(NewYorkTimeAfterEpoch, DoesOutputFormatDate) {
}
TEST(DateFormat, ThrowsUserExceptionIfGivenUnrecognizedFormatter) {
- ASSERT_THROWS_CODE(TimeZoneDatabase::utcZone().validateFormat("%x"), AssertionException, 18536);
+ ASSERT_THROWS_CODE(
+ TimeZoneDatabase::utcZone().validateToStringFormat("%x"), AssertionException, 18536);
+ ASSERT_THROWS_CODE(
+ TimeZoneDatabase::utcZone().validateFromStringFormat("%x"), AssertionException, 18536);
}
TEST(DateFormat, ThrowsUserExceptionIfGivenUnmatchedPercent) {
- ASSERT_THROWS_CODE(TimeZoneDatabase::utcZone().validateFormat("%"), AssertionException, 18535);
ASSERT_THROWS_CODE(
- TimeZoneDatabase::utcZone().validateFormat("%%%"), AssertionException, 18535);
+ TimeZoneDatabase::utcZone().validateToStringFormat("%"), AssertionException, 18535);
+ ASSERT_THROWS_CODE(
+ TimeZoneDatabase::utcZone().validateToStringFormat("%%%"), AssertionException, 18535);
+ ASSERT_THROWS_CODE(
+ TimeZoneDatabase::utcZone().validateToStringFormat("blahblah%"), AssertionException, 18535);
+
+ // Repeat the tests with the format map for $dateFromString.
+ ASSERT_THROWS_CODE(
+ TimeZoneDatabase::utcZone().validateFromStringFormat("%"), AssertionException, 18535);
ASSERT_THROWS_CODE(
- TimeZoneDatabase::utcZone().validateFormat("blahblah%"), AssertionException, 18535);
+ TimeZoneDatabase::utcZone().validateFromStringFormat("%%%"), AssertionException, 18535);
+ ASSERT_THROWS_CODE(TimeZoneDatabase::utcZone().validateFromStringFormat("blahblah%"),
+ AssertionException,
+ 18535);
}
TEST(DateFormat, ThrowsUserExceptionIfGivenDateBeforeYear0) {
@@ -976,5 +990,113 @@ TEST(DateFormat, ThrowsUserExceptionIfGivenDateAfterYear9999) {
TimeZoneDatabase::utcZone().formatDate("%G", Date_t::max()), AssertionException, 18537);
}
+TEST(DateFromString, CorrectlyParsesStringThatMatchesFormat) {
+ auto input = "2017-07-04T10:56:02Z";
+ auto format = "%Y-%m-%dT%H:%M:%SZ"_sd;
+ auto date = kDefaultTimeZoneDatabase.fromString(input, kDefaultTimeZone, format);
+ ASSERT_EQ(TimeZoneDatabase::utcZone().formatDate(format, date), input);
+}
+
+TEST(DateFromString, RejectsStringWithInvalidYearFormat) {
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("201", kDefaultTimeZone, "%Y"_sd),
+ AssertionException,
+ 40545);
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("20i7", kDefaultTimeZone, "%Y"_sd),
+ AssertionException,
+ 40553);
+}
+
+TEST(DateFromString, RejectsStringWithInvalidMinuteFormat) {
+ // Minute must be 2 digits with leading zero.
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString(
+ "2017-01-01T00:1:00", kDefaultTimeZone, "%Y-%m-%dT%H%M%S"_sd),
+ AssertionException,
+ 40553);
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString(
+ "2017-01-01T00:0i:00", kDefaultTimeZone, "%Y-%m-%dT%H%M%S"_sd),
+ AssertionException,
+ 40553);
+}
+
+TEST(DateFromString, RejectsStringWithInvalidSecondsFormat) {
+ // Seconds must be 2 digits with leading zero.
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString(
+ "2017-01-01T00:00:1", kDefaultTimeZone, "%Y-%m-%dT%H%M%S"_sd),
+ AssertionException,
+ 40553);
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString(
+ "2017-01-01T00:00:i0", kDefaultTimeZone, "%Y-%m-%dT%H%M%S"_sd),
+ AssertionException,
+ 40553);
+}
+
+TEST(DateFromString, RejectsStringWithInvalidMillisecondsFormat) {
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString(
+ "2017-01-01T00:00:00.i", kDefaultTimeZone, "%Y-%m-%dT%H:%M:%S.%L"_sd),
+ AssertionException,
+ 40553);
+}
+
+TEST(DateFromString, RejectsStringWithInvalidISOYear) {
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("20i7", kDefaultTimeZone, "%G"_sd),
+ AssertionException,
+ 40553);
+}
+
+TEST(DateFromString, RejectsStringWithInvalidISOWeekOfYear) {
+ // ISO week of year must be between 1 and 53.
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-55", kDefaultTimeZone, "%G-%V"_sd),
+ AssertionException,
+ 40553);
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-FF", kDefaultTimeZone, "%G-%V"_sd),
+ AssertionException,
+ 40553);
+}
+
+TEST(DateFromString, RejectsStringWithInvalidISODayOfWeek) {
+ // Day of week must be single digit between 1 and 7.
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-8", kDefaultTimeZone, "%G-%u"_sd),
+ AssertionException,
+ 40553);
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-0", kDefaultTimeZone, "%G-%u"_sd),
+ AssertionException,
+ 40553);
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-a", kDefaultTimeZone, "%G-%u"_sd),
+ AssertionException,
+ 40553);
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("2017-11", kDefaultTimeZone, "%G-%u"_sd),
+ AssertionException,
+ 40553);
+ ASSERT_THROWS_CODE(
+ kDefaultTimeZoneDatabase.fromString("2017-123", kDefaultTimeZone, "%G-%u"_sd),
+ AssertionException,
+ 40553);
+}
+
+TEST(DateFromString, RejectsStringWithInvalidTimezoneOffset) {
+ // Timezone offset minutes (%Z) requires format +/-mmm.
+ ASSERT_THROWS_CODE(
+ kDefaultTimeZoneDatabase.fromString("2017 500", kDefaultTimeZone, "%G %Z"_sd),
+ AssertionException,
+ 40553);
+ ASSERT_THROWS_CODE(
+ kDefaultTimeZoneDatabase.fromString("2017 0500", kDefaultTimeZone, "%G %Z"_sd),
+ AssertionException,
+ 40553);
+ ASSERT_THROWS_CODE(
+ kDefaultTimeZoneDatabase.fromString("2017 +i00", kDefaultTimeZone, "%G %Z"_sd),
+ AssertionException,
+ 40553);
+}
+
+TEST(DateFromString, EmptyFormatStringThrowsForAllInputs) {
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("1/1/2017", kDefaultTimeZone, ""_sd),
+ AssertionException,
+ 40553);
+ ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.fromString("", kDefaultTimeZone, ""_sd),
+ AssertionException,
+ 40545);
+}
+
} // namespace
} // namespace mongo