summaryrefslogtreecommitdiff
path: root/src/mongo
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 /src/mongo
parent0f2cc83cdb0320563f6de507885e9c7b17313fa7 (diff)
downloadmongo-b721d0f20ad3dc1b1e9c20c63592c7da15846935.tar.gz
SERVER-32771: Add format specifier for $dateFromString expression
Diffstat (limited to 'src/mongo')
-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
6 files changed, 442 insertions, 70 deletions
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