diff options
Diffstat (limited to 'src/mongo')
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 56 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.h | 4 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_test.cpp | 172 | ||||
-rw-r--r-- | src/mongo/db/query/datetime/date_time_support.cpp | 134 | ||||
-rw-r--r-- | src/mongo/db/query/datetime/date_time_support.h | 16 | ||||
-rw-r--r-- | src/mongo/db/query/datetime/date_time_support_test.cpp | 130 |
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 |