diff options
author | Charlie Swanson <charlie.swanson@mongodb.com> | 2017-07-07 10:28:54 -0400 |
---|---|---|
committer | Charlie Swanson <charlie.swanson@mongodb.com> | 2017-07-07 10:28:54 -0400 |
commit | 56c8b7660fd85c02b5a8ade15e1dfdb49cbfea46 (patch) | |
tree | 1b5251064b4b2cb913a4ba045697e0145887374a /src/mongo/db | |
parent | 2b198988a626df6f36c9e606b08b266c462cceb4 (diff) | |
download | mongo-56c8b7660fd85c02b5a8ade15e1dfdb49cbfea46.tar.gz |
ERVER-28611 Add timezone argument for date expressions
Diffstat (limited to 'src/mongo/db')
-rw-r--r-- | src/mongo/db/pipeline/expression.cpp | 207 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression.h | 346 | ||||
-rw-r--r-- | src/mongo/db/pipeline/expression_test.cpp | 308 | ||||
-rw-r--r-- | src/mongo/db/query/datetime/date_time_support_test.cpp | 423 |
4 files changed, 1026 insertions, 258 deletions
diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index 223e0db6ff0..798d05ca285 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -207,6 +207,22 @@ size_t getCodePointLength(char charByte) { } } // namespace +/* ------------------------- Register Date Expressions ----------------------------- */ + +REGISTER_EXPRESSION(dayOfMonth, ExpressionDayOfMonth::parse); +REGISTER_EXPRESSION(dayOfWeek, ExpressionDayOfWeek::parse); +REGISTER_EXPRESSION(dayOfYear, ExpressionDayOfYear::parse); +REGISTER_EXPRESSION(hour, ExpressionHour::parse); +REGISTER_EXPRESSION(isoDayOfWeek, ExpressionIsoDayOfWeek::parse); +REGISTER_EXPRESSION(isoWeek, ExpressionIsoWeek::parse); +REGISTER_EXPRESSION(isoWeekYear, ExpressionIsoWeekYear::parse); +REGISTER_EXPRESSION(millisecond, ExpressionMillisecond::parse); +REGISTER_EXPRESSION(minute, ExpressionMinute::parse); +REGISTER_EXPRESSION(month, ExpressionMonth::parse); +REGISTER_EXPRESSION(second, ExpressionSecond::parse); +REGISTER_EXPRESSION(week, ExpressionWeek::parse); +REGISTER_EXPRESSION(year, ExpressionYear::parse); + /* ----------------------- ExpressionAbs ---------------------------- */ Value ExpressionAbs::evaluateNumericArg(const Value& numericArg) const { @@ -920,14 +936,14 @@ intrusive_ptr<Expression> ExpressionConstant::parse( intrusive_ptr<ExpressionConstant> ExpressionConstant::create( - const intrusive_ptr<ExpressionContext>& expCtx, const Value& pValue) { - intrusive_ptr<ExpressionConstant> pEC(new ExpressionConstant(expCtx, pValue)); + const intrusive_ptr<ExpressionContext>& expCtx, const Value& value) { + intrusive_ptr<ExpressionConstant> pEC(new ExpressionConstant(expCtx, value)); return pEC; } ExpressionConstant::ExpressionConstant(const boost::intrusive_ptr<ExpressionContext>& expCtx, - const Value& pTheValue) - : Expression(expCtx), pValue(pTheValue) {} + const Value& value) + : Expression(expCtx), _value(value) {} intrusive_ptr<Expression> ExpressionConstant::optimize() { @@ -940,11 +956,11 @@ void ExpressionConstant::addDependencies(DepsTracker* deps) const { } Value ExpressionConstant::evaluate(const Document& root) const { - return pValue; + return _value; } Value ExpressionConstant::serialize(bool explain) const { - return serializeConstant(pValue); + return serializeConstant(_value); } REGISTER_EXPRESSION(const, ExpressionConstant::parse); @@ -959,10 +975,6 @@ const char* ExpressionConstant::getOpName() const { namespace { -bool isNullOrConstant(intrusive_ptr<Expression> exp) { - return !exp || dynamic_cast<ExpressionConstant*>(exp.get()); -} - boost::optional<TimeZone> makeTimeZone(const TimeZoneDatabase* tzdb, const Document& root, intrusive_ptr<Expression> _timeZone) { @@ -1130,11 +1142,17 @@ intrusive_ptr<Expression> ExpressionDateFromParts::optimize() { _timeZone = _timeZone->optimize(); } - if (isNullOrConstant(_year) && isNullOrConstant(_month) && isNullOrConstant(_day) && - isNullOrConstant(_hour) && isNullOrConstant(_minute) && isNullOrConstant(_second) && - isNullOrConstant(_milliseconds) && isNullOrConstant(_isoYear) && - isNullOrConstant(_isoWeekYear) && isNullOrConstant(_isoDayOfWeek) && - isNullOrConstant(_timeZone)) { + if (ExpressionConstant::allNullOrConstant({_year, + _month, + _day, + _hour, + _minute, + _second, + _milliseconds, + _isoYear, + _isoWeekYear, + _isoDayOfWeek, + _timeZone})) { // Everything is a constant, so we can turn into a constant. return ExpressionConstant::create(getExpressionContext(), evaluate(Document{})); } @@ -1353,8 +1371,7 @@ intrusive_ptr<Expression> ExpressionDateToParts::optimize() { _iso8601 = _iso8601->optimize(); } - if (dynamic_cast<ExpressionConstant*>(_date.get()) && isNullOrConstant(_iso8601) && - isNullOrConstant(_timeZone)) { + if (ExpressionConstant::allNullOrConstant({_date, _iso8601, _timeZone})) { // Everything is a constant, so we can turn into a constant. return ExpressionConstant::create(getExpressionContext(), evaluate(Document{})); } @@ -1511,42 +1528,6 @@ void ExpressionDateToString::addDependencies(DepsTracker* deps) const { _date->addDependencies(deps); } -/* ---------------------- ExpressionDayOfMonth ------------------------- */ - -Value ExpressionDayOfMonth::evaluate(const Document& root) const { - Value date(vpOperand[0]->evaluate(root)); - return Value(TimeZoneDatabase::utcZone().dateParts(date.coerceToDate()).dayOfMonth); -} - -REGISTER_EXPRESSION(dayOfMonth, ExpressionDayOfMonth::parse); -const char* ExpressionDayOfMonth::getOpName() const { - return "$dayOfMonth"; -} - -/* ------------------------- ExpressionDayOfWeek ----------------------------- */ - -Value ExpressionDayOfWeek::evaluate(const Document& root) const { - Value date(vpOperand[0]->evaluate(root)); - return Value(TimeZoneDatabase::utcZone().dayOfWeek(date.coerceToDate())); -} - -REGISTER_EXPRESSION(dayOfWeek, ExpressionDayOfWeek::parse); -const char* ExpressionDayOfWeek::getOpName() const { - return "$dayOfWeek"; -} - -/* ------------------------- ExpressionDayOfYear ----------------------------- */ - -Value ExpressionDayOfYear::evaluate(const Document& root) const { - Value date(vpOperand[0]->evaluate(root)); - return Value(TimeZoneDatabase::utcZone().dayOfYear(date.coerceToDate())); -} - -REGISTER_EXPRESSION(dayOfYear, ExpressionDayOfYear::parse); -const char* ExpressionDayOfYear::getOpName() const { - return "$dayOfYear"; -} - /* ----------------------- ExpressionDivide ---------------------------- */ Value ExpressionDivide::evaluate(const Document& root) const { @@ -2249,30 +2230,6 @@ void ExpressionMeta::addDependencies(DepsTracker* deps) const { } } -/* ------------------------- ExpressionMillisecond ----------------------------- */ - -Value ExpressionMillisecond::evaluate(const Document& root) const { - auto date = vpOperand[0]->evaluate(root).coerceToDate(); - return Value(TimeZoneDatabase::utcZone().dateParts(date).millisecond); -} - -REGISTER_EXPRESSION(millisecond, ExpressionMillisecond::parse); -const char* ExpressionMillisecond::getOpName() const { - return "$millisecond"; -} - -/* ------------------------- ExpressionMinute -------------------------- */ - -Value ExpressionMinute::evaluate(const Document& root) const { - Value date(vpOperand[0]->evaluate(root)); - return Value(TimeZoneDatabase::utcZone().dateParts(date.coerceToDate()).minute); -} - -REGISTER_EXPRESSION(minute, ExpressionMinute::parse); -const char* ExpressionMinute::getOpName() const { - return "$minute"; -} - /* ----------------------- ExpressionMod ---------------------------- */ Value ExpressionMod::evaluate(const Document& root) const { @@ -2330,18 +2287,6 @@ const char* ExpressionMod::getOpName() const { return "$mod"; } -/* ------------------------ ExpressionMonth ----------------------------- */ - -Value ExpressionMonth::evaluate(const Document& root) const { - auto date = vpOperand[0]->evaluate(root).coerceToDate(); - return Value(TimeZoneDatabase::utcZone().dateParts(date).month); -} - -REGISTER_EXPRESSION(month, ExpressionMonth::parse); -const char* ExpressionMonth::getOpName() const { - return "$month"; -} - /* ------------------------- ExpressionMultiply ----------------------------- */ Value ExpressionMultiply::evaluate(const Document& root) const { @@ -2405,18 +2350,6 @@ const char* ExpressionMultiply::getOpName() const { return "$multiply"; } -/* ------------------------- ExpressionHour ----------------------------- */ - -Value ExpressionHour::evaluate(const Document& root) const { - auto date = vpOperand[0]->evaluate(root).coerceToDate(); - return Value(TimeZoneDatabase::utcZone().dateParts(date).hour); -} - -REGISTER_EXPRESSION(hour, ExpressionHour::parse); -const char* ExpressionHour::getOpName() const { - return "$hour"; -} - /* ----------------------- ExpressionIfNull ---------------------------- */ Value ExpressionIfNull::evaluate(const Document& root) const { @@ -3306,18 +3239,6 @@ const char* ExpressionReverseArray::getOpName() const { return "$reverseArray"; } -/* ------------------------- ExpressionSecond ----------------------------- */ - -Value ExpressionSecond::evaluate(const Document& root) const { - auto date = vpOperand[0]->evaluate(root).coerceToDate(); - return Value(TimeZoneDatabase::utcZone().dateParts(date).second); -} - -REGISTER_EXPRESSION(second, ExpressionSecond::parse); -const char* ExpressionSecond::getOpName() const { - return "$second"; -} - namespace { ValueSet arrayToSet(const Value& val, const ValueComparator& valueComparator) { const vector<Value>& array = val.getArray(); @@ -4219,66 +4140,6 @@ const char* ExpressionType::getOpName() const { return "$type"; } -/* ------------------------- ExpressionWeek ----------------------------- */ - -Value ExpressionWeek::evaluate(const Document& root) const { - auto date = vpOperand[0]->evaluate(root).coerceToDate(); - return Value(TimeZoneDatabase::utcZone().week(date)); -} - -REGISTER_EXPRESSION(week, ExpressionWeek::parse); -const char* ExpressionWeek::getOpName() const { - return "$week"; -} - -/* ------------------------- ExpressionIsoDayOfWeek --------------------- */ - -Value ExpressionIsoDayOfWeek::evaluate(const Document& root) const { - Value date(vpOperand[0]->evaluate(root)); - return Value(TimeZoneDatabase::utcZone().isoDayOfWeek(date.coerceToDate())); -} - -REGISTER_EXPRESSION(isoDayOfWeek, ExpressionIsoDayOfWeek::parse); -const char* ExpressionIsoDayOfWeek::getOpName() const { - return "$isoDayOfWeek"; -} - -/* ------------------------- ExpressionIsoWeekYear ---------------------- */ - -Value ExpressionIsoWeekYear::evaluate(const Document& root) const { - Value date(vpOperand[0]->evaluate(root)); - return Value(TimeZoneDatabase::utcZone().isoYear(date.coerceToDate())); -} - -REGISTER_EXPRESSION(isoWeekYear, ExpressionIsoWeekYear::parse); -const char* ExpressionIsoWeekYear::getOpName() const { - return "$isoWeekYear"; -} - -/* ------------------------- ExpressionIsoWeek -------------------------- */ - -Value ExpressionIsoWeek::evaluate(const Document& root) const { - Value date(vpOperand[0]->evaluate(root)); - return Value(TimeZoneDatabase::utcZone().isoWeek(date.coerceToDate())); -} - -REGISTER_EXPRESSION(isoWeek, ExpressionIsoWeek::parse); -const char* ExpressionIsoWeek::getOpName() const { - return "$isoWeek"; -} - -/* ------------------------- ExpressionYear ----------------------------- */ - -Value ExpressionYear::evaluate(const Document& root) const { - Value date(vpOperand[0]->evaluate(root)); - return Value(TimeZoneDatabase::utcZone().dateParts(date.coerceToDate()).year); -} - -REGISTER_EXPRESSION(year, ExpressionYear::parse); -const char* ExpressionYear::getOpName() const { - return "$year"; -} - /* -------------------------- ExpressionZip ------------------------------ */ REGISTER_EXPRESSION(zip, ExpressionZip::parse); diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h index 85451aeefcf..e14e451964d 100644 --- a/src/mongo/db/pipeline/expression.h +++ b/src/mongo/db/pipeline/expression.h @@ -30,6 +30,7 @@ #include "mongo/platform/basic.h" +#include <algorithm> #include <boost/intrusive_ptr.hpp> #include <map> #include <string> @@ -411,6 +412,200 @@ public: virtual Value evaluateNumericArg(const Value& numericArg) const = 0; }; +/** + * A constant expression. Repeated calls to evaluate() will always return the same thing. + */ +class ExpressionConstant final : public Expression { +public: + boost::intrusive_ptr<Expression> optimize() final; + void addDependencies(DepsTracker* deps) const final; + Value evaluate(const Document& root) const final; + Value serialize(bool explain) const final; + + const char* getOpName() const; + + /** + * Creates a new ExpressionConstant with value 'value'. + */ + static boost::intrusive_ptr<ExpressionConstant> create( + const boost::intrusive_ptr<ExpressionContext>& expCtx, const Value& value); + + static boost::intrusive_ptr<Expression> parse( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + BSONElement bsonExpr, + const VariablesParseState& vps); + + /** + * Returns true if 'expression' is nullptr or if 'expression' is an instance of an + * ExpressionConstant. + */ + static bool isNullOrConstant(boost::intrusive_ptr<Expression> expression) { + return !expression || dynamic_cast<ExpressionConstant*>(expression.get()); + } + + /** + * Returns true if every expression in 'expressions' is either a nullptr or an instance of an + * ExpressionConstant. + */ + static bool allNullOrConstant( + const std::initializer_list<boost::intrusive_ptr<Expression>>& expressions) { + return std::all_of(expressions.begin(), expressions.end(), [](auto exp) { + return ExpressionConstant::isNullOrConstant(exp); + }); + } + + /** + * Returns the constant value represented by this Expression. + */ + Value getValue() const { + return _value; + } + +private: + ExpressionConstant(const boost::intrusive_ptr<ExpressionContext>& expCtx, const Value& value); + + Value _value; +}; + +/** + * Inherit from this class if your expression works with date types, and accepts either a single + * argument which is a date, or an object {date: <date>, timezone: <string>}. + */ +template <typename SubClass> +class DateExpressionAcceptingTimeZone : public Expression { +public: + virtual ~DateExpressionAcceptingTimeZone() {} + + Value evaluate(const Document& root) const final { + auto dateVal = _date->evaluate(root); + if (dateVal.nullish()) { + return Value(BSONNULL); + } + auto date = dateVal.coerceToDate(); + + if (!_timeZone) { + return evaluateDate(date, TimeZoneDatabase::utcZone()); + } + auto timeZoneId = _timeZone->evaluate(root); + if (timeZoneId.nullish()) { + return Value(BSONNULL); + } + + uassert(40533, + str::stream() << _opName + << " requires a string for the timezone argument, but was given a " + << typeName(timeZoneId.getType()) + << " (" + << timeZoneId.toString() + << ")", + timeZoneId.getType() == BSONType::String); + auto timeZone = TimeZoneDatabase::get(getExpressionContext()->opCtx->getServiceContext()) + ->getTimeZone(timeZoneId.getString()); + return evaluateDate(date, timeZone); + } + + void addDependencies(DepsTracker* deps) const final { + _date->addDependencies(deps); + if (_timeZone) { + _timeZone->addDependencies(deps); + } + } + + /** + * Always serializes to the full {date: <date arg>, timezone: <timezone arg>} format, leaving + * off the timezone if not specified. + */ + Value serialize(bool explain) const final { + return Value(Document{ + {_opName, + Document{{"date", _date->serialize(explain)}, + {"timezone", _timeZone ? _timeZone->serialize(explain) : Value()}}}}); + } + + boost::intrusive_ptr<Expression> optimize() final { + _date = _date->optimize(); + if (_timeZone) { + _timeZone = _timeZone->optimize(); + } + if (ExpressionConstant::allNullOrConstant({_date, _timeZone})) { + // Everything is a constant, so we can turn into a constant. + return ExpressionConstant::create(getExpressionContext(), evaluate(Document{})); + } + return this; + } + + static boost::intrusive_ptr<Expression> parse( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + BSONElement operatorElem, + const VariablesParseState& variablesParseState) { + boost::intrusive_ptr<DateExpressionAcceptingTimeZone> dateExpression{new SubClass(expCtx)}; + if (operatorElem.type() == BSONType::Object) { + if (operatorElem.embeddedObject().firstElementFieldName()[0] == '$') { + // Assume this is an expression specification representing the date argument + // like {$add: [<date>, 1000]}. + dateExpression->_date = Expression::parseObject( + expCtx, operatorElem.embeddedObject(), variablesParseState); + return dateExpression; + } else { + // It's an object specifying the date and timezone options like {date: <date>, + // timezone: <timezone>}. + for (const auto& subElem : operatorElem.embeddedObject()) { + auto argName = subElem.fieldNameStringData(); + if (argName == "date"_sd) { + dateExpression->_date = + Expression::parseOperand(expCtx, subElem, variablesParseState); + } else if (argName == "timezone"_sd) { + dateExpression->_timeZone = + Expression::parseOperand(expCtx, subElem, variablesParseState); + } else { + auto opName = operatorElem.fieldNameStringData(); + uasserted(40535, + str::stream() << "unrecognized option to " << opName << ": \"" + << argName + << "\""); + } + } + } + return dateExpression; + } else if (operatorElem.type() == BSONType::Array) { + auto elems = operatorElem.Array(); + uassert( + 40536, + str::stream() << dateExpression->_opName + << " accepts exactly one argument if given an array, but was given " + << elems.size(), + elems.size() == 1); + // We accept an argument wrapped in a single array. For example, either {$week: <date>} + // or {$week: [<date>]} are valid, but not {$week: [{date: <date>}]}. + operatorElem = elems[0]; + } + // It's some literal value, or the first element of a user-specified array. Either way it + // should be treated as the date argument. + dateExpression->_date = Expression::parseOperand(expCtx, operatorElem, variablesParseState); + return dateExpression; + } + +protected: + explicit DateExpressionAcceptingTimeZone(StringData opName, + const boost::intrusive_ptr<ExpressionContext>& expCtx) + : Expression(expCtx), _opName(opName) {} + + /** + * Subclasses should implement this to do their actual date-related logic. Uses 'timezone' to + * evaluate the expression against 'data'. If the user did not specify a time zone, 'timezone' + * will represent the UTC zone. + */ + virtual Value evaluateDate(Date_t date, const TimeZone& timezone) const = 0; + +private: + // The name of this expression, e.g. $week or $month. + StringData _opName; + + // The expression representing the date argument. + boost::intrusive_ptr<Expression> _date; + // The expression representing the timezone argument, nullptr if not specified. + boost::intrusive_ptr<Expression> _timeZone = nullptr; +}; class ExpressionAbs final : public ExpressionSingleNumericArg<ExpressionAbs> { public: @@ -629,39 +824,6 @@ private: typedef ExpressionFixedArity<ExpressionCond, 3> Base; }; - -class ExpressionConstant final : public Expression { -public: - boost::intrusive_ptr<Expression> optimize() final; - void addDependencies(DepsTracker* deps) const final; - Value evaluate(const Document& root) const final; - Value serialize(bool explain) const final; - - const char* getOpName() const; - - static boost::intrusive_ptr<ExpressionConstant> create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, const Value& pValue); - - static boost::intrusive_ptr<Expression> parse( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - BSONElement bsonExpr, - const VariablesParseState& vps); - - /* - Get the constant value represented by this Expression. - - @returns the value - */ - Value getValue() const { - return pValue; - } - -private: - ExpressionConstant(const boost::intrusive_ptr<ExpressionContext>& expCtx, const Value& pValue); - - Value pValue; -}; - class ExpressionDateFromParts final : public Expression { public: boost::intrusive_ptr<Expression> optimize() final; @@ -762,33 +924,36 @@ private: boost::intrusive_ptr<Expression> _date; }; -class ExpressionDayOfMonth final : public ExpressionFixedArity<ExpressionDayOfMonth, 1> { +class ExpressionDayOfMonth final : public DateExpressionAcceptingTimeZone<ExpressionDayOfMonth> { public: explicit ExpressionDayOfMonth(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionDayOfMonth, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionDayOfMonth>("$dayOfMonth", expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.dateParts(date).dayOfMonth); + } }; -class ExpressionDayOfWeek final : public ExpressionFixedArity<ExpressionDayOfWeek, 1> { +class ExpressionDayOfWeek final : public DateExpressionAcceptingTimeZone<ExpressionDayOfWeek> { public: explicit ExpressionDayOfWeek(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionDayOfWeek, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionDayOfWeek>("$dayOfWeek", expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.dayOfWeek(date)); + } }; -class ExpressionDayOfYear final : public ExpressionFixedArity<ExpressionDayOfYear, 1> { +class ExpressionDayOfYear final : public DateExpressionAcceptingTimeZone<ExpressionDayOfYear> { public: explicit ExpressionDayOfYear(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionDayOfYear, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionDayOfYear>("$dayOfYear", expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.dayOfYear(date)); + } }; @@ -916,13 +1081,14 @@ public: }; -class ExpressionHour final : public ExpressionFixedArity<ExpressionHour, 1> { +class ExpressionHour final : public DateExpressionAcceptingTimeZone<ExpressionHour> { public: explicit ExpressionHour(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionHour, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionHour>("$hour", expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.dateParts(date).hour); + } }; @@ -1089,23 +1255,25 @@ private: MetaType _metaType; }; -class ExpressionMillisecond final : public ExpressionFixedArity<ExpressionMillisecond, 1> { +class ExpressionMillisecond final : public DateExpressionAcceptingTimeZone<ExpressionMillisecond> { public: explicit ExpressionMillisecond(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionMillisecond, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionMillisecond>("$millisecond", expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.dateParts(date).millisecond); + } }; -class ExpressionMinute final : public ExpressionFixedArity<ExpressionMinute, 1> { +class ExpressionMinute final : public DateExpressionAcceptingTimeZone<ExpressionMinute> { public: explicit ExpressionMinute(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionMinute, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionMinute>("$minute", expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.dateParts(date).minute); + } }; @@ -1137,13 +1305,14 @@ public: }; -class ExpressionMonth final : public ExpressionFixedArity<ExpressionMonth, 1> { +class ExpressionMonth final : public DateExpressionAcceptingTimeZone<ExpressionMonth> { public: explicit ExpressionMonth(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionMonth, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionMonth>("$month", expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.dateParts(date).month); + } }; @@ -1272,13 +1441,14 @@ private: }; -class ExpressionSecond final : public ExpressionFixedArity<ExpressionSecond, 1> { +class ExpressionSecond final : public DateExpressionAcceptingTimeZone<ExpressionSecond> { public: explicit ExpressionSecond(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionSecond, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionSecond>("$second", expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.dateParts(date).second); + } }; @@ -1537,53 +1707,59 @@ public: }; -class ExpressionWeek final : public ExpressionFixedArity<ExpressionWeek, 1> { +class ExpressionWeek final : public DateExpressionAcceptingTimeZone<ExpressionWeek> { public: explicit ExpressionWeek(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionWeek, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionWeek>("$week"_sd, expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.week(date)); + } }; -class ExpressionIsoWeekYear final : public ExpressionFixedArity<ExpressionIsoWeekYear, 1> { +class ExpressionIsoWeekYear final : public DateExpressionAcceptingTimeZone<ExpressionIsoWeekYear> { public: explicit ExpressionIsoWeekYear(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionIsoWeekYear, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionIsoWeekYear>("$isoWeekYear"_sd, expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.isoYear(date)); + } }; -class ExpressionIsoDayOfWeek final : public ExpressionFixedArity<ExpressionIsoDayOfWeek, 1> { +class ExpressionIsoDayOfWeek final + : public DateExpressionAcceptingTimeZone<ExpressionIsoDayOfWeek> { public: explicit ExpressionIsoDayOfWeek(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionIsoDayOfWeek, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionIsoDayOfWeek>("$isoDayOfWeek"_sd, expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.isoDayOfWeek(date)); + } }; -class ExpressionIsoWeek final : public ExpressionFixedArity<ExpressionIsoWeek, 1> { +class ExpressionIsoWeek final : public DateExpressionAcceptingTimeZone<ExpressionIsoWeek> { public: explicit ExpressionIsoWeek(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionIsoWeek, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionIsoWeek>("$isoWeek", expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.isoWeek(date)); + } }; -class ExpressionYear final : public ExpressionFixedArity<ExpressionYear, 1> { +class ExpressionYear final : public DateExpressionAcceptingTimeZone<ExpressionYear> { public: explicit ExpressionYear(const boost::intrusive_ptr<ExpressionContext>& expCtx) - : ExpressionFixedArity<ExpressionYear, 1>(expCtx) {} + : DateExpressionAcceptingTimeZone<ExpressionYear>("$year", expCtx) {} - Value evaluate(const Document& root) const final; - const char* getOpName() const final; + Value evaluateDate(Date_t date, const TimeZone& timeZone) const final { + return Value(timeZone.dateParts(date).year); + } }; diff --git a/src/mongo/db/pipeline/expression_test.cpp b/src/mongo/db/pipeline/expression_test.cpp index 0bb9a2e7c7b..eabb8208be7 100644 --- a/src/mongo/db/pipeline/expression_test.cpp +++ b/src/mongo/db/pipeline/expression_test.cpp @@ -4566,6 +4566,314 @@ TEST_F(ExpressionDateToPartsTest, OptimizesToConstantIfAllInputsAreConstant) { } // namespace ExpressionDateToPartsTest +namespace DateExpressionsTest { + +std::vector<StringData> dateExpressions = {"$year"_sd, + "$isoWeekYear"_sd, + "$month"_sd, + "$dayOfMonth"_sd, + "$hour"_sd, + "$minute"_sd, + "$second"_sd, + "$millisecond"_sd, + "$week"_sd, + "$isoWeek"_sd, + "$dayOfYear"_sd}; + +// This provides access to an ExpressionContext that has a valid ServiceContext with a +// TimeZoneDatabase via getExpCtx(), but we'll use a different name for this test suite. +using DateExpressionTest = AggregationContextFixture; + +TEST_F(DateExpressionTest, ParsingAcceptsAllFormats) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + auto possibleSyntaxes = { + // Single argument. + BSON(expName << Date_t{}), + BSON(expName << "$date"), + BSON(expName << BSON("$add" << BSON_ARRAY(Date_t{} << 1000))), + // Single argument wrapped in an array. + BSON(expName << BSON_ARRAY("$date")), + BSON(expName << BSON_ARRAY(Date_t{})), + BSON(expName << BSON_ARRAY(BSON("$add" << BSON_ARRAY(Date_t{} << 1000)))), + // Object literal syntax. + BSON(expName << BSON("date" << Date_t{})), + BSON(expName << BSON("date" + << "$date")), + BSON(expName << BSON("date" << BSON("$add" << BSON_ARRAY("$date" << 1000)))), + BSON(expName << BSON("date" << Date_t{} << "timezone" + << "Europe/London")), + BSON(expName << BSON("date" << Date_t{} << "timezone" + << "$tz"))}; + for (auto&& syntax : possibleSyntaxes) { + Expression::parseExpression(expCtx, syntax, expCtx->variablesParseState); + } + } +} + +TEST_F(DateExpressionTest, ParsingRejectsUnrecognizedFieldsInObjectSpecification) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "Europe/London" + << "extra" + << 4)); + ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), + UserException, + 40535); + } +} + +TEST_F(DateExpressionTest, RejectsEmptyArray) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSONArray()); + // It will parse as an ExpressionArray, and fail at runtime. + ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), + UserException, + 40536); + } +} + +TEST_F(DateExpressionTest, RejectsArraysWithMoreThanOneElement) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSON_ARRAY("$date" + << "$tz")); + // It will parse as an ExpressionArray, and fail at runtime. + ASSERT_THROWS_CODE(Expression::parseExpression(expCtx, spec, expCtx->variablesParseState), + UserException, + 40536); + } +} + +TEST_F(DateExpressionTest, RejectsArraysWithinObjectSpecification) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSON("date" << BSON_ARRAY(Date_t{}) << "timezone" + << "Europe/London")); + // It will parse as an ExpressionArray, and fail at runtime. + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"_id", 0}}; + ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), UserException, 16006); + + // Test that it rejects an array for the timezone option. + spec = + BSON(expName << BSON("date" << Date_t{} << "timezone" << BSON_ARRAY("Europe/London"))); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + contextDoc = Document{{"_id", 0}}; + ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), UserException, 40533); + } +} + +TEST_F(DateExpressionTest, RejectsTypesThatCannotCoerceToDate) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << "$stringField"); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"stringField", "string"_sd}}; + ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), UserException, 16006); + } +} + +TEST_F(DateExpressionTest, AcceptsObjectIds) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << "$oid"); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"oid", OID::gen()}}; + dateExp->evaluate(contextDoc); // Should not throw. + } +} + +TEST_F(DateExpressionTest, AcceptsTimestamps) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << "$ts"); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"ts", Timestamp{Date_t{}}}}; + dateExp->evaluate(contextDoc); // Should not throw. + } +} + +TEST_F(DateExpressionTest, RejectsNonStringTimezone) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "$intField")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"intField", 4}}; + ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), UserException, 40533); + } +} + +TEST_F(DateExpressionTest, RejectsUnrecognizedTimeZoneSpecification) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "UNRECOGNIZED!")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"_id", 0}}; + ASSERT_THROWS_CODE(dateExp->evaluate(contextDoc), UserException, 40485); + } +} + +TEST_F(DateExpressionTest, SerializesToObjectSyntax) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + // Test that it serializes to the full format if given an object specification. + BSONObj spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "Europe/London")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto expectedSerialization = + Value(Document{{expName, + Document{{"date", Document{{"$const", Date_t{}}}}, + {"timezone", Document{{"$const", "Europe/London"_sd}}}}}}); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); + + // Test that it serializes to the full format if given a date. + spec = BSON(expName << Date_t{}); + expectedSerialization = + Value(Document{{expName, Document{{"date", Document{{"$const", Date_t{}}}}}}}); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); + + // Test that it serializes to the full format if given a date within an array. + spec = BSON(expName << BSON_ARRAY(Date_t{})); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(dateExp->serialize(true), expectedSerialization); + ASSERT_VALUE_EQ(dateExp->serialize(false), expectedSerialization); + } +} + +TEST_F(DateExpressionTest, OptimizesToConstantIfAllInputsAreConstant) { + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + // Test that it becomes a constant if only date is provided, and it is constant. + auto spec = BSON(expName << BSON("date" << Date_t{})); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both date and timezone are provided, and are both + // constants. + spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it becomes a constant if both date and timezone are provided, and are both + // expressions which evaluate to constants. + spec = BSON(expName << BSON("date" << BSON("$add" << BSON_ARRAY(Date_t{} << 1000)) + << "timezone" + << BSON("$concat" << BSON_ARRAY("Europe" + << "/" + << "London")))); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both date and timezone are provided, but + // date is not a constant. + spec = BSON(expName << BSON("date" + << "$date" + << "timezone" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + + // Test that it does *not* become a constant if both date and timezone are provided, but + // timezone is not a constant. + spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "$tz")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_FALSE(dynamic_cast<ExpressionConstant*>(dateExp->optimize().get())); + } +} + +TEST_F(DateExpressionTest, DoesRespectTimeZone) { + // Make sure they each successfully evaluate with a different TimeZone. + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + auto spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "America/New_York")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + auto contextDoc = Document{{"_id", 0}}; + dateExp->evaluate(contextDoc); // Should not throw. + } + + // Make sure the time zone is used during evaluation. + auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); // 2017-06-06T19:38:43:234Z. + auto specWithoutTimezone = BSON("$hour" << BSON("date" << date)); + auto hourWithoutTimezone = + Expression::parseExpression(expCtx, specWithoutTimezone, expCtx->variablesParseState) + ->evaluate({}); + ASSERT_VALUE_EQ(hourWithoutTimezone, Value(19)); + + auto specWithTimezone = BSON("$hour" << BSON("date" << date << "timezone" + << "America/New_York")); + auto hourWithTimezone = + Expression::parseExpression(expCtx, specWithTimezone, expCtx->variablesParseState) + ->evaluate({}); + ASSERT_VALUE_EQ(hourWithTimezone, Value(15)); +} + +TEST_F(DateExpressionTest, DoesResultInNullIfGivenNullishInput) { + // Make sure they each successfully evaluate with a different TimeZone. + auto expCtx = getExpCtx(); + for (auto&& expName : dateExpressions) { + auto contextDoc = Document{{"_id", 0}}; + + // Test that the expression results in null if the date is nullish and the timezone is not + // specified. + auto spec = BSON(expName << BSON("date" + << "$missing")); + auto dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + spec = BSON(expName << BSON("date" << BSONNULL)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + spec = BSON(expName << BSON("date" << BSONUndefined)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + // Test that the expression results in null if the date is present but the timezone is + // nullish. + spec = BSON(expName << BSON("date" << Date_t{} << "timezone" + << "$missing")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + spec = BSON(expName << BSON("date" << Date_t{} << "timezone" << BSONNULL)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + spec = BSON(expName << BSON("date" << Date_t{} << "timezone" << BSONUndefined)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + // Test that the expression results in null if the date and timezone both nullish. + spec = BSON(expName << BSON("date" + << "$missing" + << "timezone" + << BSONUndefined)); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + + // Test that the expression results in null if the date is nullish and timezone is present. + spec = BSON(expName << BSON("date" + << "$missing" + << "timezone" + << "Europe/London")); + dateExp = Expression::parseExpression(expCtx, spec, expCtx->variablesParseState); + ASSERT_VALUE_EQ(Value(BSONNULL), dateExp->evaluate(contextDoc)); + } +} + +} // namespace DateExpressionsTest + class All : public Suite { public: All() : Suite("expression") {} 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 503e9cc3645..6601d7f572e 100644 --- a/src/mongo/db/query/datetime/date_time_support_test.cpp +++ b/src/mongo/db/query/datetime/date_time_support_test.cpp @@ -36,6 +36,21 @@ namespace mongo { namespace { +const TimeZoneDatabase kDefaultTimeZoneDatabase{}; + +TEST(GetTimeZone, DoesReturnKnownTimeZone) { + // Just asserting that these do not throw exceptions. + kDefaultTimeZoneDatabase.getTimeZone("UTC"); + kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + kDefaultTimeZoneDatabase.getTimeZone("Australia/Sydney"); +} + +TEST(GetTimeZone, DoesNotReturnUnknownTimeZone) { + ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("The moon"), UserException, 40485); + ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("xyz"), UserException, 40485); + ASSERT_THROWS_CODE(kDefaultTimeZoneDatabase.getTimeZone("Jupiter"), UserException, 40485); +} + TEST(UTCTimeBeforeEpoch, DoesExtractDateParts) { // Dec 30, 1969 13:42:23:211 auto date = Date_t::fromMillisSinceEpoch(-123456789LL); @@ -49,6 +64,21 @@ TEST(UTCTimeBeforeEpoch, DoesExtractDateParts) { ASSERT_EQ(dateParts.millisecond, 211); } +TEST(NewYorkTimeBeforeEpoch, DoesExtractDateParts) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 1969-12-30T13:42:23.211Z. + auto date = Date_t::fromMillisSinceEpoch(-123456789LL); + auto dateParts = newYorkZone.dateParts(date); + ASSERT_EQ(dateParts.year, 1969); + ASSERT_EQ(dateParts.month, 12); + ASSERT_EQ(dateParts.dayOfMonth, 30); + ASSERT_EQ(dateParts.hour, 8); + ASSERT_EQ(dateParts.minute, 42); + ASSERT_EQ(dateParts.second, 23); + ASSERT_EQ(dateParts.millisecond, 211); +} + TEST(UTCTimeBeforeEpoch, DoesComputeISOYear) { // Sunday, December 28, 1969. auto date = Date_t::fromMillisSinceEpoch(-345600000LL); @@ -61,6 +91,22 @@ TEST(UTCTimeBeforeEpoch, DoesComputeISOYear) { ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 1965); } +TEST(NewYorkTimeBeforeEpoch, DoesComputeISOYear) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // Sunday, December 28, 1969. + auto date = Date_t::fromMillisSinceEpoch(-345600000LL); + ASSERT_EQ(newYorkZone.isoYear(date), 1969); + + // 1969-12-30T13:42:23.211Z (Tuesday), part of the following year. + date = Date_t::fromMillisSinceEpoch(-123456000LL); + ASSERT_EQ(newYorkZone.isoYear(date), 1970); + + // 1966-01-01T00:00:00.000Z (Saturday), part of the previous year. + date = Date_t::fromMillisSinceEpoch(-126230400000LL); + ASSERT_EQ(newYorkZone.isoYear(date), 1965); +} + TEST(UTCTimeBeforeEpoch, DoesComputeDayOfWeek) { // Sunday, December 28, 1969. auto date = Date_t::fromMillisSinceEpoch(-345600000LL); @@ -73,6 +119,19 @@ TEST(UTCTimeBeforeEpoch, DoesComputeDayOfWeek) { ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfWeek(date), 7); } +TEST(NewYorkTimeBeforeEpoch, DoesComputeDayOfWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 1969-12-28T00:00:00.000Z (Sunday). + auto date = Date_t::fromMillisSinceEpoch(-345600000LL); + // Part of the previous day (Saturday) in New York. + ASSERT_EQ(newYorkZone.dayOfWeek(date), 7); + + // 1969-12-30T13:42:23.211Z (Tuesday). + date = Date_t::fromMillisSinceEpoch(-123456000LL); + ASSERT_EQ(newYorkZone.dayOfWeek(date), 3); +} + TEST(UTCTimeBeforeEpoch, DoesComputeISODayOfWeek) { // Sunday, December 28, 1969. auto date = Date_t::fromMillisSinceEpoch(-345600000LL); @@ -85,6 +144,19 @@ TEST(UTCTimeBeforeEpoch, DoesComputeISODayOfWeek) { ASSERT_EQ(TimeZoneDatabase::utcZone().isoDayOfWeek(date), 6); } +TEST(NewYorkTimeBeforeEpoch, DoesComputeISODayOfWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 1969-12-28T00:00:00.000Z (Sunday). + auto date = Date_t::fromMillisSinceEpoch(-345600000LL); + // Part of the previous day (Saturday) in New York. + ASSERT_EQ(newYorkZone.isoDayOfWeek(date), 6); + + // 1969-12-30T13:42:23.211Z (Tuesday). + date = Date_t::fromMillisSinceEpoch(-123456000LL); + ASSERT_EQ(newYorkZone.isoDayOfWeek(date), 2); +} + TEST(UTCTimeBeforeEpoch, DoesComputeDayOfYear) { // December 30, 1969. auto date = Date_t::fromMillisSinceEpoch(-123456000LL); @@ -117,6 +189,41 @@ TEST(UTCTimeBeforeEpoch, DoesComputeDayOfYear) { ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 365); } +TEST(NewYorkTimeBeforeEpoch, DoesComputeDayOfYear) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 1969-12-28T13:42:24.000Z (Sunday). + auto date = Date_t::fromMillisSinceEpoch(-123456000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 364); + + // 1966-01-01T00:00:00.000Z (Saturday). + date = Date_t::fromMillisSinceEpoch(-126230400000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 365); + + // 1960-02-28T00:00:00.000Z (leap year). + date = Date_t::fromMillisSinceEpoch(-310608000000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 58); + // 1960-02-29T00:00:00.000Z. + date = Date_t::fromMillisSinceEpoch(-310521600000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 59); + // 1960-01-01T00:00:00.000Z. + date = Date_t::fromMillisSinceEpoch(-310435200000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 60); + // 1960-12-31T00:00:00.000Z. + date = Date_t::fromMillisSinceEpoch(-284083200000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 365); + + // 1900-02-28T00:00:00.000Z (not leap year). + date = Date_t::fromMillisSinceEpoch(-2203977600000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 58); + // 1900-03-01T00:00:00.000Z. + date = Date_t::fromMillisSinceEpoch(-2203891200000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 59); + // 1900-12-31T00:00:00.000Z. + date = Date_t::fromMillisSinceEpoch(-2177539200000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 364); +} + TEST(UTCTimeBeforeEpoch, DoesComputeWeek) { // Sunday, December 28, 1969. auto date = Date_t::fromMillisSinceEpoch(-345600000LL); @@ -126,6 +233,18 @@ TEST(UTCTimeBeforeEpoch, DoesComputeWeek) { ASSERT_EQ(TimeZoneDatabase::utcZone().week(date), 0); } +TEST(NewYorkTimeBeforeEpoch, DoesComputeWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 1969-12-28T00:00:00.000Z (Sunday). + auto date = Date_t::fromMillisSinceEpoch(-345600000LL); + ASSERT_EQ(newYorkZone.week(date), 51); + + // 1966-01-01T00:00:00.000Z (Saturday). + date = Date_t::fromMillisSinceEpoch(-126230400000LL); + ASSERT_EQ(newYorkZone.week(date), 52); +} + TEST(UTCTimeBeforeEpoch, DoesComputeISOWeek) { // Sunday, December 28, 1969. auto date = Date_t::fromMillisSinceEpoch(-345600000LL); @@ -144,6 +263,28 @@ TEST(UTCTimeBeforeEpoch, DoesComputeISOWeek) { ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 53); } +TEST(NewYorkTimeBeforeEpoch, DoesComputeISOWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 1969-12-28T00:00:00.000Z (Sunday). + auto date = Date_t::fromMillisSinceEpoch(-345600000LL); + ASSERT_EQ(newYorkZone.isoWeek(date), 52); + + // 1969-12-30T00:00:00.000Z (Tuesday). + date = Date_t::fromMillisSinceEpoch(-123456000LL); + ASSERT_EQ(newYorkZone.isoWeek(date), 1); + + // 1966-01-01T00:00:00.000Z (Saturday), part of previous year. + date = Date_t::fromMillisSinceEpoch(-126230400000LL); + ASSERT_EQ(newYorkZone.isoWeek(date), 52); + // 1959-12-29T00:00:00.000Z (Tuesday). + date = Date_t::fromMillisSinceEpoch(-315878400000LL); + ASSERT_EQ(newYorkZone.isoWeek(date), 53); + // 1960-01-02T00:00:00.000Z (Saturday), part of previous year. + date = Date_t::fromMillisSinceEpoch(-315532800000LL); + ASSERT_EQ(newYorkZone.isoWeek(date), 53); +} + TEST(UTCTimeBeforeEpoch, DoesFormatDate) { // Tuesday, Dec 30, 1969 13:42:23:211 auto date = Date_t::fromMillisSinceEpoch(-123456789LL); @@ -155,6 +296,19 @@ TEST(UTCTimeBeforeEpoch, DoesFormatDate) { "isoWeek: 01, isoDayOfWeek: 2, percent: %"); } +TEST(NewYorkTimeBeforeEpoch, DoesFormatDate) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // Tuesday, Dec 30, 1969 13:42:23:211 + auto date = Date_t::fromMillisSinceEpoch(-123456789LL); + ASSERT_EQ(newYorkZone.formatDate("%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, " + "dayOfWeek: %w, week: %U, isoYear: %G, " + "isoWeek: %V, isoDayOfWeek: %u, percent: %%", + date), + "1969/12/30 08:42:23:211, dayOfYear: 364, dayOfWeek: 3, week: 52, isoYear: 1970, " + "isoWeek: 01, isoDayOfWeek: 2, percent: %"); +} + TEST(UTCTimeBeforeEpoch, DoesOutputFormatDate) { // Tuesday, Dec 30, 1969 13:42:23:211 auto date = Date_t::fromMillisSinceEpoch(-123456789LL); @@ -169,6 +323,22 @@ TEST(UTCTimeBeforeEpoch, DoesOutputFormatDate) { "isoWeek: 01, isoDayOfWeek: 2, percent: %"); } +TEST(NewYorkTimeBeforeEpoch, DoesOutputFormatDate) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // Tuesday, Dec 30, 1969 13:42:23:211 + auto date = Date_t::fromMillisSinceEpoch(-123456789LL); + std::ostringstream os; + newYorkZone.outputDateWithFormat(os, + "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, " + "dayOfWeek: %w, week: %U, isoYear: %G, " + "isoWeek: %V, isoDayOfWeek: %u, percent: %%", + date); + ASSERT_EQ(os.str(), + "1969/12/30 08:42:23:211, dayOfYear: 364, dayOfWeek: 3, week: 52, isoYear: 1970, " + "isoWeek: 01, isoDayOfWeek: 2, percent: %"); +} + TEST(UTCTimeAtEpoch, DoesExtractDateParts) { // Jan 1, 1970 00:00:00:000 auto date = Date_t::fromMillisSinceEpoch(0); @@ -182,42 +352,105 @@ TEST(UTCTimeAtEpoch, DoesExtractDateParts) { ASSERT_EQ(dateParts.millisecond, 0); } +TEST(NewYorkTimeAtEpoch, DoesExtractDateParts) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // Jan 1, 1970 00:00:00:000 + auto date = Date_t::fromMillisSinceEpoch(0); + auto dateParts = newYorkZone.dateParts(date); + ASSERT_EQ(dateParts.year, 1969); + ASSERT_EQ(dateParts.month, 12); + ASSERT_EQ(dateParts.dayOfMonth, 31); + ASSERT_EQ(dateParts.hour, 19); + ASSERT_EQ(dateParts.minute, 0); + ASSERT_EQ(dateParts.second, 0); + ASSERT_EQ(dateParts.millisecond, 0); +} + TEST(UTCTimeAtEpoch, DoesComputeISOYear) { // Thursday, January 1, 1970. auto date = Date_t::fromMillisSinceEpoch(0); ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 1970); } +TEST(NewYorkTimeAtEpoch, DoesComputeISOYear) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 1970-1-1T00:00:00.000Z (Thursday) + auto date = Date_t::fromMillisSinceEpoch(0); + ASSERT_EQ(newYorkZone.isoYear(date), 1970); // The Wednesday is still considered part of 1970. +} + TEST(UTCTimeAtEpoch, DoesComputeDayOfWeek) { // Thursday, January 1, 1970. auto date = Date_t::fromMillisSinceEpoch(0); ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfWeek(date), 5); } +TEST(NewYorkTimeAtEpoch, DoesComputeDayOfWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 1970-1-1T00:00:00.000Z (Thursday) + auto date = Date_t::fromMillisSinceEpoch(0); + ASSERT_EQ(newYorkZone.dayOfWeek(date), 4); // The Wednesday before. +} + TEST(UTCTimeAtEpoch, DoesComputeISODayOfWeek) { // Thursday, January 1, 1970. auto date = Date_t::fromMillisSinceEpoch(0); ASSERT_EQ(TimeZoneDatabase::utcZone().isoDayOfWeek(date), 4); } +TEST(NewYorkTimeAtEpoch, DoesComputeISODayOfWeek) { + // 1970-1-1T00:00:00.000Z (Thursday) + auto date = Date_t::fromMillisSinceEpoch(0); + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + ASSERT_EQ(newYorkZone.isoDayOfWeek(date), 3); // The Wednesday before. +} + TEST(UTCTimeAtEpoch, DoesComputeDayOfYear) { // Thursday, January 1, 1970. auto date = Date_t::fromMillisSinceEpoch(0); ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 1); } +TEST(NewYorkTimeAtEpoch, DoesComputeDayOfYear) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 1970-1-1T00:00:00.000Z + auto date = Date_t::fromMillisSinceEpoch(0); + ASSERT_EQ(newYorkZone.dayOfYear(date), 365); +} + TEST(UTCTimeAtEpoch, DoesComputeWeek) { // Thursday, January 1, 1970. auto date = Date_t::fromMillisSinceEpoch(0); ASSERT_EQ(TimeZoneDatabase::utcZone().week(date), 0); } +TEST(NewYorkTimeAtEpoch, DoesComputeWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 1970-1-1T00:00:00.000Z + auto date = Date_t::fromMillisSinceEpoch(0); + ASSERT_EQ(newYorkZone.week(date), 52); +} + TEST(UTCTimeAtEpoch, DoesComputeISOWeek) { // Thursday, January 1, 1970. auto date = Date_t::fromMillisSinceEpoch(0); ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 1); } +TEST(NewYorkTimeAtEpoch, DoesComputeISOWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // Thu, Jan 1, 1970 00:00:00:000Z + auto date = Date_t::fromMillisSinceEpoch(0); + // This is Wednesday in New York, but that is still part of the first week. + ASSERT_EQ(newYorkZone.isoWeek(date), 1); +} + TEST(UTCTimeAtEpoch, DoesFormatDate) { // Thu, Jan 1, 1970 00:00:00:000 auto date = Date_t::fromMillisSinceEpoch(0); @@ -229,6 +462,19 @@ TEST(UTCTimeAtEpoch, DoesFormatDate) { "isoWeek: 01, isoDayOfWeek: 4, percent: %"); } +TEST(NewYorkTimeAtEpoch, DoesFormatDate) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // Thu, Jan 1, 1970 00:00:00:000Z + auto date = Date_t::fromMillisSinceEpoch(0); + ASSERT_EQ(newYorkZone.formatDate("%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, " + "dayOfWeek: %w, week: %U, isoYear: %G, " + "isoWeek: %V, isoDayOfWeek: %u, percent: %%", + date), + "1969/12/31 19:00:00:000, dayOfYear: 365, dayOfWeek: 4, week: 52, isoYear: 1970, " + "isoWeek: 01, isoDayOfWeek: 3, percent: %"); +} + TEST(UTCTimeAtEpoch, DoesOutputFormatDate) { auto date = Date_t::fromMillisSinceEpoch(0); std::ostringstream os; @@ -242,6 +488,20 @@ TEST(UTCTimeAtEpoch, DoesOutputFormatDate) { "isoWeek: 01, isoDayOfWeek: 4, percent: %"); } +TEST(NewYorkTimeAtEpoch, DoesOutputFormatDate) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + auto date = Date_t::fromMillisSinceEpoch(0); + std::ostringstream os; + newYorkZone.outputDateWithFormat(os, + "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, " + "dayOfWeek: %w, week: %U, isoYear: %G, " + "isoWeek: %V, isoDayOfWeek: %u, percent: %%", + date); + ASSERT_EQ(os.str(), + "1969/12/31 19:00:00:000, dayOfYear: 365, dayOfWeek: 4, week: 52, isoYear: 1970, " + "isoWeek: 01, isoDayOfWeek: 3, percent: %"); +} + TEST(UTCTimeAfterEpoch, DoesExtractDateParts) { // Jun 6, 2017 19:38:43:123. auto date = Date_t::fromMillisSinceEpoch(1496777923123LL); @@ -255,6 +515,21 @@ TEST(UTCTimeAfterEpoch, DoesExtractDateParts) { ASSERT_EQ(dateParts.millisecond, 123); } +TEST(NewYorkTimeAfterEpoch, DoesExtractDateParts) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // Jun 6, 2017 19:38:43:123Z. + auto date = Date_t::fromMillisSinceEpoch(1496777923123LL); + auto dateParts = newYorkZone.dateParts(date); + ASSERT_EQ(dateParts.year, 2017); + ASSERT_EQ(dateParts.month, 6); + ASSERT_EQ(dateParts.dayOfMonth, 6); + ASSERT_EQ(dateParts.hour, 15); + ASSERT_EQ(dateParts.minute, 38); + ASSERT_EQ(dateParts.second, 43); + ASSERT_EQ(dateParts.millisecond, 123); +} + TEST(UTCTimeAfterEpoch, DoesComputeISOYear) { // Tuesday, June 6, 2017. auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); @@ -270,6 +545,30 @@ TEST(UTCTimeAfterEpoch, DoesComputeISOYear) { ASSERT_EQ(TimeZoneDatabase::utcZone().isoYear(date), 2008); } +TEST(NewYorkTimeAfterEpoch, DoesComputeISOYear) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 2017-06-06T19:38:43:123Z (Tuesday). + auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); + ASSERT_EQ(newYorkZone.isoYear(date), 2017); + + // 2005-01-01T00:00:00.000Z (Saturday), part of 2004. + date = Date_t::fromMillisSinceEpoch(1104537600000LL); + ASSERT_EQ(newYorkZone.isoYear(date), 2004); + + // 2007-01-01T00:00:00.000Z (Monday), part of 2007. + date = Date_t::fromMillisSinceEpoch(1167609600000LL); + // ISO weeks are Mon-Sun, so this is part of the previous week in New York, so part of the + // previous year, 2006. + ASSERT_EQ(newYorkZone.isoYear(date), 2006); + + // 2007-12-31T00:00:00.000Z (Monday), part of 2007. + date = Date_t::fromMillisSinceEpoch(1199059200000LL); + // ISO weeks are Mon-Sun, so this is part of the previous week in New York, so part of the + // previous year, 2007. + ASSERT_EQ(newYorkZone.isoYear(date), 2007); +} + TEST(UTCTimeAfterEpoch, DoesComputeDayOfWeek) { // Tuesday, June 6, 2017. auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); @@ -282,6 +581,18 @@ TEST(UTCTimeAfterEpoch, DoesComputeDayOfWeek) { ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfWeek(date), 2); } +TEST(NewYorkTimeAfterEpoch, DoesComputeDayOfWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 2017-06-06T19:38:43.123Z (Tuesday). + auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); + ASSERT_EQ(newYorkZone.dayOfWeek(date), 3); + + // 2005-01-01T00:00:00.000Z (Saturday). + date = Date_t::fromMillisSinceEpoch(1104537600000LL); + ASSERT_EQ(newYorkZone.dayOfWeek(date), 6); // Part of the previous day in New York. +} + TEST(UTCTimeAfterEpoch, DoesComputeISODayOfWeek) { // Tuesday, June 6, 2017. auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); @@ -294,6 +605,18 @@ TEST(UTCTimeAfterEpoch, DoesComputeISODayOfWeek) { ASSERT_EQ(TimeZoneDatabase::utcZone().isoDayOfWeek(date), 1); } +TEST(NewYorkTimeAfterEpoch, DoesComputeISODayOfWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 2017-06-06T19:38:43.123Z (Tuesday). + auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); + ASSERT_EQ(newYorkZone.isoDayOfWeek(date), 2); + + // 2005-01-01T00:00:00.000Z (Saturday). + date = Date_t::fromMillisSinceEpoch(1104537600000LL); + ASSERT_EQ(newYorkZone.isoDayOfWeek(date), 5); // Part of the previous day in New York. +} + TEST(UTCTimeAfterEpoch, DoesComputeDayOfYear) { // June 6, 2017. auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); @@ -325,6 +648,47 @@ TEST(UTCTimeAfterEpoch, DoesComputeDayOfYear) { ASSERT_EQ(TimeZoneDatabase::utcZone().dayOfYear(date), 365); } +TEST(NewYorkTimeAfterEpoch, DoesComputeDayOfYear) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // 2017-06-06T19:38:43.123Z. + auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 157); + + // 2005-01-01T00:00:00.000Z, part of 2004 in New York, which was a leap year. + date = Date_t::fromMillisSinceEpoch(1104537600000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 366); + + // 2008-02-28T00:00:00.000Z (leap year). + date = Date_t::fromMillisSinceEpoch(1204156800000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 58); + // 2008-02-29T00:00:00.000Z. + date = Date_t::fromMillisSinceEpoch(1204243200000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 59); + // 2008-03-01T00:00:00.000Z. + date = Date_t::fromMillisSinceEpoch(1204329600000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 60); + // 2008-12-31T00:00:00.000Z. + date = Date_t::fromMillisSinceEpoch(1230681600000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 365); + // 2009-01-01T00:00:00.000Z, part of the previous year in New York. + date = Date_t::fromMillisSinceEpoch(1230768000000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 366); + + // 2001-02-28T00:00:00.000Z (not leap year). + date = Date_t::fromMillisSinceEpoch(983318400000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 58); + // 2001-03-01T00:00:00.000Z. + date = Date_t::fromMillisSinceEpoch(983404800000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 59); + // 2001-12-31T00:00:00.000Z. + date = Date_t::fromMillisSinceEpoch(1009756800000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 364); + // 2002-01-01T00:00:00.000Z, part of the previous year in New York. + date = Date_t::fromMillisSinceEpoch(1009843200000LL); + ASSERT_EQ(newYorkZone.dayOfYear(date), 365); +} + TEST(UTCTimeAfterEpoch, DoesComputeWeek) { // Tuesday, June 6, 2017. auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); @@ -337,6 +701,22 @@ TEST(UTCTimeAfterEpoch, DoesComputeWeek) { ASSERT_EQ(TimeZoneDatabase::utcZone().week(date), 0); } +TEST(NewYorkTimeAfterEpoch, DoesComputeWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // Tuesday, June 6, 2017. + auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); + ASSERT_EQ(newYorkZone.week(date), 23); + + // 2005-01-01T00:00:00.00Z (Saturday). + date = Date_t::fromMillisSinceEpoch(1104537600000LL); + ASSERT_EQ(newYorkZone.week(date), 52); + + // 2007-01-01T00:00:00.00Z (Monday), the last Sunday of 2006 in New York. + date = Date_t::fromMillisSinceEpoch(1167609600000LL); + ASSERT_EQ(newYorkZone.week(date), 53); +} + TEST(UTCTimeAfterEpoch, DoesComputeISOWeek) { // Tuesday, June 6, 2017. auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); @@ -349,6 +729,22 @@ TEST(UTCTimeAfterEpoch, DoesComputeISOWeek) { ASSERT_EQ(TimeZoneDatabase::utcZone().isoWeek(date), 1); } +TEST(NewYorkTimeAfterEpoch, DoesComputeISOWeek) { + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + + // Tuesday, June 6, 2017. + auto date = Date_t::fromMillisSinceEpoch(1496777923000LL); + ASSERT_EQ(newYorkZone.isoWeek(date), 23); + + // 2005-01-01T00:00:00.000Z (Saturday), part of 2004. + date = Date_t::fromMillisSinceEpoch(1104537600000LL); + ASSERT_EQ(newYorkZone.isoWeek(date), 53); + + // 2007-01-01T00:00:00.000Z (Monday), part of 2006 in New York. + date = Date_t::fromMillisSinceEpoch(1167609600000LL); + ASSERT_EQ(newYorkZone.isoWeek(date), 52); +} + TEST(UTCTimeAfterEpoch, DoesFormatDate) { // Tue, Jun 6, 2017 19:38:43:234. auto date = Date_t::fromMillisSinceEpoch(1496777923234LL); @@ -360,6 +756,18 @@ TEST(UTCTimeAfterEpoch, DoesFormatDate) { "isoWeek: 23, isoDayOfWeek: 2, percent: %"); } +TEST(NewYorkTimeAfterEpoch, DoesFormatDate) { + // 2017-06-06T19:38:43:234Z (Tuesday). + auto date = Date_t::fromMillisSinceEpoch(1496777923234LL); + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + ASSERT_EQ(newYorkZone.formatDate("%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, " + "dayOfWeek: %w, week: %U, isoYear: %G, " + "isoWeek: %V, isoDayOfWeek: %u, percent: %%", + date), + "2017/06/06 15:38:43:234, dayOfYear: 157, dayOfWeek: 3, week: 23, isoYear: 2017, " + "isoWeek: 23, isoDayOfWeek: 2, percent: %"); +} + TEST(UTCTimeAfterEpoch, DoesOutputFormatDate) { // Tue, Jun 6, 2017 19:38:43:234. auto date = Date_t::fromMillisSinceEpoch(1496777923234LL); @@ -374,6 +782,21 @@ TEST(UTCTimeAfterEpoch, DoesOutputFormatDate) { "isoWeek: 23, isoDayOfWeek: 2, percent: %"); } +TEST(NewYorkTimeAfterEpoch, DoesOutputFormatDate) { + // 2017-06-06T19:38:43:234Z (Tuesday). + auto date = Date_t::fromMillisSinceEpoch(1496777923234LL); + std::ostringstream os; + auto newYorkZone = kDefaultTimeZoneDatabase.getTimeZone("America/New_York"); + newYorkZone.outputDateWithFormat(os, + "%Y/%m/%d %H:%M:%S:%L, dayOfYear: %j, " + "dayOfWeek: %w, week: %U, isoYear: %G, " + "isoWeek: %V, isoDayOfWeek: %u, percent: %%", + date); + ASSERT_EQ(os.str(), + "2017/06/06 15:38:43:234, dayOfYear: 157, dayOfWeek: 3, week: 23, isoYear: 2017, " + "isoWeek: 23, isoDayOfWeek: 2, percent: %"); +} + TEST(DateFormat, ThrowsUserExceptionIfGivenUnrecognizedFormatter) { ASSERT_THROWS_CODE(TimeZoneDatabase::utcZone().validateFormat("%x"), UserException, 18536); } |