diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java index a087f612a..544e1c8dc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2016. the original author or authors. + * Copyright 2016-2018. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import org.bson.Document; import org.springframework.util.ObjectUtils; @@ -44,31 +45,8 @@ abstract class AbstractAggregationExpression implements AggregationExpression { return toDocument(this.value, context); } - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { - - Object valueToUse; - if (value instanceof List) { - - List arguments = (List) value; - List args = new ArrayList(arguments.size()); - - for (Object val : arguments) { - args.add(unpack(val, context)); - } - valueToUse = args; - } else if (value instanceof java.util.Map) { - - Document dbo = new Document(); - for (java.util.Map.Entry entry : ((java.util.Map) value).entrySet()) { - dbo.put(entry.getKey(), unpack(entry.getValue(), context)); - } - valueToUse = dbo; - } else { - valueToUse = unpack(value, context); - } - - return new Document(getMongoMethod(), valueToUse); + return new Document(getMongoMethod(), unpack(value, context)); } protected static List asFields(String... fieldRefs) { @@ -85,26 +63,28 @@ abstract class AbstractAggregationExpression implements AggregationExpression { if (value instanceof AggregationExpression) { return ((AggregationExpression) value).toDocument(context); - } - - if (value instanceof Field) { + } else if (value instanceof DateFactory) { + return ((DateFactory) value).currentDate(); + } else if (value instanceof Field) { return context.getReference((Field) value).toString(); - } - - if (value instanceof List) { + } else if (value instanceof List) { List sourceList = (List) value; - List mappedList = new ArrayList(sourceList.size()); + List mappedList = new ArrayList<>(sourceList.size()); + + sourceList.stream().map((item) -> unpack(item, context)).forEach(mappedList::add); - for (Object item : sourceList) { - mappedList.add(unpack(item, context)); - } return mappedList; + } else if (value instanceof java.util.Map) { + Document dbo = new Document(); + ((Map) value).forEach((k, v) -> dbo.put(k, unpack(v, context))); + return dbo; } return value; } + @SuppressWarnings({ "unchecked", "rawtypes" }) protected List append(Object value) { if (this.value instanceof List) { @@ -136,6 +116,7 @@ abstract class AbstractAggregationExpression implements AggregationExpression { } + @SuppressWarnings({ "unchecked", "rawtypes" }) protected List values() { if (value instanceof List) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateFactory.java new file mode 100644 index 000000000..a3c44860f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.aggregation; + +import java.util.Date; + +/** + * Used for {@link DateOperators} related functions to access the current date + * + * @since 2.1 + * @author Matt Morrissette + */ +@FunctionalInterface +public interface DateFactory { + + /** + * @author Matt Morrissette + * @param currentDate + * @return A date factory that always uses the given date as the current date. Primary used in testing and mock + * scenarios. + */ + public static DateFactory fixedDate(final Object currentDate) { + return () -> currentDate; + } + + /** + * DateFactory that uses the date as it is on the local server + */ + public static final DateFactory LOCAL_DATE_FACTORY = Date::new; + + /** + * Should return an object that is serializable by the BSON encoder and would resolve to a BSON Date when evaluated. + *

+ * This includes + *

    + *
  • {@link java.util.Date}
  • + *
  • {@link java.util.Calendar}
  • + *
  • {@link java.time.Instant}
  • + *
  • {@link java.time.ZonedDateTime}
  • + *
  • {@link java.lang.Long}
  • + *
  • org.joda.time.AbstractInstant
  • + *
+ * + * @author Matt Morrissette + * @return + */ + public Object currentDate(); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java index 4b501d782..37ae95216 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java @@ -1,5 +1,5 @@ /* - * Copyright 2016. the original author or authors. + * Copyright 2016-2018. the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,224 +15,967 @@ */ package org.springframework.data.mongodb.core.aggregation; -import java.util.LinkedHashMap; +import static org.springframework.data.mongodb.core.aggregation.ConditionalOperators.*; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.bson.Document; import org.springframework.data.mongodb.core.aggregation.ArithmeticOperators.ArithmeticOperatorFactory; +import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.*; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** * Gateway to {@literal Date} aggregation operations. + *

+ * Prior to Mongo 3.6, all Date operations were in the UTC timezone
+ * New in Mongo 3.6 is support for timezone conversion on all aggregation operations. This is a breaking change + * and using any of the aggregation methods with a 'timezone' attribute on a Mongo server prior to 3.6 will cause + * errors. * * @author Christoph Strobl + * @author Matt Morrissette * @since 1.10 */ public class DateOperators { /** - * Take the date referenced by given {@literal fieldReference}. + * Take the date referenced by given {@literal fieldReference} in the UTC timezone. * * @param fieldReference must not be {@literal null}. * @return */ public static DateOperatorFactory dateOf(String fieldReference) { - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new DateOperatorFactory(fieldReference); + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new DateOperatorFactory(fieldReference, null); } /** - * Take the date resulting from the given {@link AggregationExpression}. + * Take the date referenced by given {@literal fieldReference} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static DateOperatorFactory dateOfWithTimezone(String fieldReference, @Nullable String timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new DateOperatorFactory(fieldReference, timezone); + } + + /** + * Take the date referenced by given {@literal fieldReference} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezoneExpression Must not be null. The expression to bind the timezone value from + * @since 2.1 + * @return + */ + public static DateOperatorFactory dateOfWithTimezoneOf(String fieldReference, + AggregationExpression timezoneExpression) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + Assert.notNull(timezoneExpression, "timezoneExpression must not be null!"); + return new DateOperatorFactory(fieldReference, timezoneExpression); + } + + /** + * Take the date referenced by given {@literal fieldReference} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. The field to bind the date value from + * @param timezoneField Must not be null. The name of the field to bind the timezone value from + * @since 2.1 + * @return + */ + public static DateOperatorFactory dateOfWithTimezoneOf(String fieldReference, String timezoneField) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new DateOperatorFactory(fieldReference, Fields.field(timezoneField)); + } + + /** + * Take the date resulting from the given {@link AggregationExpression} in the UTC timezone. * * @param expression must not be {@literal null}. * @return */ public static DateOperatorFactory dateOf(AggregationExpression expression) { + return DateOperators.dateOfWithTimezone(expression, null); + } + + /** + * Take the date resulting from the given {@link AggregationExpression} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static DateOperatorFactory dateOfWithTimezone(AggregationExpression expression, @Nullable String timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new DateOperatorFactory(expression); + return new DateOperatorFactory(expression, timezone); + } + + /** + * Take the date referenced by given {@link AggregationExpression} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezoneExpression Must not be null. The expression to bind the timezone value from + * @since 2.1 + * @return + */ + public static DateOperatorFactory dateOfWithTimezoneOf(AggregationExpression expression, + AggregationExpression timezoneExpression) { + + Assert.notNull(expression, "expression must not be null!"); + Assert.notNull(timezoneExpression, "timezoneExpression must not be null!"); + return new DateOperatorFactory(expression, timezoneExpression); + } + + /** + * Take the date referenced by given {@link AggregationExpression} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezoneField Must not be null. The name of the field to bind the timezone value from + * @since 2.1 + * @return + */ + public static DateOperatorFactory dateOfWithTimezoneOf(AggregationExpression expression, String timezoneField) { + + Assert.notNull(expression, "expression must not be null!"); + return new DateOperatorFactory(expression, Fields.field(timezoneField)); + } + + /** + * Take the current date as supplied by the {@link DateFactory} in the UTC timezone + * + * @param factory not nullable. The DateFactory to get the current date from + * @since 2.1 + * @return + */ + public static DateOperatorFactory dateOf(DateFactory factory) { + return new DateOperatorFactory(factory, null); + } + + /** + * Take the current date resulting from the given {@link DateFactory} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If using Mongo prior to 3.6, + * specify timezone as null. + * + * @param factory not nullable. The DateFactory to get the current date from + * @param timezone nullable. The timezone ID or offset. If null, UTC is assumed. Must specify as null if using Mongo + * prior to 3.6 + * @since 2.1 + * @return + */ + public static DateOperatorFactory dateOfWithTimezone(DateFactory factory, @Nullable String timezone) { + return new DateOperatorFactory(factory, timezone); + } + + /** + * Take the date referenced by given {@link DateFactory} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param factory not nullable. The DateFactory to get the current date from + * @param timezoneExpression Must not be null. The expression to bind the timezone value from + * @since 2.1 + * @return + */ + public static DateOperatorFactory dateOfWithTimezoneOf(DateFactory factory, + AggregationExpression timezoneExpression) { + + Assert.notNull(factory, "Factory must not be null!"); + Assert.notNull(timezoneExpression, "timezoneExpression must not be null!"); + return new DateOperatorFactory(factory, timezoneExpression); + } + + /** + * Take the date referenced by given {@link DateFactory}in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param factory {@link DateFactory#LOCAL_DATE_FACTORY}. Defaults to {@link DateOperators#getCurrentDateFactory} if + * null. + * @param timezoneField Must not be null. The name of the field to bind the timezone value from + * @since 2.1 + * @return + */ + public static DateOperatorFactory dateOfWithTimezoneOf(DateFactory factory, String timezoneField) { + + Assert.notNull(factory, "Factory must not be null!"); + Assert.notNull(timezoneField, "timezoneField must not be null!"); + return new DateOperatorFactory(factory, Fields.field(timezoneField)); + } + + /** + * Take the current date using the default {@link DateOperators#getCurrentDateFactory()} in the UTC timezone. + * + * @since 2.1 + * @return + */ + public static DateOperatorFactory currentDate() { + return dateOf(CURRENT_DATE_FACTORY); + } + + /** + * Take the current date using the default {@link DateOperators#getCurrentDateFactory()} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. The timezone ID or offset. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static DateOperatorFactory currentDateWithTimezone(@Nullable String timezone) { + return DateOperators.dateOfWithTimezone(CURRENT_DATE_FACTORY, timezone); + } + + /** + * Take the current date using the default {@link DateOperators#getCurrentDateFactory()} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezoneExpression Must not be null. The name of the field to bind the timezone value from + * @since 2.1 + * @return + */ + public static DateOperatorFactory currentDateWithTimezoneOf(AggregationExpression timezoneExpression) { + return DateOperators.dateOfWithTimezoneOf(CURRENT_DATE_FACTORY, timezoneExpression); + } + + /** + * Take the current date using the default {@link DateOperators#getCurrentDateFactory()} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezoneField Must not be null. The name of the field to bind the timezone value from + * @since 2.1 + * @return + */ + public static DateOperatorFactory currentDateWithTimezoneOf(String timezoneField) { + return DateOperators.dateOfWithTimezoneOf(CURRENT_DATE_FACTORY, timezoneField); + } + + /** + * @see DateFromParts#fromParts + * @since 2.1 + * @author Matt Morrissette + * @return + */ + public static DateFromParts.CalendarDatePartsBuilder dateFromParts() { + return DateFromParts.fromParts(); + } + + /** + * @see DateFromParts#fromIsoWeekParts + * @since 2.1 + * @author Matt Morrissette + * @return + */ + public static DateFromParts.IsoWeekDatePartsBuilder dateFromIsoWeekParts() { + return DateFromParts.fromIsoWeekParts(); } /** * @author Christoph Strobl + * @author Matt Morrissette */ public static class DateOperatorFactory { private final String fieldReference; private final AggregationExpression expression; + private final DateFactory dateFactory; + private final Object timezone; /** - * Creates new {@link ArithmeticOperatorFactory} for given {@literal fieldReference}. + * Creates new {@link ArithmeticOperatorFactory} for given {@literal fieldReference} in the UTC timezone. * * @param fieldReference must not be {@literal null}. */ public DateOperatorFactory(String fieldReference) { + this(fieldReference, null); + } - Assert.notNull(fieldReference, "FieldReference must not be null!"); + private DateOperatorFactory(String fieldReference, Object timezone) { + Assert.hasText(fieldReference, "FieldReference must not be null!"); this.fieldReference = fieldReference; this.expression = null; + this.dateFactory = null; + this.timezone = timezone; } /** - * Creates new {@link ArithmeticOperatorFactory} for given {@link AggregationExpression}. + * Creates new {@link ArithmeticOperatorFactory} for given {@link AggregationExpression} in the UTC timezone. * * @param expression must not be {@literal null}. */ public DateOperatorFactory(AggregationExpression expression) { + this(expression, null); + } + + private DateOperatorFactory(AggregationExpression expression, Object timezone) { Assert.notNull(expression, "Expression must not be null!"); this.fieldReference = null; this.expression = expression; + this.timezone = timezone; + this.dateFactory = null; + } + + private DateOperatorFactory(@Nullable DateFactory dateFactory, Object timezone) { + + this.fieldReference = null; + this.expression = null; + this.timezone = timezone; + this.dateFactory = dateFactory != null ? dateFactory : CURRENT_DATE_FACTORY; + } + + private DateOperatorFactory(String fieldRefererence, AggregationExpression expression, DateFactory dateFactory, + Object timezone) { + + this.fieldReference = fieldRefererence; + this.expression = expression; + this.dateFactory = dateFactory; + this.timezone = timezone; + } + + /** + * @param timezone nullable. The timezone ID or offset as a String. + * @return a new DateOperator factory with the same date reference/expression/factory but with the given timezone + */ + public DateOperatorFactory withTimezone(String timezone) { + return new DateOperatorFactory(fieldReference, expression, dateFactory, timezone); + } + + /** + * @param timezoneField not nullable. The field reference to bind the timezone from. + * @return a new DateOperator factory with the same date reference/expression/factory but with the given timezone + */ + public DateOperatorFactory withTimezoneOf(String timezoneField) { + + Assert.hasText(timezoneField, "timezoneField cannot be null or empty"); + return new DateOperatorFactory(fieldReference, expression, dateFactory, Fields.field(timezoneField)); + } + + /** + * @param timezoneExpression not nullable. The expression to bind the timezone from + * @return a new DateOperator factory with the same date reference/expression/factory but with the given timezone + */ + public DateOperatorFactory withTimezoneOf(AggregationExpression timezoneExpression) { + + Assert.notNull(timezoneExpression, "timezoneExpression cannot be null or empty"); + return new DateOperatorFactory(fieldReference, expression, dateFactory, timezoneExpression); } /** * Creates new {@link AggregationExpression} that returns the day of the year for a date as a number between 1 and - * 366. + * 366 in the factory timezone (default UTC). * * @return */ public DayOfYear dayOfYear() { - return usesFieldRef() ? DayOfYear.dayOfYear(fieldReference) : DayOfYear.dayOfYear(expression); + return dayOfYear(timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the day of the year for a date as a number between 1 and + * 366 in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public DayOfYear dayOfYear(@Nullable Object timezone) { + return usesFieldRef() ? DayOfYear.dayOfYear(fieldReference, timezone) + : usesExpression() ? DayOfYear.dayOfYear(expression, timezone) : DayOfYear.dayOfYear(dateFactory, timezone); } /** * Creates new {@link AggregationExpression} that returns the day of the month for a date as a number between 1 and - * 31. + * 31 in the factory timezone (default UTC). * * @return */ public DayOfMonth dayOfMonth() { - return usesFieldRef() ? DayOfMonth.dayOfMonth(fieldReference) : DayOfMonth.dayOfMonth(expression); + return dayOfMonth(timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the day of the month for a date as a number between 1 and + * 31 in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public DayOfMonth dayOfMonth(@Nullable Object timezone) { + return usesFieldRef() ? DayOfMonth.dayOfMonth(fieldReference, timezone) + : usesExpression() ? DayOfMonth.dayOfMonth(expression, timezone) + : DayOfMonth.dayOfMonth(dateFactory, timezone); } /** * Creates new {@link AggregationExpression} that returns the day of the week for a date as a number between 1 - * (Sunday) and 7 (Saturday). + * (Sunday) and 7 (Saturday) in the factory timezone (default UTC). * * @return */ public DayOfWeek dayOfWeek() { - return usesFieldRef() ? DayOfWeek.dayOfWeek(fieldReference) : DayOfWeek.dayOfWeek(expression); + return dayOfWeek(timezone); } /** - * Creates new {@link AggregationExpression} that returns the year portion of a date. + * Creates new {@link AggregationExpression} that returns the day of the week for a date as a number between 1 + * (Sunday) and 7 (Saturday) in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public DayOfWeek dayOfWeek(@Nullable Object timezone) { + return usesFieldRef() ? DayOfWeek.dayOfWeek(fieldReference, timezone) + : usesExpression() ? DayOfWeek.dayOfWeek(expression, timezone) : DayOfWeek.dayOfWeek(dateFactory, timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the year portion of a date in the factory timezone + * (default UTC). * * @return */ public Year year() { - return usesFieldRef() ? Year.yearOf(fieldReference) : Year.yearOf(expression); + return year(timezone); } /** - * Creates new {@link AggregationExpression} that returns the month of a date as a number between 1 and 12. + * Creates new {@link AggregationExpression} that returns the year portion of a date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public Year year(@Nullable Object timezone) { + return usesFieldRef() ? Year.yearOf(fieldReference, timezone) + : usesExpression() ? Year.yearOf(expression, timezone) : Year.yearOf(dateFactory, timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the quarter of a date as a number between 1 and 4 in the + * factory timezone (default UTC). + * + * @return + */ + public Quarter quarter() { + return quarter(timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the business quarter of a date as a number between 1 and 4 + * in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public Quarter quarter(@Nullable Object timezone) { + return usesFieldRef() ? Quarter.quarterOf(fieldReference, timezone) + : usesExpression() ? Quarter.quarterOf(expression, timezone) : Quarter.quarterOf(dateFactory, timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the month of a date as a number between 1 and 12 in the + * factory timezone (default UTC). * * @return */ public Month month() { - return usesFieldRef() ? Month.monthOf(fieldReference) : Month.monthOf(expression); + return month(timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the month of a date as a number between 1 and 12 in the + * given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public Month month(@Nullable Object timezone) { + return usesFieldRef() ? Month.monthOf(fieldReference, timezone) + : usesExpression() ? Month.monthOf(expression, timezone) : Month.monthOf(dateFactory, timezone); } /** * Creates new {@link AggregationExpression} that returns the week of the year for a date as a number between 0 and - * 53. + * 53 in the factory timezone (default UTC). * * @return */ public Week week() { - return usesFieldRef() ? Week.weekOf(fieldReference) : Week.weekOf(expression); + return week(timezone); } /** - * Creates new {@link AggregationExpression} that returns the hour portion of a date as a number between 0 and 23. + * Creates new {@link AggregationExpression} that returns the week of the year for a date as a number between 0 and + * 53 in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. The timezone ID or offset. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public Week week(@Nullable Object timezone) { + return usesFieldRef() ? Week.weekOf(fieldReference, timezone) + : usesExpression() ? Week.weekOf(expression, timezone) : Week.weekOf(dateFactory, timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the hour portion of a date as a number between 0 and 23 in + * the factory timezone (default UTC). * * @return */ public Hour hour() { - return usesFieldRef() ? Hour.hourOf(fieldReference) : Hour.hourOf(expression); + return hour(timezone); } /** - * Creates new {@link AggregationExpression} that returns the minute portion of a date as a number between 0 and 59. + * Creates new {@link AggregationExpression} that returns the hour portion of a date as a number between 0 and 23 in + * the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public Hour hour(@Nullable Object timezone) { + return usesFieldRef() ? Hour.hourOf(fieldReference, timezone) + : usesExpression() ? Hour.hourOf(expression, timezone) : Hour.hourOf(dateFactory, timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the minute portion of a date as a number between 0 and 59 + * in the factory timezone (default UTC). * * @return */ public Minute minute() { - return usesFieldRef() ? Minute.minuteOf(fieldReference) : Minute.minuteOf(expression); + return minute(timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the minute portion of a date as a number between 0 and 59 + * in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public Minute minute(@Nullable Object timezone) { + return usesFieldRef() ? Minute.minuteOf(fieldReference, timezone) + : usesExpression() ? Minute.minuteOf(expression, timezone) : Minute.minuteOf(dateFactory, timezone); } /** * Creates new {@link AggregationExpression} that returns the second portion of a date as a number between 0 and 59, - * but can be 60 to account for leap seconds. + * but can be 60 to account for leap seconds in the factory timezone (default UTC). * * @return */ public Second second() { - return usesFieldRef() ? Second.secondOf(fieldReference) : Second.secondOf(expression); + return second(timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the second portion of a date as a number between 0 and 59, + * but can be 60 to account for leap seconds in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public Second second(@Nullable Object timezone) { + return usesFieldRef() ? Second.secondOf(fieldReference, timezone) + : usesExpression() ? Second.secondOf(expression, timezone) : Second.secondOf(dateFactory, timezone); } /** * Creates new {@link AggregationExpression} that returns the millisecond portion of a date as an integer between 0 - * and 999. + * and 999 in the factory timezone (default UTC). * * @return */ public Millisecond millisecond() { - return usesFieldRef() ? Millisecond.millisecondOf(fieldReference) : Millisecond.millisecondOf(expression); + return millisecond(timezone); } /** - * Creates new {@link AggregationExpression} that converts a date object to a string according to a user-specified - * {@literal format}. + * Creates new {@link AggregationExpression} that returns the millisecond portion of a date as an integer between 0 + * and 999 in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. * - * @param format must not be {@literal null}. + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 * @return */ - public DateToString toString(String format) { - return (usesFieldRef() ? DateToString.dateOf(fieldReference) : DateToString.dateOf(expression)).toString(format); + public Millisecond millisecond(@Nullable Object timezone) { + return usesFieldRef() ? Millisecond.millisecondOf(fieldReference, timezone) + : usesExpression() ? Millisecond.millisecondOf(expression, timezone) + : Millisecond.millisecondOf(dateFactory, timezone); } /** - * Creates new {@link AggregationExpression} that returns the weekday number in ISO 8601 format, ranging from 1 (for - * Monday) to 7 (for Sunday). + * Creates new {@link AggregationExpression} that returns the weekday number in ISO 8601 format, ranging from 1 + * (for Monday) to 7 (for Sunday) in the factory timezone (default UTC). * * @return */ public IsoDayOfWeek isoDayOfWeek() { - return usesFieldRef() ? IsoDayOfWeek.isoDayOfWeek(fieldReference) : IsoDayOfWeek.isoDayOfWeek(expression); + return isoDayOfWeek(timezone); } /** - * Creates new {@link AggregationExpression} that returns the week number in ISO 8601 format, ranging from 1 to 53. + * Creates new {@link AggregationExpression} that returns the weekday number in ISO 8601-2018 format, ranging from 1 + * (for Monday) to 7 (for Sunday) in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public IsoDayOfWeek isoDayOfWeek(@Nullable Object timezone) { + return usesFieldRef() ? IsoDayOfWeek.isoDayOfWeek(fieldReference, timezone) + : usesExpression() ? IsoDayOfWeek.isoDayOfWeek(expression, timezone) + : IsoDayOfWeek.isoDayOfWeek(dateFactory, timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the week number in ISO 8601 format, ranging from 1 to + * 53 in the factory timezone (default UTC). * * @return */ public IsoWeek isoWeek() { - return usesFieldRef() ? IsoWeek.isoWeekOf(fieldReference) : IsoWeek.isoWeekOf(expression); + return isoWeek(timezone); } /** - * Creates new {@link AggregationExpression} that returns the year number in ISO 8601 format. + * Creates new {@link AggregationExpression} that returns the week number in ISO 8601-2018 format, ranging from 1 to + * 53 in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public IsoWeek isoWeek(@Nullable Object timezone) { + return usesFieldRef() ? IsoWeek.isoWeekOf(fieldReference, timezone) + : usesExpression() ? IsoWeek.isoWeekOf(expression, timezone) : IsoWeek.isoWeekOf(dateFactory, timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the year number in ISO 8601 format in the factory + * timezone (default UTC). * * @return */ public IsoWeekYear isoWeekYear() { - return usesFieldRef() ? IsoWeekYear.isoWeekYearOf(fieldReference) : IsoWeekYear.isoWeekYearOf(expression); + return isoWeekYear(timezone); + } + + /** + * Creates new {@link AggregationExpression} that returns the year number in ISO 8601-2018 format in the given + * timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public IsoWeekYear isoWeekYear(@Nullable Object timezone) { + return usesFieldRef() ? IsoWeekYear.isoWeekYearOf(fieldReference, timezone) + : usesExpression() ? IsoWeekYear.isoWeekYearOf(expression, timezone) + : IsoWeekYear.isoWeekYearOf(dateFactory, timezone); + } + + /** + * Creates new {@link AggregationExpression} that converts a date object to a string according to a user-specified + * {@literal format} in the factory timezone (default UTC). + * + * @param format must not be {@literal null}. + * @since 2.1 + * @return + */ + public DateToString toString(String format) { + return toString(format, timezone); + } + + /** + * Creates new {@link AggregationExpression} that converts a date object to a string according to a user-specified + * {@literal format} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param format must not be {@literal null}. + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public DateToString toString(String format, @Nullable Object timezone) { + return (usesFieldRef() ? DateToString.dateOf(fieldReference, timezone) + : usesExpression() ? DateToString.dateOf(expression, timezone) : DateToString.dateOf(dateFactory, timezone)) + .toString(format); + } + + /** + * Creates new {@link AggregationExpression} that converts a string to a date object in the factory timezone + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @since 2.1 + * @return + */ + public DateFromString fromString() { + return fromString(timezone); + } + + /** + * Creates new {@link AggregationExpression} that converts a string to a date object in the given timezone + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public DateFromString fromString(@Nullable Object timezone) { + return usesFieldRef() ? DateFromString.dateFromString(fieldReference, timezone) + : usesExpression() ? DateFromString.dateFromString(expression, timezone) + : DateFromString.dateFromString(dateFactory, timezone); + } + + /** + * Creates new {@link AggregationExpression} that converts a string to a date object in the factory timezone using + * calendar parts (year/month/day) + *

+ * WARNING: Mongo 3.6+ only + * + * @since 2.1 + * @return + */ + public DateToParts toParts() { + return toParts(timezone, null); + } + + /** + * Creates new {@link AggregationExpression} that converts a string to a date object in the factory timezone using + * isoWeek parts (isoWeekYear/isoWeek/isoDayOfWeek) + *

+ * WARNING: Mongo 3.6+ only + * + * @since 2.1 + * @return + */ + public DateToParts toIsoWeekParts() { + return toParts(timezone, true); + } + + /** + * Creates new {@link AggregationExpression} that converts a string to a date object in the factory timezone + *

+ * WARNING: Mongo 3.6+ only + * + * @param iso8601 If set to true, modifies the output document to use ISO week date fields. Defaults to false. + * @since 2.1 + * @return + */ + public DateToParts toParts(Boolean iso8601) { + return toParts(timezone, iso8601); + } + + /** + * Creates new {@link AggregationExpression} that converts a string to a date object in the given timezone using + * calendar parts (year/month/day) + *

+ * WARNING: Mongo 3.6+ only + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public DateToParts toParts(@Nullable Object timezone) { + return toParts(timezone, null); + } + + /** + * Creates new {@link AggregationExpression} that converts a string to a date object in the given timezone using + * isoWeek parts (isoWeekYear/isoWeek/isoDayOfWeek) + *

+ * WARNING: Mongo 3.6+ only + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @since 2.1 + * @return + */ + public DateToParts toIsoWeekParts(@Nullable Object timezone) { + return toParts(timezone, true); + } + + /** + * Creates new {@link AggregationExpression} that converts a string to a date object in the given timezone + *

+ * WARNING: Mongo 3.6+ only + * + * @param timezone nullable. Overrides factory timezone. The timezone ID or offset as a String. Also accepts a + * {@link AggregationExpression} or {@link Field}. If null UTC is assumed. + * @param iso8601 If set to true, modifies the output document to use ISO week date fields + * (isoWeekYear/isoWeek/isoDayOfWeek). Defaults to false. + * @since 2.1 + * @return + */ + public DateToParts toParts(@Nullable Object timezone, @Nullable Boolean iso8601) { + return usesFieldRef() ? DateToParts.dateToParts(fieldReference, timezone, iso8601) + : usesExpression() ? DateToParts.dateToParts(expression, timezone, iso8601) + : DateToParts.dateToParts(dateFactory, timezone, iso8601); } private boolean usesFieldRef() { return fieldReference != null; } + + private boolean usesExpression() { + return expression != null; + } + } + + // contemplates support for the future functionality described in: + // https://jira.mongodb.org/browse/SERVER-23656 + private static DateFactory CURRENT_DATE_FACTORY = DateFactory.LOCAL_DATE_FACTORY; + + /** + * Sets the {@link DateFactory} used by {@link DateOperators#currentDate()} and + * {@link DateOperators#currentDate(java.lang.String)} for the entire application (statically). + * + * @param defaultFactory + */ + public static void setCurrentDateFactory(final DateFactory defaultFactory) { + + Assert.notNull(defaultFactory, "Default DateFactory cannot be null"); + CURRENT_DATE_FACTORY = defaultFactory; + } + + /** + * @return the {@link DateFactory} used by {@link DateOperators#currentDate()} and + * {@link DateOperators#currentDate(java.lang.String)} + */ + public static DateFactory getCurrentDateFactory() { + return CURRENT_DATE_FACTORY; + } + + /** + * New in Mongo 3.6 is support for timezone specification with all date types. + *

+ * WARNING: Using timezone requires Mongo 3.6+ and will error on prior versions of Mongo + * + * @since 2.1 + */ + private abstract static class DateAggregationExpression extends AbstractAggregationExpression { + + protected DateAggregationExpression(Object date, @Nullable Object timezone) { + super(arguments(date, timezone)); + } + + private static Object arguments(Object date, @Nullable Object timezone) { + + if (timezone != null) { + Assert.isTrue(DateAggregationExpression.isValidTimezoneObject(timezone), + () -> "Timezone was not a valid timezone: " + timezone + + ". Must be String, AggregationExpression or Field"); + + java.util.Map args = new LinkedHashMap<>(4); + args.put("date", date); + args.put("timezone", timezone); + return args; + } else { + return date; + } + } + + public static boolean isValidTimezoneObject(Object timezone) { + return timezone == null + || (timezone instanceof String || timezone instanceof Field || timezone instanceof AggregationExpression); + } + } /** * {@link AggregationExpression} for {@code $dayOfYear}. * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dayOfYear/ */ - public static class DayOfYear extends AbstractAggregationExpression { + public static class DayOfYear extends DateAggregationExpression { - private DayOfYear(Object value) { - super(value); + private DayOfYear(Object value, Object timezone) { + super(value, timezone); } @Override @@ -241,27 +984,74 @@ public class DateOperators { } /** - * Creates new {@link DayOfYear}. + * Creates new {@link DayOfYear} in UTC. * * @param fieldReference must not be {@literal null}. * @return */ public static DayOfYear dayOfYear(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new DayOfYear(Fields.field(fieldReference)); + return dayOfYear(fieldReference, null); } /** - * Creates new {@link DayOfYear}. + * Creates new {@link DayOfYear} for the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static DayOfYear dayOfYear(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new DayOfYear(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link DayOfYear} in UTC. * * @param expression must not be {@literal null}. * @return */ public static DayOfYear dayOfYear(AggregationExpression expression) { + return dayOfYear(expression, null); + } + + /** + * Creates new {@link DayOfYear} for the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static DayOfYear dayOfYear(AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new DayOfYear(expression); + return new DayOfYear(expression, timezone); + } + + /** + * Creates new {@link DayOfYear} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static DayOfYear dayOfYear(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new DayOfYear(dateFactory, timezone); } } @@ -269,11 +1059,12 @@ public class DateOperators { * {@link AggregationExpression} for {@code $dayOfMonth}. * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dayOfMonth/ */ - public static class DayOfMonth extends AbstractAggregationExpression { + public static class DayOfMonth extends DateAggregationExpression { - private DayOfMonth(Object value) { - super(value); + private DayOfMonth(Object value, Object timezone) { + super(value, timezone); } @Override @@ -282,27 +1073,75 @@ public class DateOperators { } /** - * Creates new {@link DayOfMonth}. + * Creates new {@link DayOfMonth} in UTC. * * @param fieldReference must not be {@literal null}. * @return */ public static DayOfMonth dayOfMonth(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new DayOfMonth(Fields.field(fieldReference)); + return dayOfMonth(fieldReference, null); } /** - * Creates new {@link DayOfMonth}. + * Creates new {@link DayOfMonth} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static DayOfMonth dayOfMonth(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new DayOfMonth(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link DayOfMonth} in UTC. * * @param expression must not be {@literal null}. * @return */ public static DayOfMonth dayOfMonth(AggregationExpression expression) { + return dayOfMonth(expression, null); + } + + /** + * Creates new {@link DayOfMonth} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static DayOfMonth dayOfMonth(AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new DayOfMonth(expression); + return new DayOfMonth(expression, timezone); + } + + /** + * Creates new {@link DayOfMonth} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static DayOfMonth dayOfMonth(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new DayOfMonth(dateFactory, timezone); } } @@ -310,11 +1149,12 @@ public class DateOperators { * {@link AggregationExpression} for {@code $dayOfWeek}. * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dayOfWeek/ */ - public static class DayOfWeek extends AbstractAggregationExpression { + public static class DayOfWeek extends DateAggregationExpression { - private DayOfWeek(Object value) { - super(value); + private DayOfWeek(Object value, Object timezone) { + super(value, timezone); } @Override @@ -329,9 +1169,24 @@ public class DateOperators { * @return */ public static DayOfWeek dayOfWeek(String fieldReference) { + return dayOfWeek(fieldReference, null); + } - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new DayOfWeek(Fields.field(fieldReference)); + /** + * Creates new {@link DayOfWeek} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static DayOfWeek dayOfWeek(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new DayOfWeek(Fields.field(fieldReference), timezone); } /** @@ -341,9 +1196,42 @@ public class DateOperators { * @return */ public static DayOfWeek dayOfWeek(AggregationExpression expression) { + return dayOfWeek(expression, null); + } + + /** + * Creates new {@link DayOfWeek} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static DayOfWeek dayOfWeek(AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new DayOfWeek(expression); + return new DayOfWeek(expression, timezone); + } + + /** + * Creates new {@link DayOfWeek} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static DayOfWeek dayOfWeek(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new DayOfWeek(dateFactory, timezone); } } @@ -351,11 +1239,12 @@ public class DateOperators { * {@link AggregationExpression} for {@code $year}. * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/year/ */ - public static class Year extends AbstractAggregationExpression { + public static class Year extends DateAggregationExpression { - private Year(Object value) { - super(value); + private Year(Object value, Object timezone) { + super(value, timezone); } @Override @@ -364,27 +1253,174 @@ public class DateOperators { } /** - * Creates new {@link Year}. + * Creates new {@link Year} in the UTC timezone. * * @param fieldReference must not be {@literal null}. * @return */ public static Year yearOf(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new Year(Fields.field(fieldReference)); + return yearOf(fieldReference, null); } /** - * Creates new {@link Year}. + * Creates new {@link Year} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Year yearOf(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new Year(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link Year} in the UTC timezone. * * @param expression must not be {@literal null}. * @return */ public static Year yearOf(AggregationExpression expression) { + return yearOf(expression, null); + } + + /** + * Creates new {@link Year} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Year yearOf(AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new Year(expression); + return new Year(expression, timezone); + } + + /** + * Creates new {@link Year} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static Year yearOf(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new Year(dateFactory, timezone); + } + } + + /** + * Pseudo {@link AggregationExpression} to represent the current business quarter using conditionals. Can be used in a + * $group aggregation state to group results by business quarter + * + * @author Matt Morrissette + */ + public static class Quarter implements AggregationExpression { + + private final Cond cond; + + private Quarter(Object value, Object timezone) { + + final Month month = new Month(value, timezone); + cond = when(quarterConditional(month, 3)).then(1).otherwiseValueOf(when(quarterConditional(month, 6)).then(2) + .otherwiseValueOf(when(quarterConditional(month, 9)).then(3).otherwise(4))); + } + + private static AggregationExpression quarterConditional(final Month month, int mininumMonth) { + return ComparisonOperators.valueOf(month).lessThanEqualToValue(mininumMonth); + } + + /** + * Creates new {@link Quarter} in the UTC timezone. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static Quarter quarterOf(String fieldReference) { + return quarterOf(fieldReference, null); + } + + /** + * Creates new {@link Quarter} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Quarter quarterOf(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new Quarter(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link Quarter} in the UTC timezone. + * + * @param expression must not be {@literal null}. + * @return + */ + public static Quarter quarterOf(AggregationExpression expression) { + return quarterOf(expression, null); + } + + /** + * Creates new {@link Quarter} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Quarter quarterOf(AggregationExpression expression, @Nullable Object timezone) { + + Assert.notNull(expression, "Expression must not be null!"); + return new Quarter(expression, timezone); + } + + /** + * Creates new {@link Quarter} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static Quarter quarterOf(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new Quarter(dateFactory, timezone); + } + + @Override + public Document toDocument(AggregationOperationContext context) { + return cond.toDocument(context); } } @@ -392,11 +1428,12 @@ public class DateOperators { * {@link AggregationExpression} for {@code $month}. * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/month/ */ - public static class Month extends AbstractAggregationExpression { + public static class Month extends DateAggregationExpression { - private Month(Object value) { - super(value); + private Month(Object value, Object timezone) { + super(value, timezone); } @Override @@ -405,39 +1442,88 @@ public class DateOperators { } /** - * Creates new {@link Month}. + * Creates new {@link Month} in the UTC timezone. * * @param fieldReference must not be {@literal null}. * @return */ public static Month monthOf(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new Month(Fields.field(fieldReference)); + return monthOf(fieldReference, null); } /** - * Creates new {@link Month}. + * Creates new {@link Month} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Month monthOf(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new Month(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link Month} in the UTC timezone. * * @param expression must not be {@literal null}. * @return */ public static Month monthOf(AggregationExpression expression) { + return monthOf(expression, null); + } + + /** + * Creates new {@link Month} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Month monthOf(AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new Month(expression); + return new Month(expression, timezone); + } + + /** + * Creates new {@link Month} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static Month monthOf(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new Month(dateFactory, timezone); } } /** - * {@link AggregationExpression} for {@code $week}. + * {@link AggregationExpression} for {@code $week}. This behavior is the same as the “%U” operator to the strftime * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/week/ */ - public static class Week extends AbstractAggregationExpression { + public static class Week extends DateAggregationExpression { - private Week(Object value) { - super(value); + private Week(Object value, Object timezone) { + super(value, timezone); } @Override @@ -446,27 +1532,75 @@ public class DateOperators { } /** - * Creates new {@link Week}. + * Creates new {@link Week} in the UTC timezone. * * @param fieldReference must not be {@literal null}. * @return */ public static Week weekOf(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new Week(Fields.field(fieldReference)); + return weekOf(fieldReference, null); } /** - * Creates new {@link Week}. + * Creates new {@link Week} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Week weekOf(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new Week(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link Week} in the UTC timezone. * * @param expression must not be {@literal null}. * @return */ public static Week weekOf(AggregationExpression expression) { + return weekOf(expression, null); + } + + /** + * Creates new {@link Week} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Week weekOf(AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new Week(expression); + return new Week(expression, timezone); + } + + /** + * Creates new {@link Week} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static Week weekOf(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new Week(dateFactory, timezone); } } @@ -474,11 +1608,12 @@ public class DateOperators { * {@link AggregationExpression} for {@code $hour}. * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/hour/ */ - public static class Hour extends AbstractAggregationExpression { + public static class Hour extends DateAggregationExpression { - private Hour(Object value) { - super(value); + private Hour(Object value, Object timezone) { + super(value, timezone); } @Override @@ -487,27 +1622,75 @@ public class DateOperators { } /** - * Creates new {@link Hour}. + * Creates new {@link Hour} in the UTC timezone. * * @param fieldReference must not be {@literal null}. * @return */ public static Hour hourOf(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new Hour(Fields.field(fieldReference)); + return hourOf(fieldReference, null); } /** - * Creates new {@link Hour}. + * Creates new {@link Hour} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Hour hourOf(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new Hour(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link Hour} in the UTC timezone. * * @param expression must not be {@literal null}. * @return */ public static Hour hourOf(AggregationExpression expression) { + return hourOf(expression, null); + } + + /** + * Creates new {@link Hour} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Hour hourOf(AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new Hour(expression); + return new Hour(expression, timezone); + } + + /** + * Creates new {@link Hour} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static Hour hourOf(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new Hour(dateFactory, timezone); } } @@ -515,11 +1698,12 @@ public class DateOperators { * {@link AggregationExpression} for {@code $minute}. * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/minute/ */ - public static class Minute extends AbstractAggregationExpression { + public static class Minute extends DateAggregationExpression { - private Minute(Object value) { - super(value); + private Minute(Object value, Object timezone) { + super(value, timezone); } @Override @@ -528,27 +1712,75 @@ public class DateOperators { } /** - * Creates new {@link Minute}. + * Creates new {@link Minute} in the UTC timezone. * * @param fieldReference must not be {@literal null}. * @return */ public static Minute minuteOf(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new Minute(Fields.field(fieldReference)); + return minuteOf(fieldReference, null); } /** - * Creates new {@link Minute}. + * Creates new {@link Minute} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Minute minuteOf(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new Minute(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link Minute} in the UTC timezone. * * @param expression must not be {@literal null}. * @return */ public static Minute minuteOf(AggregationExpression expression) { + return minuteOf(expression, null); + } + + /** + * Creates new {@link Minute} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Minute minuteOf(AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new Minute(expression); + return new Minute(expression, timezone); + } + + /** + * Creates new {@link Minute} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static Minute minuteOf(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new Minute(dateFactory, timezone); } } @@ -556,11 +1788,12 @@ public class DateOperators { * {@link AggregationExpression} for {@code $second}. * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/second/ */ - public static class Second extends AbstractAggregationExpression { + public static class Second extends DateAggregationExpression { - private Second(Object value) { - super(value); + private Second(Object value, Object timezone) { + super(value, timezone); } @Override @@ -569,27 +1802,75 @@ public class DateOperators { } /** - * Creates new {@link Second}. + * Creates new {@link Second} in the UTC timezone. * * @param fieldReference must not be {@literal null}. * @return */ public static Second secondOf(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new Second(Fields.field(fieldReference)); + return secondOf(fieldReference, null); } /** - * Creates new {@link Second}. + * Creates new {@link Second} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Second secondOf(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new Second(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link Second} in the UTC timezone. * * @param expression must not be {@literal null}. * @return */ public static Second secondOf(AggregationExpression expression) { + return secondOf(expression, null); + } + + /** + * Creates new {@link Second} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Second secondOf(AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new Second(expression); + return new Second(expression, timezone); + } + + /** + * Creates new {@link Second} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static Second secondOf(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new Second(dateFactory, timezone); } } @@ -597,11 +1878,12 @@ public class DateOperators { * {@link AggregationExpression} for {@code $millisecond}. * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/millisecond/ */ - public static class Millisecond extends AbstractAggregationExpression { + public static class Millisecond extends DateAggregationExpression { - private Millisecond(Object value) { - super(value); + private Millisecond(Object value, Object timezone) { + super(value, timezone); } @Override @@ -610,27 +1892,346 @@ public class DateOperators { } /** - * Creates new {@link Millisecond}. + * Creates new {@link Millisecond} in the UTC timezone. * * @param fieldReference must not be {@literal null}. * @return */ public static Millisecond millisecondOf(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new Millisecond(Fields.field(fieldReference)); + return millisecondOf(fieldReference, null); } /** - * Creates new {@link Millisecond}. + * Creates new {@link Millisecond} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Millisecond millisecondOf(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new Millisecond(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link Millisecond} in the UTC timezone. * * @param expression must not be {@literal null}. * @return */ public static Millisecond millisecondOf(AggregationExpression expression) { + return millisecondOf(expression, null); + } + + /** + * Creates new {@link Millisecond} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static Millisecond millisecondOf(AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); - return new Millisecond(expression); + return new Millisecond(expression, timezone); + } + + /** + * Creates new {@link Millisecond} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static Millisecond millisecondOf(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new Millisecond(dateFactory, timezone); + } + } + + /** + * {@link AggregationExpression} for {@code $isoDayOfWeek}. + * + * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/isoDayOfWeek/ + */ + public static class IsoDayOfWeek extends DateAggregationExpression { + + private IsoDayOfWeek(Object value, Object timezone) { + super(value, timezone); + } + + @Override + protected String getMongoMethod() { + return "$isoDayOfWeek"; + } + + /** + * Creates new {@link IsoDayOfWeek} in the UTC timezone. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static IsoDayOfWeek isoDayOfWeek(String fieldReference) { + return isoDayOfWeek(fieldReference, null); + } + + /** + * Creates new {@link IsoDayOfWeek} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @since 2.1 + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @return + */ + public static IsoDayOfWeek isoDayOfWeek(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new IsoDayOfWeek(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link IsoDayOfWeek} in the UTC timezone. + * + * @param expression must not be {@literal null}. + * @return + */ + public static IsoDayOfWeek isoDayOfWeek(AggregationExpression expression) { + return isoDayOfWeek(expression, null); + } + + /** + * Creates new {@link IsoDayOfWeek} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static IsoDayOfWeek isoDayOfWeek(AggregationExpression expression, @Nullable Object timezone) { + + Assert.notNull(expression, "Expression must not be null!"); + return new IsoDayOfWeek(expression, timezone); + } + + /** + * Creates new {@link IsoDayOfWeek} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static IsoDayOfWeek isoDayOfWeek(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new IsoDayOfWeek(dateFactory, timezone); + } + + } + + /** + * {@link AggregationExpression} for {@code $isoWeek}. + * + * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/isoWeek/ + */ + public static class IsoWeek extends DateAggregationExpression { + + private IsoWeek(Object value, Object timezone) { + super(value, timezone); + } + + @Override + protected String getMongoMethod() { + return "$isoWeek"; + } + + /** + * Creates new {@link IsoWeek} in the UTC timezone. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static IsoWeek isoWeekOf(String fieldReference) { + return isoWeekOf(fieldReference, null); + } + + /** + * Creates new {@link IsoWeek} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static IsoWeek isoWeekOf(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new IsoWeek(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link IsoWeek} in the UTC timezone. + * + * @param expression must not be {@literal null}. + * @return + */ + public static IsoWeek isoWeekOf(AggregationExpression expression) { + return isoWeekOf(expression, null); + } + + /** + * Creates new {@link IsoWeek} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static IsoWeek isoWeekOf(AggregationExpression expression, @Nullable Object timezone) { + + Assert.notNull(expression, "Expression must not be null!"); + return new IsoWeek(expression, timezone); + } + + /** + * Creates new {@link IsoWeek} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static IsoWeek isoWeekOf(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new IsoWeek(dateFactory, timezone); + } + } + + /** + * {@link AggregationExpression} for {@code $isoWeekYear}. + * + * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/isoWeekYear/ + */ + public static class IsoWeekYear extends DateAggregationExpression { + + private IsoWeekYear(Object value, Object timezone) { + super(value, timezone); + } + + @Override + protected String getMongoMethod() { + return "$isoWeekYear"; + } + + /** + * Creates new {@link IsoWeekYear} in the UTC timezone. + * + * @param fieldReference must not be {@literal null}. + * @return + */ + public static IsoWeekYear isoWeekYearOf(String fieldReference) { + return isoWeekYearOf(fieldReference, null); + } + + /** + * Creates new {@link IsoWeekYear} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static IsoWeekYear isoWeekYearOf(String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); + return new IsoWeekYear(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link Millisecond} in the UTC timezone. + * + * @param expression must not be {@literal null}. + * @return + */ + public static IsoWeekYear isoWeekYearOf(AggregationExpression expression) { + return isoWeekYearOf(expression, null); + } + + /** + * Creates new {@link Millisecond} in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static IsoWeekYear isoWeekYearOf(AggregationExpression expression, @Nullable Object timezone) { + + Assert.notNull(expression, "Expression must not be null!"); + return new IsoWeekYear(expression, timezone); + } + + /** + * Creates new {@link IsoWeekYear} for current date in the given timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static IsoWeekYear isoWeekYearOf(DateFactory dateFactory, @Nullable Object timezone) { + + Assert.notNull(dateFactory, "dateFactory must not be null!"); + return new IsoWeekYear(dateFactory, timezone); } } @@ -638,6 +2239,7 @@ public class DateOperators { * {@link AggregationExpression} for {@code $dateToString}. * * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateToString/ */ public static class DateToString extends AbstractAggregationExpression { @@ -651,187 +2253,986 @@ public class DateOperators { } /** - * Creates new {@link FormatBuilder} allowing to define the date format to apply. + * Creates new {@link FormatBuilder} allowing to define the date format to apply in the UTC timezone * * @param fieldReference must not be {@literal null}. * @return */ public static FormatBuilder dateOf(final String fieldReference) { + return dateOf(fieldReference, null); + } - Assert.notNull(fieldReference, "FieldReference must not be null!"); + /** + * Creates new {@link FormatBuilder} allowing to define the date format to apply in the specified timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static FormatBuilder dateOf(final String fieldReference, @Nullable Object timezone) { + + Assert.hasText(fieldReference, "FieldReference must not be null!"); return new FormatBuilder() { @Override public DateToString toString(String format) { + return toString(format, timezone); + } + + @Override + public DateToString toString(String format, @Nullable Object timezone) { Assert.notNull(format, "Format must not be null!"); - return new DateToString(argumentMap(Fields.field(fieldReference), format)); + return new DateToString(argumentMap(Fields.field(fieldReference), format, timezone)); } }; } /** - * Creates new {@link FormatBuilder} allowing to define the date format to apply. + * Creates new {@link FormatBuilder} allowing to define the date format to apply in the UTC timezone. * * @param expression must not be {@literal null}. * @return */ public static FormatBuilder dateOf(final AggregationExpression expression) { + return dateOf(expression, null); + } + + /** + * Creates new {@link FormatBuilder} allowing to define the date format to apply in the specified timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + public static FormatBuilder dateOf(final AggregationExpression expression, @Nullable Object timezone) { Assert.notNull(expression, "Expression must not be null!"); + Assert.isTrue(DateAggregationExpression.isValidTimezoneObject(timezone), + () -> "Timezone was not a valid timezone: " + timezone + ". Must be String, AggregationExpression or Field"); return new FormatBuilder() { @Override public DateToString toString(String format) { + return toString(format, timezone); + } + @Override + public DateToString toString(String format, @Nullable Object timezone) { Assert.notNull(format, "Format must not be null!"); - return new DateToString(argumentMap(expression, format)); + return new DateToString(argumentMap(expression, format, timezone)); } }; } - private static java.util.Map argumentMap(Object date, String format) { + /** + * Creates new {@link FormatBuilder} allowing to define the date format to apply in the specified timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. If Mongo is less than 3.6, + * timezone must be null. + * + * @param dateFactory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. (supports Mongo prior to 3.6 if null) + * @since 2.1 + * @return + */ + public static FormatBuilder dateOf(final DateFactory dateFactory, @Nullable Object timezone) { - java.util.Map args = new LinkedHashMap(2); - args.put("format", format); + Assert.notNull(dateFactory, "CurrentDateFactory must not be null!"); + Assert.isTrue(DateAggregationExpression.isValidTimezoneObject(timezone), + () -> "Timezone was not a valid timezone: " + timezone + ". Must be String, AggregationExpression or Field"); + + return new FormatBuilder() { + + @Override + public DateToString toString(String format) { + return toString(format, timezone); + } + + @Override + public DateToString toString(String format, @Nullable Object timezone) { + Assert.notNull(format, "Format must not be null!"); + return new DateToString(argumentMap(dateFactory, format, timezone)); + } + }; + } + + private static java.util.Map argumentMap(Object date, String format, @Nullable Object timezone) { + + java.util.Map args = new LinkedHashMap<>(5); args.put("date", date); + args.put("format", format); + if (timezone != null) { + Assert.isTrue(DateAggregationExpression.isValidTimezoneObject(timezone), + () -> "Timezone was not a valid timezone: " + timezone + + ". Must be String, AggregationExpression or Field"); + args.put("timezone", timezone); + } return args; } public interface FormatBuilder { /** - * Creates new {@link DateToString} with all previously added arguments appending the given one. + * Creates new {@link DateToString} with all previously added arguments appending the given one in the builder + * timezone (default UTC). * * @param format must not be {@literal null}. * @return */ DateToString toString(String format); + + /** + * Creates new {@link DateToString} with all previously added arguments appending the given one in the given + * timezone. + *

+ * WARNING: Mongo 3.6+ only. Using timezone on prior Mongo versions will cause errors. + * + * @param format must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} + * or {@link Field}. If null, UTC is assumed. + * @since 2.1 + * @return + */ + DateToString toString(String format, @Nullable Object timezone); } } /** - * {@link AggregationExpression} for {@code $isoDayOfWeek}. + * {@link AggregationExpression} for {@code $dateFromString}. + *

+ * WARNING: Mongo 3.6+ only. * - * @author Christoph Strobl + * @since 2.1 + * @author Matt Morrissette + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/ */ - public static class IsoDayOfWeek extends AbstractAggregationExpression { + public static class DateFromString extends AbstractAggregationExpression { - private IsoDayOfWeek(Object value) { - super(value); + private DateFromString(Object value, Object timezone) { + super(argumentMap(value, timezone)); + } + + private static Map argumentMap(final Object value, final Object timezone) { + + final Map vals = new LinkedHashMap<>(4); + vals.put("dateString", value); + if (timezone != null) { + Assert.isTrue(DateAggregationExpression.isValidTimezoneObject(timezone), + () -> "Timezone was not a valid timezone: " + timezone + + ". Must be String, AggregationExpression or Field"); + vals.put("timezone", timezone); + } + return vals; } @Override protected String getMongoMethod() { - return "$isoDayOfWeek"; + return "$dateFromString"; } /** - * Creates new {@link IsoDayOfWeek}. + * Creates new {@link DateFromString} in the UTC timezone for a date referencing a field. * * @param fieldReference must not be {@literal null}. * @return */ - public static IsoDayOfWeek isoDayOfWeek(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new IsoDayOfWeek(Fields.field(fieldReference)); + public static DateFromString dateFromString(final String fieldReference) { + return dateFromString(fieldReference, null); } /** - * Creates new {@link IsoDayOfWeek}. + * Creates new {@link DateFromString} in the given timezone for a date referencing a field. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @return + */ + public static DateFromString dateFromString(final String fieldReference, @Nullable final Object timezone) { + + Assert.notNull(fieldReference, "fieldReference must not be null!"); + return new DateFromString(Fields.field(fieldReference), timezone); + } + + /** + * Creates new {@link DateFromString} in the given timezone for a date from evaluating an AggregationExpression. * * @param expression must not be {@literal null}. * @return */ - public static IsoDayOfWeek isoDayOfWeek(AggregationExpression expression) { + public static DateFromString dateFromString(final AggregationExpression expression) { + return dateFromString(expression, null); + } - Assert.notNull(expression, "Expression must not be null!"); - return new IsoDayOfWeek(expression); + /** + * Creates new {@link DateFromString} in the given timezone for a date from evaluating an AggregationExpression. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @return + */ + public static DateFromString dateFromString(final AggregationExpression expression, + @Nullable final Object timezone) { + + Assert.notNull(expression, "expression must not be null!"); + return new DateFromString(expression, timezone); + } + + /** + * Creates new {@link DateFromString} in the given timezone for a date provided by the given factory. + * + * @param factory must not be {@literal null}. + * @return + */ + public static DateFromString dateFromString(final DateFactory factory) { + return dateFromString(factory, null); + } + + /** + * Creates new {@link DateFromString} in the given timezone for a date provided by the given factory. + * + * @param factory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @return + */ + public static DateFromString dateFromString(final DateFactory factory, final @Nullable Object timezone) { + + Assert.notNull(factory, "factory must not be null!"); + return new DateFromString(factory, timezone); } } /** - * {@link AggregationExpression} for {@code $isoWeek}. + * {@link AggregationExpression} for {@code $dateToParts}. + *

+ * WARNING: Mongo 3.6+ only. * - * @author Christoph Strobl + * @since 2.1 + * @author Matt Morrissette + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/ */ - public static class IsoWeek extends AbstractAggregationExpression { + public static class DateToParts extends AbstractAggregationExpression { - private IsoWeek(Object value) { - super(value); + private DateToParts(Object value, Object timezone, Boolean iso8601) { + super(argumentMap(value, timezone, iso8601)); + } + + private static Map argumentMap(final Object value, Object timezone, Boolean iso8601) { + + final Map vals = new LinkedHashMap<>(6); + vals.put("date", value); + if (timezone != null) { + if (timezone instanceof Boolean) { + // iso8601 passed as second argument + if (iso8601 == null) { + iso8601 = (Boolean) timezone; + } else { + throw new IllegalArgumentException( + "Timezone was not a valid timezone: " + timezone + ". Must be String, AggregationExpression or Field"); + } + } else { + Assert.isTrue(DateAggregationExpression.isValidTimezoneObject(timezone), + () -> "Timezone was not a valid timezone: " + timezone + + ". Must be String, AggregationExpression or Field"); + vals.put("timezone", timezone); + } + } + if (iso8601 != null) { + vals.put("iso8601", iso8601); + } + return vals; } @Override protected String getMongoMethod() { - return "$isoWeek"; + return "$dateToParts"; } /** - * Creates new {@link IsoWeek}. + * Creates new {@link DateToParts} in the UTC timezone. * * @param fieldReference must not be {@literal null}. * @return */ - public static IsoWeek isoWeekOf(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new IsoWeek(Fields.field(fieldReference)); + public static DateToParts dateToParts(final String fieldReference) { + return dateToParts(fieldReference, null, null); } /** - * Creates new {@link IsoWeek}. + * Creates new {@link DateToParts} in the given timezone. + * + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @param fieldReference must not be {@literal null}. + * @return + */ + public static DateToParts dateToParts(final String fieldReference, final Object timezone) { + return dateToParts(fieldReference, timezone, null); + } + + /** + * Creates new {@link DateToParts} in the given timezone. + * + * @param fieldReference must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @param iso8601 If set to true, modifies the output document to use ISO week date fields. Defaults to false. + * @return + */ + public static DateToParts dateToParts(final String fieldReference, @Nullable final Object timezone, + @Nullable final Boolean iso8601) { + + Assert.notNull(fieldReference, "fieldReference must not be null!"); + return new DateToParts(Fields.field(fieldReference), timezone, iso8601); + } + + /** + * Creates new {@link DateToParts} in the UTC timezone. * * @param expression must not be {@literal null}. * @return */ - public static IsoWeek isoWeekOf(AggregationExpression expression) { - - Assert.notNull(expression, "Expression must not be null!"); - return new IsoWeek(expression); + public static DateToParts dateToParts(final AggregationExpression expression) { + return dateToParts(expression, null, null); } + + /** + * Creates new {@link DateToParts} in the given timezone. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @return + */ + public static DateToParts dateToParts(final AggregationExpression expression, @Nullable final Object timezone) { + return dateToParts(expression, timezone, null); + } + + /** + * Creates new {@link DateToParts} in the given timezone. + * + * @param expression must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @param iso8601 If set to true, modifies the output document to use ISO week date fields. Defaults to false. + * @return + */ + public static DateToParts dateToParts(final AggregationExpression expression, @Nullable final Object timezone, + final Boolean iso8601) { + + Assert.notNull(expression, "expression must not be null!"); + return new DateToParts(expression, timezone, iso8601); + } + + /** + * Creates new {@link DateToParts} in the UTC timezone. + * + * @param factory must not be {@literal null}. + * @return + */ + public static DateToParts dateToParts(final DateFactory factory) { + return dateToParts(factory, null, null); + } + + /** + * Creates new {@link DateToParts} in the given timezone. + * + * @param factory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @return + */ + public static DateToParts dateToParts(final DateFactory factory, final @Nullable Object timezone) { + return dateToParts(factory, timezone, null); + } + + /** + * Creates new {@link DateToParts} in the given timezone. + * + * @param factory must not be {@literal null}. + * @param timezone nullable. The timezone ID or offset as a String. Also accepts a {@link AggregationExpression} or + * {@link Field}. If null, UTC is assumed. + * @param iso8601 If set to true, modifies the output document to use ISO week date fields. Defaults to false. + * @return + */ + public static DateToParts dateToParts(final DateFactory factory, final @Nullable Object timezone, + @Nullable final Boolean iso8601) { + + Assert.notNull(factory, "factory must not be null!"); + return new DateToParts(factory, timezone, iso8601); + } + } /** - * {@link AggregationExpression} for {@code $isoWeekYear}. + * AggregationExpression for '$dateFromParts' * - * @author Christoph Strobl + * @author matt.morrissette + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/ */ - public static class IsoWeekYear extends AbstractAggregationExpression { + public static class DateFromParts extends AbstractAggregationExpression { - private IsoWeekYear(Object value) { - super(value); + private DateFromParts(boolean isoWeek, Object yearOrIsoWeekYear, Object monthOrIsoWeek, Object dayOrIsoDayOfWeek, + Object hour, Object minute, Object second, Object millisecond, Object timezone) { + super(isoWeek + ? isoWeekMap(yearOrIsoWeekYear, monthOrIsoWeek, dayOrIsoDayOfWeek, hour, minute, second, millisecond, + timezone) + : calMap(yearOrIsoWeekYear, monthOrIsoWeek, dayOrIsoDayOfWeek, hour, minute, second, millisecond, timezone)); } @Override protected String getMongoMethod() { - return "$isoWeekYear"; + return "$dateFromParts"; } /** - * Creates new {@link IsoWeekYear}. - * - * @param fieldReference must not be {@literal null}. - * @return + * @return a new builder for {@link DateFromParts} using year/month/day + *

+ * year is required + *

+ * Timezone defaults to UTC if not specified + *

+ * year and month default to 1 if not specified. All other fields default to 0. */ - public static IsoWeekYear isoWeekYearOf(String fieldReference) { - - Assert.notNull(fieldReference, "FieldReference must not be null!"); - return new IsoWeekYear(Fields.field(fieldReference)); + public static CalendarDatePartsBuilder fromParts() { + return new CalendarDatePartsBuilder(); } /** - * Creates new {@link Millisecond}. - * - * @param expression must not be {@literal null}. - * @return + * @return a new builder for {@link DateFromParts} using isoWeekYear/isoWeek/isoDayOfWeek + *

+ * isoWeekYear is required + *

+ * Timezone defaults to UTC if not specified + *

+ * isoWeek and isoDayOfWeek default to 1 if not specified. All other fields default to 0. */ - public static IsoWeekYear isoWeekYearOf(AggregationExpression expression) { + public static IsoWeekDatePartsBuilder fromIsoWeekParts() { + return new IsoWeekDatePartsBuilder(); + } - Assert.notNull(expression, "Expression must not be null!"); - return new IsoWeekYear(expression); + /** + * A Mutable builder to create a {@link DateFromParts} aggregation expression. All methods mutate this builder (they + * all return this for convenience) + * + * @param The concrete builder (either {@link CalendarDatePartsBuilder} for calendar date (i.e. + * year/month/day) or {@link IsoWeekDatePartsBuilder} for ISO week 8601 dates + * (isoWeekYear/isoWeek/isoDayOfWeek) + */ + public abstract static class DatePartsBuilder> { + + protected Object hour; + + protected Object minute; + + protected Object second; + + protected Object millisecond; + + protected Object timezone; + + /** + * Sets the 'hour' of the {@link DateFromParts} to given the fixed value + * + * @param hour the fixed value to bind the field + * @return + */ + @SuppressWarnings("unchecked") + public Builder hour(Number hour) { + + this.hour = hour; + return (Builder) this; + } + + /** + * Sets the 'hour' of the {@link DateFromParts} to given the field + * + * @param hour the field to read the 'hour' value from + * @return + */ + @SuppressWarnings("unchecked") + public Builder hourOf(String hour) { + + this.hour = Fields.field(hour); + return (Builder) this; + } + + /** + * Sets the 'hour' of the {@link DateFromParts} to given the fixed value + * + * @param hour the expression to evaluate the 'hour' value + * @return + */ + @SuppressWarnings("unchecked") + public Builder hourOf(AggregationExpression hour) { + + this.hour = hour; + return (Builder) this; + } + + /** + * Sets the 'minute' of the {@link DateFromParts} to given the fixed value + * + * @param minute the fixed value to bind the field + * @return + */ + @SuppressWarnings("unchecked") + public Builder minute(Number minute) { + + this.minute = minute; + return (Builder) this; + } + + /** + * Sets the 'minute' of the {@link DateFromParts} to given the field + * + * @param minute the field to read the 'minute' value from + * @return + */ + @SuppressWarnings("unchecked") + public Builder minuteOf(String minute) { + + this.minute = Fields.field(minute); + return (Builder) this; + } + + /** + * Sets the 'minute' of the {@link DateFromParts} to given the fixed value + * + * @param minute the expression to evaluate the 'minute' value + * @return + */ + @SuppressWarnings("unchecked") + public Builder minuteOf(AggregationExpression minute) { + + this.minute = minute; + return (Builder) this; + } + + /** + * Sets the 'second' of the {@link DateFromParts} to given the fixed value + * + * @param second the fixed value to bind the field + * @return + */ + @SuppressWarnings("unchecked") + public Builder second(Number second) { + + this.second = second; + return (Builder) this; + } + + /** + * Sets the 'second' of the {@link DateFromParts} to given the field + * + * @param second the field to read the 'second' value from + * @return + */ + @SuppressWarnings("unchecked") + public Builder secondOf(String second) { + + this.second = Fields.field(second); + return (Builder) this; + } + + /** + * Sets the 'second' of the {@link DateFromParts} to given the fixed value + * + * @param second the expression to evaluate the 'second' value + * @return + */ + @SuppressWarnings("unchecked") + public Builder secondOf(AggregationExpression second) { + + this.second = second; + return (Builder) this; + } + + /** + * Sets the 'millisecond' of the {@link DateFromParts} to given the fixed value + * + * @param millisecond the fixed value to bind the field + * @return + */ + @SuppressWarnings("unchecked") + public Builder millisecond(Number millisecond) { + + this.millisecond = millisecond; + return (Builder) this; + } + + /** + * Sets the 'millisecond' of the {@link DateFromParts} to given the field + * + * @param millisecond the field to read the 'millisecond' value from + * @return + */ + @SuppressWarnings("unchecked") + public Builder millisecondOf(String millisecond) { + + this.millisecond = Fields.field(millisecond); + return (Builder) this; + } + + /** + * Sets the 'millisecond' of the {@link DateFromParts} to given the fixed value + * + * @param millisecond the expression to evaluate the 'millisecond' value + * @return + */ + @SuppressWarnings("unchecked") + public Builder millisecondOf(AggregationExpression millisecond) { + + this.millisecond = millisecond; + return (Builder) this; + } + + /** + * Sets the 'timezone' of the {@link DateFromParts} to given the fixed value + * + * @param timezone the fixed value to bind the field + * @return + */ + @SuppressWarnings("unchecked") + public Builder timezone(String timezone) { + + this.timezone = timezone; + return (Builder) this; + } + + /** + * Sets the 'timezone' of the {@link DateFromParts} to given the field + * + * @param timezone the field to read the 'timezone' value from + * @return + */ + @SuppressWarnings("unchecked") + public Builder timezoneOf(String timezone) { + + this.timezone = Fields.field(timezone); + return (Builder) this; + } + + /** + * Sets the 'timezone' of the {@link DateFromParts} to given the fixed value + * + * @param timezone the expression to evaluate the 'timezone' value + * @return + */ + @SuppressWarnings("unchecked") + public Builder timezoneOf(AggregationExpression timezone) { + + this.timezone = timezone; + return (Builder) this; + } + + public abstract DateFromParts toDate(); + } + + public static class CalendarDatePartsBuilder extends DatePartsBuilder { + + private Object year; + + private Object month; + + private Object day; + + @Override + public DateFromParts toDate() { + return new DateFromParts(false, year, month, day, hour, minute, second, millisecond, timezone); + } + + /** + * Sets the 'year' of the {@link DateFromParts} to given the fixed value + * + * @param year the fixed value to bind the field + * @return + */ + public CalendarDatePartsBuilder year(Number year) { + + this.year = year; + return this; + } + + /** + * Sets the 'year' of the {@link DateFromParts} to given the field + * + * @param year the field to read the 'year' value from + * @return + */ + public CalendarDatePartsBuilder yearOf(String year) { + + this.year = Fields.field(year); + return this; + } + + /** + * Sets the 'year' of the {@link DateFromParts} to given the fixed value + * + * @param year the expression to evaluate the 'year' value + * @return + */ + public CalendarDatePartsBuilder yearOf(AggregationExpression year) { + + this.year = year; + return this; + } + + /** + * Sets the 'month' of the {@link DateFromParts} to given the fixed value + * + * @param month the fixed value to bind the field + * @return + */ + public CalendarDatePartsBuilder month(Number month) { + + this.month = month; + return this; + } + + /** + * Sets the 'month' of the {@link DateFromParts} to given the field + * + * @param month the field to read the 'month' value from + * @return + */ + public CalendarDatePartsBuilder monthOf(String month) { + + this.month = Fields.field(month); + return this; + } + + /** + * Sets the 'month' of the {@link DateFromParts} to given the fixed value + * + * @param month the expression to evaluate the 'month' value + * @return + */ + public CalendarDatePartsBuilder monthOf(AggregationExpression month) { + + this.month = month; + return this; + } + + /** + * Sets the 'day' of the {@link DateFromParts} to given the fixed value + * + * @param day the fixed value to bind the field + * @return + */ + public CalendarDatePartsBuilder day(Number day) { + + this.day = day; + return this; + } + + /** + * Sets the 'day' of the {@link DateFromParts} to given the field + * + * @param day the field to read the 'day' value from + * @return + */ + public CalendarDatePartsBuilder dayOf(String day) { + + this.day = Fields.field(day); + return this; + } + + /** + * Sets the 'day' of the {@link DateFromParts} to given the fixed value + * + * @param day the expression to evaluate the 'day' value + * @return + */ + public CalendarDatePartsBuilder dayOf(AggregationExpression day) { + + this.day = day; + return this; + } + + } + + public static class IsoWeekDatePartsBuilder extends DatePartsBuilder { + + private Object isoWeekYear; + + private Object isoWeek; + + private Object isoDayOfWeek; + + @Override + public DateFromParts toDate() { + return new DateFromParts(true, isoWeekYear, isoWeek, isoDayOfWeek, hour, minute, second, millisecond, timezone); + } + + /** + * Sets the 'isoWeekYear' of the {@link DateFromParts} to given the fixed value + * + * @param isoWeekYear the fixed value to bind the field + * @return + */ + public IsoWeekDatePartsBuilder isoWeekYear(Number isoWeekYear) { + + this.isoWeekYear = isoWeekYear; + return this; + } + + /** + * Sets the 'isoWeekYear' of the {@link DateFromParts} to given the field + * + * @param isoWeekYear the field to read the 'isoWeekYear' value from + * @return + */ + public IsoWeekDatePartsBuilder isoWeekYearOf(String isoWeekYear) { + + this.isoWeekYear = Fields.field(isoWeekYear); + return this; + } + + /** + * Sets the 'isoWeekYear' of the {@link DateFromParts} to given the fixed value + * + * @param isoWeekYear the expression to evaluate the 'isoWeekYear' value + * @return + */ + public IsoWeekDatePartsBuilder isoWeekYearOf(AggregationExpression isoWeekYear) { + + this.isoWeekYear = isoWeekYear; + return this; + } + + /** + * Sets the 'isoWeek' of the {@link DateFromParts} to given the fixed value + * + * @param isoWeek the fixed value to bind the field + * @return + */ + public IsoWeekDatePartsBuilder isoWeek(Number isoWeek) { + + this.isoWeek = isoWeek; + return this; + } + + /** + * Sets the 'isoWeek' of the {@link DateFromParts} to given the field + * + * @param isoWeek the field to read the 'isoWeek' value from + * @return + */ + public IsoWeekDatePartsBuilder isoWeekOf(String isoWeek) { + + this.isoWeek = Fields.field(isoWeek); + return this; + } + + /** + * Sets the 'isoWeek' of the {@link DateFromParts} to given the fixed value + * + * @param isoWeek the expression to evaluate the 'isoWeek' value + * @return + */ + public IsoWeekDatePartsBuilder isoWeekOf(AggregationExpression isoWeek) { + + this.isoWeek = isoWeek; + return this; + } + + /** + * Sets the 'isoDayOfWeek' of the {@link DateFromParts} to given the fixed value + * + * @param isoDayOfWeek the fixed value to bind the field + * @return + */ + public IsoWeekDatePartsBuilder isoDayOfWeek(Number isoDayOfWeek) { + + this.isoDayOfWeek = isoDayOfWeek; + return this; + } + + /** + * Sets the 'isoDayOfWeek' of the {@link DateFromParts} to given the field + * + * @param isoDayOfWeek the field to read the 'isoDayOfWeek' value from + * @return + */ + public IsoWeekDatePartsBuilder isoDayOfWeekOf(String isoDayOfWeek) { + + this.isoDayOfWeek = Fields.field(isoDayOfWeek); + return this; + } + + /** + * Sets the 'isoDayOfWeek' of the {@link DateFromParts} to given the fixed value + * + * @param isoDayOfWeek the expression to evaluate the 'isoDayOfWeek' value + * @return + */ + public IsoWeekDatePartsBuilder isoDayOfWeekOf(AggregationExpression isoDayOfWeek) { + + this.isoDayOfWeek = isoDayOfWeek; + return this; + } + + } + + private static Map calMap(Object year, Object month, Object day, Object hour, Object minute, + Object second, Object millisecond, Object timezone) { + + final Map vals = new LinkedHashMap<>(11); + put(vals, "year", year, true); + put(vals, "month", month, false); + put(vals, "day", day, false); + putCommonMap(vals, hour, minute, second, millisecond, timezone); + return vals; + } + + private static Map isoWeekMap(Object isoWeekYear, Object isoWeek, Object isoDayOfWeek, Object hour, + Object minute, Object second, Object millisecond, Object timezone) { + + final Map vals = new LinkedHashMap<>(11); + put(vals, "isoWeekYear", isoWeekYear, true); + put(vals, "isoWeek", isoWeek, false); + put(vals, "isoDayOfWeek", isoDayOfWeek, false); + putCommonMap(vals, hour, minute, second, millisecond, timezone); + return vals; + } + + private static void putCommonMap(final Map vals, Object hour, Object minute, Object second, + Object millisecond, Object timezone) { + + put(vals, "hour", hour, false); + put(vals, "minute", minute, false); + put(vals, "second", second, false); + put(vals, "millisecond", millisecond, false); + if (timezone != null) { + Assert.isTrue(DateAggregationExpression.isValidTimezoneObject(timezone), + () -> "Timezone was not a valid timezone: " + timezone + + ". Must be String, AggregationExpression or Field"); + vals.put("timezone", timezone); + } + } + + private static void put(final Map map, final String key, final Object val, boolean throwIfAbsent) { + + if (val != null) { + map.put(key, val); + } else if (throwIfAbsent) { + throw new IllegalArgumentException(key + "is required"); + } } } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java new file mode 100644 index 000000000..3054479d9 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java @@ -0,0 +1,899 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.aggregation; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.mongodb.core.aggregation.DateOperators.*; +import static org.springframework.data.mongodb.core.aggregation.LiteralOperators.Literal.*; + +import java.util.Date; + +import org.bson.Document; +import org.junit.Test; + +import com.google.common.collect.Lists; + +/** + * Unit tests for {@link DateOperators}. DATAMONGO-1834 - Add support for aggregation operators $dateFromString, + * $dateFromParts and $dateToParts This test case now covers all existing methods in the DateOperators class as well as + * those added as part of DATAMONGO-1834 + * + * @author Matt Morrissette + */ +public class DateOperatorsUnitTests { + + private static final String FIELD = "field"; + + private static final String VAR = "$"; + + private static final String VAR_FIELD = VAR + FIELD; + + private static final String TIMEZONE = "America/Los_Angeles"; + + private static final String TIMEZONE2 = "America/New_York"; + + private static final String FORMAT = "%Y-%m-%d"; + + private static final Document LITERAL = new Document("$literal", VAR_FIELD); + + private static final String TO_STRING_OP = "$dateToString"; + + private static Object CURRENT_DATE; + + @Test(expected = IllegalArgumentException.class) + public void rejectsEmptyFieldName() { + dateOf(""); + } + + @Test + public void shouldRenderFieldCorrectly() { + + final DateOperatorFactory f = dateOf(FIELD); + assertDateFieldOp(f.dayOfMonth(), "dayOfMonth"); + assertDateFieldOp(f.dayOfWeek(), "dayOfWeek"); + assertDateFieldOp(f.dayOfYear(), "dayOfYear"); + assertDateFieldOp(f.hour(), "hour"); + assertDateFieldOp(f.isoDayOfWeek(), "isoDayOfWeek"); + assertDateFieldOp(f.isoWeek(), "isoWeek"); + assertDateFieldOp(f.isoWeekYear(), "isoWeekYear"); + assertDateFieldOp(f.millisecond(), "millisecond"); + assertDateFieldOp(f.minute(), "minute"); + assertDateFieldOp(f.month(), "month"); + assertDateFieldOp(f.second(), "second"); + assertDateFieldOp(f.week(), "week"); + assertDateFieldOp(f.year(), "year"); + assertQuarterFieldOp(f.quarter()); + assertDateFromStringField(f.fromString()); + assertDateToPartsField(f.toParts(), null); + assertDateToPartsField(f.toIsoWeekParts(), true); + assertDateToPartsField(f.toParts(true), true); + assertDateToPartsField(f.toParts(false), false); + assertDateFieldStringNoTimezoneOp(f.toString(FORMAT)); + assertDateFieldTimezoneOp(f.dayOfMonth(TIMEZONE), "dayOfMonth"); + assertDateFieldTimezoneOp(f.dayOfWeek(TIMEZONE), "dayOfWeek"); + assertDateFieldTimezoneOp(f.dayOfYear(TIMEZONE), "dayOfYear"); + assertDateFieldTimezoneOp(f.hour(TIMEZONE), "hour"); + assertDateFieldTimezoneOp(f.isoDayOfWeek(TIMEZONE), "isoDayOfWeek"); + assertDateFieldTimezoneOp(f.isoWeek(TIMEZONE), "isoWeek"); + assertDateFieldTimezoneOp(f.isoWeekYear(TIMEZONE), "isoWeekYear"); + assertDateFieldTimezoneOp(f.millisecond(TIMEZONE), "millisecond"); + assertDateFieldTimezoneOp(f.minute(TIMEZONE), "minute"); + assertDateFieldTimezoneOp(f.month(TIMEZONE), "month"); + assertDateFieldTimezoneOp(f.second(TIMEZONE), "second"); + assertDateFieldTimezoneOp(f.week(TIMEZONE), "week"); + assertDateFieldTimezoneOp(f.year(TIMEZONE), "year"); + assertDateToPartsFieldTimezone(f.toParts(TIMEZONE), null); + assertDateToPartsFieldTimezone(f.toIsoWeekParts(TIMEZONE), true); + assertDateToPartsFieldTimezone(f.toParts(TIMEZONE, true), true); + assertDateToPartsFieldTimezone(f.toParts(TIMEZONE, false), false); + assertQuarterFieldTimezoneOp(f.quarter(TIMEZONE)); + assertDateFromStringFieldTimezone(f.fromString(TIMEZONE)); + assertDateFieldStringTimezoneOp(f.toString(FORMAT, TIMEZONE)); + } + + @Test + public void shouldRenderFieldTimezoneCorrectly() { + + final DateOperatorFactory f = dateOfWithTimezone(FIELD, TIMEZONE); + assertDateFieldTimezoneOp(f.dayOfMonth(), "dayOfMonth"); + assertDateFieldTimezoneOp(f.dayOfWeek(), "dayOfWeek"); + assertDateFieldTimezoneOp(f.dayOfYear(), "dayOfYear"); + assertDateFieldTimezoneOp(f.hour(), "hour"); + assertDateFieldTimezoneOp(f.isoDayOfWeek(), "isoDayOfWeek"); + assertDateFieldTimezoneOp(f.isoWeek(), "isoWeek"); + assertDateFieldTimezoneOp(f.isoWeekYear(), "isoWeekYear"); + assertDateFieldTimezoneOp(f.millisecond(), "millisecond"); + assertDateFieldTimezoneOp(f.minute(), "minute"); + assertDateFieldTimezoneOp(f.month(), "month"); + assertDateFieldTimezoneOp(f.second(), "second"); + assertDateFieldTimezoneOp(f.week(), "week"); + assertDateFieldTimezoneOp(f.year(), "year"); + assertQuarterFieldTimezoneOp(f.quarter()); + assertDateFromStringFieldTimezone(f.fromString()); + assertDateToPartsFieldTimezone(f.toParts(), null); + assertDateToPartsFieldTimezone(f.toIsoWeekParts(), true); + assertDateToPartsFieldTimezone(f.toParts(true), true); + assertDateToPartsFieldTimezone(f.toParts(false), false); + assertDateFieldStringTimezoneOp(f.toString(FORMAT)); + + assertDateFieldTimezone2Op(f.dayOfMonth(TIMEZONE2), "dayOfMonth"); + assertDateFieldTimezone2Op(f.dayOfWeek(TIMEZONE2), "dayOfWeek"); + assertDateFieldTimezone2Op(f.dayOfYear(TIMEZONE2), "dayOfYear"); + assertDateFieldTimezone2Op(f.hour(TIMEZONE2), "hour"); + assertDateFieldTimezone2Op(f.isoDayOfWeek(TIMEZONE2), "isoDayOfWeek"); + assertDateFieldTimezone2Op(f.isoWeek(TIMEZONE2), "isoWeek"); + assertDateFieldTimezone2Op(f.isoWeekYear(TIMEZONE2), "isoWeekYear"); + assertDateFieldTimezone2Op(f.millisecond(TIMEZONE2), "millisecond"); + assertDateFieldTimezone2Op(f.minute(TIMEZONE2), "minute"); + assertDateFieldTimezone2Op(f.month(TIMEZONE2), "month"); + assertDateFieldTimezone2Op(f.second(TIMEZONE2), "second"); + assertDateFieldTimezone2Op(f.week(TIMEZONE2), "week"); + assertDateFieldTimezone2Op(f.year(TIMEZONE2), "year"); + assertQuarterFieldTimezone2Op(f.quarter(TIMEZONE2)); + assertDateFromStringFieldTimezone2(f.fromString(TIMEZONE2)); + assertDateToPartsFieldTimezone2(f.toParts(TIMEZONE2), null); + assertDateToPartsFieldTimezone2(f.toIsoWeekParts(TIMEZONE2), true); + assertDateToPartsFieldTimezone2(f.toParts(TIMEZONE2, true), true); + assertDateToPartsFieldTimezone2(f.toParts(TIMEZONE2, false), false); + assertDateFieldStringTimezone2Op(f.toString(FORMAT, TIMEZONE2)); + + assertDateFieldOp(f.dayOfMonth(null), "dayOfMonth"); + assertDateFieldOp(f.dayOfWeek(null), "dayOfWeek"); + assertDateFieldOp(f.dayOfYear(null), "dayOfYear"); + assertDateFieldOp(f.hour(null), "hour"); + assertDateFieldOp(f.isoDayOfWeek(null), "isoDayOfWeek"); + assertDateFieldOp(f.isoWeek(null), "isoWeek"); + assertDateFieldOp(f.isoWeekYear(null), "isoWeekYear"); + assertDateFieldOp(f.millisecond(null), "millisecond"); + assertDateFieldOp(f.minute(null), "minute"); + assertDateFieldOp(f.month(null), "month"); + assertDateFieldOp(f.second(null), "second"); + assertDateFieldOp(f.week(null), "week"); + assertDateFieldOp(f.year(null), "year"); + assertQuarterFieldOp(f.quarter(null)); + assertDateFromStringField(f.fromString(null)); + assertDateToPartsField(f.toParts((String) null), null); + assertDateToPartsField(f.toIsoWeekParts(null), true); + assertDateToPartsField(f.toParts(null, true), true); + assertDateToPartsField(f.toParts(null, false), false); + assertDateFieldStringNoTimezoneOp(f.toString(FORMAT, null)); + } + + @Test + public void shouldRenderExprCorrectly() { + + final DateOperatorFactory f = dateOf(asLiteral(VAR_FIELD)); + assertDateExprOp(f.dayOfMonth(), "dayOfMonth"); + assertDateExprOp(f.dayOfWeek(), "dayOfWeek"); + assertDateExprOp(f.dayOfYear(), "dayOfYear"); + assertDateExprOp(f.hour(), "hour"); + assertDateExprOp(f.isoDayOfWeek(), "isoDayOfWeek"); + assertDateExprOp(f.isoWeek(), "isoWeek"); + assertDateExprOp(f.isoWeekYear(), "isoWeekYear"); + assertDateExprOp(f.millisecond(), "millisecond"); + assertDateExprOp(f.minute(), "minute"); + assertDateExprOp(f.month(), "month"); + assertDateExprOp(f.second(), "second"); + assertDateExprOp(f.week(), "week"); + assertDateExprOp(f.year(), "year"); + assertQuarterExprOp(f.quarter()); + assertDateFromStringExpr(f.fromString()); + assertDateToPartsExpr(f.toParts(), null); + assertDateToPartsExpr(f.toIsoWeekParts(), true); + assertDateToPartsExpr(f.toParts(true), true); + assertDateToPartsExpr(f.toParts(false), false); + assertDateExprStringNoTimezoneOp(f.toString(FORMAT)); + assertDateExprTimezoneOp(f.dayOfMonth(TIMEZONE), "dayOfMonth"); + assertDateExprTimezoneOp(f.dayOfWeek(TIMEZONE), "dayOfWeek"); + assertDateExprTimezoneOp(f.dayOfYear(TIMEZONE), "dayOfYear"); + assertDateExprTimezoneOp(f.hour(TIMEZONE), "hour"); + assertDateExprTimezoneOp(f.isoDayOfWeek(TIMEZONE), "isoDayOfWeek"); + assertDateExprTimezoneOp(f.isoWeek(TIMEZONE), "isoWeek"); + assertDateExprTimezoneOp(f.isoWeekYear(TIMEZONE), "isoWeekYear"); + assertDateExprTimezoneOp(f.millisecond(TIMEZONE), "millisecond"); + assertDateExprTimezoneOp(f.minute(TIMEZONE), "minute"); + assertDateExprTimezoneOp(f.month(TIMEZONE), "month"); + assertDateExprTimezoneOp(f.second(TIMEZONE), "second"); + assertDateExprTimezoneOp(f.week(TIMEZONE), "week"); + assertDateExprTimezoneOp(f.year(TIMEZONE), "year"); + assertQuarterExprTimezoneOp(f.quarter(TIMEZONE)); + assertDateFromStringExprTimezone(f.fromString(TIMEZONE)); + assertDateToPartsExprTimezone(f.toParts(TIMEZONE), null); + assertDateToPartsExprTimezone(f.toIsoWeekParts(TIMEZONE), true); + assertDateToPartsExprTimezone(f.toParts(TIMEZONE, true), true); + assertDateToPartsExprTimezone(f.toParts(TIMEZONE, false), false); + assertDateExprStringTimezoneOp(f.toString(FORMAT, TIMEZONE)); + } + + @Test + public void shouldRenderExprTimezoneCorrectly() { + + final DateOperatorFactory f = dateOfWithTimezone(asLiteral(VAR_FIELD), TIMEZONE); + assertDateExprTimezoneOp(f.dayOfMonth(), "dayOfMonth"); + assertDateExprTimezoneOp(f.dayOfWeek(), "dayOfWeek"); + assertDateExprTimezoneOp(f.dayOfYear(), "dayOfYear"); + assertDateExprTimezoneOp(f.hour(), "hour"); + assertDateExprTimezoneOp(f.isoDayOfWeek(), "isoDayOfWeek"); + assertDateExprTimezoneOp(f.isoWeek(), "isoWeek"); + assertDateExprTimezoneOp(f.isoWeekYear(), "isoWeekYear"); + assertDateExprTimezoneOp(f.millisecond(), "millisecond"); + assertDateExprTimezoneOp(f.minute(), "minute"); + assertDateExprTimezoneOp(f.month(), "month"); + assertDateExprTimezoneOp(f.second(), "second"); + assertDateExprTimezoneOp(f.week(), "week"); + assertDateExprTimezoneOp(f.year(), "year"); + assertQuarterExprTimezoneOp(f.quarter()); + assertDateFromStringExprTimezone(f.fromString()); + assertDateToPartsExprTimezone(f.toParts(), null); + assertDateToPartsExprTimezone(f.toIsoWeekParts(), true); + assertDateToPartsExprTimezone(f.toParts(true), true); + assertDateToPartsExprTimezone(f.toParts(false), false); + assertDateExprStringTimezoneOp(f.toString(FORMAT)); + assertDateExprTimezone2Op(f.dayOfMonth(TIMEZONE2), "dayOfMonth"); + assertDateExprTimezone2Op(f.dayOfWeek(TIMEZONE2), "dayOfWeek"); + assertDateExprTimezone2Op(f.dayOfYear(TIMEZONE2), "dayOfYear"); + assertDateExprTimezone2Op(f.hour(TIMEZONE2), "hour"); + assertDateExprTimezone2Op(f.isoDayOfWeek(TIMEZONE2), "isoDayOfWeek"); + assertDateExprTimezone2Op(f.isoWeek(TIMEZONE2), "isoWeek"); + assertDateExprTimezone2Op(f.isoWeekYear(TIMEZONE2), "isoWeekYear"); + assertDateExprTimezone2Op(f.millisecond(TIMEZONE2), "millisecond"); + assertDateExprTimezone2Op(f.minute(TIMEZONE2), "minute"); + assertDateExprTimezone2Op(f.month(TIMEZONE2), "month"); + assertDateExprTimezone2Op(f.second(TIMEZONE2), "second"); + assertDateExprTimezone2Op(f.week(TIMEZONE2), "week"); + assertDateExprTimezone2Op(f.year(TIMEZONE2), "year"); + assertDateToPartsExprTimezone2(f.toParts(TIMEZONE2), null); + assertDateToPartsExprTimezone2(f.toIsoWeekParts(TIMEZONE2), true); + assertDateToPartsExprTimezone2(f.toParts(TIMEZONE2, true), true); + assertDateToPartsExprTimezone2(f.toParts(TIMEZONE2, false), false); + assertDateFromStringExprTimezone2(f.fromString(TIMEZONE2)); + assertQuarterExprStringTimezone2Op(f.quarter(TIMEZONE2)); + assertDateExprTimezone2Op(f.toString(FORMAT, TIMEZONE2)); + assertDateExprOp(f.dayOfMonth(null), "dayOfMonth"); + assertDateExprOp(f.dayOfWeek(null), "dayOfWeek"); + assertDateExprOp(f.dayOfYear(null), "dayOfYear"); + assertDateExprOp(f.hour(null), "hour"); + assertDateExprOp(f.isoDayOfWeek(null), "isoDayOfWeek"); + assertDateExprOp(f.isoWeek(null), "isoWeek"); + assertDateExprOp(f.isoWeekYear(null), "isoWeekYear"); + assertDateExprOp(f.millisecond(null), "millisecond"); + assertDateExprOp(f.minute(null), "minute"); + assertDateExprOp(f.month(null), "month"); + assertDateExprOp(f.second(null), "second"); + assertDateExprOp(f.week(null), "week"); + assertDateExprOp(f.year(null), "year"); + assertQuarterExprOp(f.quarter(null)); + assertDateFromStringExpr(f.fromString(null)); + assertDateToPartsExpr(f.toParts((String) null), null); + assertDateToPartsExpr(f.toIsoWeekParts(null), true); + assertDateToPartsExpr(f.toParts(null, true), true); + assertDateToPartsExpr(f.toParts(null, false), false); + assertDateExprStringNoTimezoneOp(f.toString(FORMAT, null)); + } + + @Test + public void shouldRenderCurrentDateCorrectly() { + + CURRENT_DATE = new Date(); + pShouldRenderCurrentDateCorrectly(DateFactory.fixedDate(CURRENT_DATE)); + } + + private void pShouldRenderCurrentDateCorrectly(DateFactory dateFactory) { + pShouldRenderCurrentDateCorrectly(dateOf(dateFactory).withTimezone(TIMEZONE)); + } + + private void pShouldRenderCurrentDateCorrectly(DateOperatorFactory f) { + + assertCurrentDateTimezoneOp(f.dayOfMonth(), "dayOfMonth"); + assertCurrentDateTimezoneOp(f.dayOfWeek(), "dayOfWeek"); + assertCurrentDateTimezoneOp(f.dayOfYear(), "dayOfYear"); + assertCurrentDateTimezoneOp(f.hour(), "hour"); + assertCurrentDateTimezoneOp(f.isoDayOfWeek(), "isoDayOfWeek"); + assertCurrentDateTimezoneOp(f.isoWeek(), "isoWeek"); + assertCurrentDateTimezoneOp(f.isoWeekYear(), "isoWeekYear"); + assertCurrentDateTimezoneOp(f.millisecond(), "millisecond"); + assertCurrentDateTimezoneOp(f.minute(), "minute"); + assertCurrentDateTimezoneOp(f.month(), "month"); + assertCurrentDateTimezoneOp(f.second(), "second"); + assertCurrentDateTimezoneOp(f.week(), "week"); + assertCurrentDateTimezoneOp(f.year(), "year"); + assertCurrentDateToPartsTimezone(f.toParts(), null); + assertCurrentDateToPartsTimezone(f.toIsoWeekParts(), true); + assertCurrentDateToPartsTimezone(f.toParts(true), true); + assertCurrentDateToPartsTimezone(f.toParts(false), false); + assertQuarterCurrentDateTimezoneOp(f.quarter()); + assertCurrentDateFromStringTimezone(f.fromString()); + assertCurrentDateStringTimezoneOp(f.toString(FORMAT)); + assertCurrentDateTimezone2Op(f.dayOfMonth(TIMEZONE2), "dayOfMonth"); + assertCurrentDateTimezone2Op(f.dayOfWeek(TIMEZONE2), "dayOfWeek"); + assertCurrentDateTimezone2Op(f.dayOfYear(TIMEZONE2), "dayOfYear"); + assertCurrentDateTimezone2Op(f.hour(TIMEZONE2), "hour"); + assertCurrentDateTimezone2Op(f.isoDayOfWeek(TIMEZONE2), "isoDayOfWeek"); + assertCurrentDateTimezone2Op(f.isoWeek(TIMEZONE2), "isoWeek"); + assertCurrentDateTimezone2Op(f.isoWeekYear(TIMEZONE2), "isoWeekYear"); + assertCurrentDateTimezone2Op(f.millisecond(TIMEZONE2), "millisecond"); + assertCurrentDateTimezone2Op(f.minute(TIMEZONE2), "minute"); + assertCurrentDateTimezone2Op(f.month(TIMEZONE2), "month"); + assertCurrentDateTimezone2Op(f.second(TIMEZONE2), "second"); + assertCurrentDateTimezone2Op(f.week(TIMEZONE2), "week"); + assertCurrentDateTimezone2Op(f.year(TIMEZONE2), "year"); + assertCurrentDateToPartsTimezone2(f.toParts(TIMEZONE2), null); + assertCurrentDateToPartsTimezone2(f.toIsoWeekParts(TIMEZONE2), true); + assertCurrentDateToPartsTimezone2(f.toParts(TIMEZONE2, true), true); + assertCurrentDateToPartsTimezone2(f.toParts(TIMEZONE2, false), false); + assertQuarterCurrentDateTimezone2Op(f.quarter(TIMEZONE2)); + assertCurrentDateFromStringTimezone2(f.fromString(TIMEZONE2)); + assertCurrentDateStringTimezone2Op(f.toString(FORMAT, TIMEZONE2)); + assertCurrentDateOp(f.dayOfMonth(null), "dayOfMonth"); + assertCurrentDateOp(f.dayOfWeek(null), "dayOfWeek"); + assertCurrentDateOp(f.dayOfYear(null), "dayOfYear"); + assertCurrentDateOp(f.hour(null), "hour"); + assertCurrentDateOp(f.isoDayOfWeek(null), "isoDayOfWeek"); + assertCurrentDateOp(f.isoWeek(null), "isoWeek"); + assertCurrentDateOp(f.isoWeekYear(null), "isoWeekYear"); + assertCurrentDateOp(f.millisecond(null), "millisecond"); + assertCurrentDateOp(f.minute(null), "minute"); + assertCurrentDateOp(f.month(null), "month"); + assertCurrentDateOp(f.second(null), "second"); + assertCurrentDateOp(f.week(null), "week"); + assertCurrentDateOp(f.year(null), "year"); + assertQuarterCurrentDateOp(f.quarter(null)); + assertCurrentDateFromString(f.fromString(null)); + assertCurrentDateStringNoTimezoneOp(f.toString(FORMAT, null)); + assertCurrentDateToParts(f.toParts((String) null), null); + assertCurrentDateToParts(f.toIsoWeekParts(null), true); + assertCurrentDateToParts(f.toParts(null, true), true); + assertCurrentDateToParts(f.toParts(null, false), false); + } + + @Test + public void shouldRenderCalendarDateFromPartsCorrectly() { + + final Document doc = new Document(); + final Document parts = new Document(); + doc.put("$dateFromParts", parts); + + int year = 2017; + DateFromParts.CalendarDatePartsBuilder dateFromParts = dateFromParts(); + parts.put("year", year); + assertThat(dateFromParts.year(year).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("year", "$year"); + assertThat(dateFromParts.yearOf("year").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("year", LITERAL); + assertThat(dateFromParts.yearOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("year", year); + dateFromParts.year(year); + + int month = 10; + parts.put("month", month); + assertThat(dateFromParts.month(month).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("month", "$month"); + assertThat(dateFromParts.monthOf("month").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("month", LITERAL); + assertThat(dateFromParts.monthOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.remove("month"); + dateFromParts.month(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + int day = 8; + parts.put("day", day); + assertThat(dateFromParts.day(day).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("day", "$day"); + assertThat(dateFromParts.dayOf("day").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("day", LITERAL); + assertThat(dateFromParts.dayOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.remove("day"); + dateFromParts.day(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + int hour = 9; + parts.put("hour", hour); + assertThat(dateFromParts.hour(hour).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("hour", "$hour"); + assertThat(dateFromParts.hourOf("hour").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("hour", LITERAL); + assertThat(dateFromParts.hourOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.remove("hour"); + dateFromParts.hour(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + int minute = 15; + parts.put("minute", minute); + assertThat(dateFromParts.minute(minute).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("minute", "$minute"); + assertThat(dateFromParts.minuteOf("minute").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("minute", LITERAL); + assertThat(dateFromParts.minuteOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.remove("minute"); + dateFromParts.minute(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + int second = 35; + parts.put("second", second); + assertThat(dateFromParts.second(second).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("second", "$second"); + assertThat(dateFromParts.secondOf("second").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("second", LITERAL); + assertThat(dateFromParts.secondOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.remove("second"); + dateFromParts.second(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + int millisecond = 35; + parts.put("millisecond", millisecond); + assertThat(dateFromParts.millisecond(millisecond).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("millisecond", "$millisecond"); + assertThat(dateFromParts.millisecondOf("millisecond").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("millisecond", LITERAL); + assertThat(dateFromParts.millisecondOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), + is(doc)); + parts.remove("millisecond"); + dateFromParts.millisecond(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + String timezone = "America/New_York"; + parts.put("timezone", timezone); + assertThat(dateFromParts.timezone(timezone).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("timezone", "$timezone"); + assertThat(dateFromParts.timezoneOf("timezone").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("timezone", LITERAL); + assertThat(dateFromParts.timezoneOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), + is(doc)); + parts.remove("timezone"); + dateFromParts.timezone(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + } + + @Test + public void shouldRenderIsoWeekDateFromPartsCorrectly() { + + final Document doc = new Document(); + final Document parts = new Document(); + doc.put("$dateFromParts", parts); + + int isoWeekYear = 2017; + DateFromParts.IsoWeekDatePartsBuilder dateFromParts = dateFromIsoWeekParts(); + parts.put("isoWeekYear", isoWeekYear); + assertThat(dateFromParts.isoWeekYear(isoWeekYear).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("isoWeekYear", "$isoWeekYear"); + assertThat(dateFromParts.isoWeekYearOf("isoWeekYear").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("isoWeekYear", LITERAL); + assertThat(dateFromParts.isoWeekYearOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), + is(doc)); + parts.put("isoWeekYear", isoWeekYear); + dateFromParts.isoWeekYear(isoWeekYear); + + int isoWeek = 25; + parts.put("isoWeek", isoWeek); + assertThat(dateFromParts.isoWeek(isoWeek).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("isoWeek", "$isoWeek"); + assertThat(dateFromParts.isoWeekOf("isoWeek").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("isoWeek", LITERAL); + assertThat(dateFromParts.isoWeekOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.remove("isoWeek"); + dateFromParts.isoWeek(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + int isoDayOfWeek = 4; + parts.put("isoDayOfWeek", isoDayOfWeek); + assertThat(dateFromParts.isoDayOfWeek(isoDayOfWeek).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("isoDayOfWeek", "$isoDayOfWeek"); + assertThat(dateFromParts.isoDayOfWeekOf("isoDayOfWeek").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("isoDayOfWeek", LITERAL); + assertThat(dateFromParts.isoDayOfWeekOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), + is(doc)); + parts.remove("isoDayOfWeek"); + dateFromParts.isoDayOfWeek(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + int hour = 9; + parts.put("hour", hour); + assertThat(dateFromParts.hour(hour).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("hour", "$hour"); + assertThat(dateFromParts.hourOf("hour").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("hour", LITERAL); + assertThat(dateFromParts.hourOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.remove("hour"); + dateFromParts.hour(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + int minute = 15; + parts.put("minute", minute); + assertThat(dateFromParts.minute(minute).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("minute", "$minute"); + assertThat(dateFromParts.minuteOf("minute").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("minute", LITERAL); + assertThat(dateFromParts.minuteOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.remove("minute"); + dateFromParts.minute(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + int second = 35; + parts.put("second", second); + assertThat(dateFromParts.second(second).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("second", "$second"); + assertThat(dateFromParts.secondOf("second").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("second", LITERAL); + assertThat(dateFromParts.secondOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.remove("second"); + dateFromParts.second(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + int millisecond = 35; + parts.put("millisecond", millisecond); + assertThat(dateFromParts.millisecond(millisecond).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("millisecond", "$millisecond"); + assertThat(dateFromParts.millisecondOf("millisecond").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("millisecond", LITERAL); + assertThat(dateFromParts.millisecondOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), + is(doc)); + parts.remove("millisecond"); + dateFromParts.millisecond(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + + String timezone = "America/New_York"; + parts.put("timezone", timezone); + assertThat(dateFromParts.timezone(timezone).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("timezone", "$timezone"); + assertThat(dateFromParts.timezoneOf("timezone").toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + parts.put("timezone", LITERAL); + assertThat(dateFromParts.timezoneOf(asLiteral(VAR_FIELD)).toDate().toDocument(Aggregation.DEFAULT_CONTEXT), + is(doc)); + parts.remove("timezone"); + dateFromParts.timezone(null); + assertThat(dateFromParts.toDate().toDocument(Aggregation.DEFAULT_CONTEXT), is(doc)); + } + + @Test(expected = IllegalArgumentException.class) + public void testDateFromPartsNoCalendarYearException() { + dateFromParts().toDate(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDateFromPartsNoIsoWeekYearException() { + dateFromIsoWeekParts().toDate(); + } + + private void assertDateFieldOp(AggregationExpression operation, final String opName) { + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(VAR + opName, VAR_FIELD))); + } + + private void assertDateFieldTimezoneOp(AggregationExpression operation, final String opName) { + + final Document val = new Document(); + val.put("date", VAR_FIELD); + val.put("timezone", TIMEZONE); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(VAR + opName, val))); + } + + private void assertDateFieldTimezone2Op(AggregationExpression operation, final String opName) { + + final Document val = new Document(); + val.put("date", VAR_FIELD); + val.put("timezone", TIMEZONE2); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(VAR + opName, val))); + } + + private void assertDateExprOp(AggregationExpression operation, final String opName) { + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(VAR + opName, LITERAL))); + } + + private void assertDateExprTimezoneOp(AggregationExpression operation, final String opName) { + + final Document val = new Document(); + val.put("date", LITERAL); + val.put("timezone", TIMEZONE); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(VAR + opName, val))); + } + + private void assertDateExprTimezone2Op(AggregationExpression operation, final String opName) { + + final Document val = new Document(); + val.put("date", LITERAL); + val.put("timezone", TIMEZONE2); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(VAR + opName, val))); + } + + private void assertDateFieldStringNoTimezoneOp(AggregationExpression operation) { + + final Document val = new Document(); + val.put("date", VAR_FIELD); + val.put("format", FORMAT); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(TO_STRING_OP, val))); + } + + private void assertDateFieldStringTimezoneOp(AggregationExpression operation) { + + final Document val = new Document(); + val.put("date", VAR_FIELD); + val.put("format", FORMAT); + val.put("timezone", TIMEZONE); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(TO_STRING_OP, val))); + } + + private void assertDateFieldStringTimezone2Op(AggregationExpression operation) { + + final Document val = new Document(); + val.put("date", VAR_FIELD); + val.put("format", FORMAT); + val.put("timezone", TIMEZONE2); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(TO_STRING_OP, val))); + } + + private void assertDateExprStringNoTimezoneOp(AggregationExpression operation) { + + final Document val = new Document(); + val.put("date", LITERAL); + val.put("format", FORMAT); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(TO_STRING_OP, val))); + } + + private void assertDateExprStringTimezoneOp(AggregationExpression operation) { + final Document val = new Document(); + val.put("date", LITERAL); + val.put("format", FORMAT); + val.put("timezone", TIMEZONE); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(TO_STRING_OP, val))); + } + + private void assertDateExprTimezone2Op(AggregationExpression operation) { + + final Document val = new Document(); + val.put("date", LITERAL); + val.put("format", FORMAT); + val.put("timezone", TIMEZONE2); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(TO_STRING_OP, val))); + } + + private void assertDateFromStringField(AggregationExpression operation) { + + final Document val = new Document(); + val.put("dateString", VAR_FIELD); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document("$dateFromString", val))); + } + + private void assertDateFromStringFieldTimezone(AggregationExpression operation) { + + final Document val = new Document(); + val.put("dateString", VAR_FIELD); + val.put("timezone", TIMEZONE); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document("$dateFromString", val))); + } + + private void assertDateFromStringFieldTimezone2(AggregationExpression operation) { + + final Document val = new Document(); + val.put("dateString", VAR_FIELD); + val.put("timezone", TIMEZONE2); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document("$dateFromString", val))); + } + + private void assertDateFromStringExpr(AggregationExpression operation) { + + final Document val = new Document(); + val.put("dateString", LITERAL); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document("$dateFromString", val))); + } + + private void assertDateFromStringExprTimezone(AggregationExpression operation) { + + final Document val = new Document(); + val.put("dateString", LITERAL); + val.put("timezone", TIMEZONE); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document("$dateFromString", val))); + } + + private void assertDateFromStringExprTimezone2(AggregationExpression operation) { + + final Document val = new Document(); + val.put("dateString", LITERAL); + val.put("timezone", TIMEZONE2); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document("$dateFromString", val))); + } + + private void assertCurrentDateFromString(AggregationExpression operation) { + + final Document val = new Document(); + val.put("dateString", CURRENT_DATE); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document("$dateFromString", val))); + } + + private void assertCurrentDateFromStringTimezone(AggregationExpression operation) { + + final Document val = new Document(); + val.put("dateString", CURRENT_DATE); + val.put("timezone", TIMEZONE); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document("$dateFromString", val))); + } + + private void assertCurrentDateFromStringTimezone2(AggregationExpression operation) { + + final Document val = new Document(); + val.put("dateString", CURRENT_DATE); + val.put("timezone", TIMEZONE2); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document("$dateFromString", val))); + } + + private void assertDateToParts(AggregationExpression operation, Boolean iso8601, Object dateValue, + Object timezoneValue) { + + final Document val = new Document(); + val.put("date", dateValue); + if (iso8601 != null) { + val.put("iso8601", iso8601); + } + if (timezoneValue != null) { + val.put("timezone", timezoneValue); + } + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document("$dateToParts", val))); + } + + private void assertDateToPartsField(AggregationExpression operation, Boolean iso8601) { + assertDateToParts(operation, iso8601, VAR_FIELD, null); + } + + private void assertDateToPartsFieldTimezone(AggregationExpression operation, Boolean iso8601) { + assertDateToParts(operation, iso8601, VAR_FIELD, TIMEZONE); + } + + private void assertDateToPartsFieldTimezone2(AggregationExpression operation, Boolean iso8601) { + assertDateToParts(operation, iso8601, VAR_FIELD, TIMEZONE2); + } + + private void assertDateToPartsExpr(AggregationExpression operation, Boolean iso8601) { + assertDateToParts(operation, iso8601, LITERAL, null); + } + + private void assertDateToPartsExprTimezone(AggregationExpression operation, Boolean iso8601) { + assertDateToParts(operation, iso8601, LITERAL, TIMEZONE); + } + + private void assertDateToPartsExprTimezone2(AggregationExpression operation, Boolean iso8601) { + assertDateToParts(operation, iso8601, LITERAL, TIMEZONE2); + } + + private void assertCurrentDateToParts(AggregationExpression operation, Boolean iso8601) { + assertDateToParts(operation, iso8601, CURRENT_DATE, null); + } + + private void assertCurrentDateToPartsTimezone(AggregationExpression operation, Boolean iso8601) { + assertDateToParts(operation, iso8601, CURRENT_DATE, TIMEZONE); + } + + private void assertCurrentDateToPartsTimezone2(AggregationExpression operation, Boolean iso8601) { + assertDateToParts(operation, iso8601, CURRENT_DATE, TIMEZONE2); + } + + private void assertQuarter(AggregationExpression operation, final Document monthDoc) { + final Document document = new Document("$cond", + new Document() + .append("if", + new Document("$lte", + Lists.newArrayList(monthDoc, + 3))) + .append("then", 1).append("else", + new Document("$cond", + new Document().append("if", new Document("$lte", Lists.newArrayList(monthDoc, 6))).append("then", 2) + .append("else", + new Document("$cond", + new Document().append("if", new Document("$lte", Lists.newArrayList(monthDoc, 9))) + .append("then", 3).append("else", 4)))))); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(document)); + } + + private void assertQuarterFieldOp(AggregationExpression operation) { + assertQuarter(operation, new Document("$month", VAR_FIELD)); + } + + private void assertQuarterFieldTimezoneOp(AggregationExpression operation) { + final Document val = new Document(); + val.put("date", VAR_FIELD); + val.put("timezone", TIMEZONE); + assertQuarter(operation, new Document("$month", val)); + } + + private void assertQuarterFieldTimezone2Op(AggregationExpression operation) { + final Document val = new Document(); + val.put("date", VAR_FIELD); + val.put("timezone", TIMEZONE2); + assertQuarter(operation, new Document("$month", val)); + } + + private void assertQuarterExprOp(AggregationExpression operation) { + assertQuarter(operation, new Document("$month", LITERAL)); + } + + private void assertQuarterExprTimezoneOp(AggregationExpression operation) { + final Document val = new Document(); + val.put("date", LITERAL); + val.put("timezone", TIMEZONE); + assertQuarter(operation, new Document("$month", val)); + } + + private void assertQuarterExprStringTimezone2Op(AggregationExpression operation) { + final Document val = new Document(); + val.put("date", LITERAL); + val.put("timezone", TIMEZONE2); + assertQuarter(operation, new Document("$month", val)); + } + + private void assertQuarterCurrentDateOp(AggregationExpression operation) { + assertQuarter(operation, new Document("$month", CURRENT_DATE)); + } + + private void assertQuarterCurrentDateTimezoneOp(AggregationExpression operation) { + final Document val = new Document(); + val.put("date", CURRENT_DATE); + val.put("timezone", TIMEZONE); + assertQuarter(operation, new Document("$month", val)); + } + + private void assertQuarterCurrentDateTimezone2Op(AggregationExpression operation) { + final Document val = new Document(); + val.put("date", CURRENT_DATE); + val.put("timezone", TIMEZONE2); + assertQuarter(operation, new Document("$month", val)); + } + + private void assertCurrentDateOp(AggregationExpression operation, final String opName) { + + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(VAR + opName, CURRENT_DATE))); + } + + private void assertCurrentDateTimezoneOp(AggregationExpression operation, final String opName) { + + final Document val = new Document(); + val.put("date", CURRENT_DATE); + val.put("timezone", TIMEZONE); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(VAR + opName, val))); + } + + private void assertCurrentDateTimezone2Op(AggregationExpression operation, final String opName) { + + final Document val = new Document(); + val.put("date", CURRENT_DATE); + val.put("timezone", TIMEZONE2); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(VAR + opName, val))); + } + + private void assertCurrentDateStringNoTimezoneOp(AggregationExpression operation) { + + final Document val = new Document(); + val.put("date", CURRENT_DATE); + val.put("format", FORMAT); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(TO_STRING_OP, val))); + } + + private void assertCurrentDateStringTimezoneOp(AggregationExpression operation) { + + final Document val = new Document(); + val.put("date", CURRENT_DATE); + val.put("format", FORMAT); + val.put("timezone", TIMEZONE); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(TO_STRING_OP, val))); + } + + private void assertCurrentDateStringTimezone2Op(AggregationExpression operation) { + + final Document val = new Document(); + val.put("date", CURRENT_DATE); + val.put("format", FORMAT); + val.put("timezone", TIMEZONE2); + assertThat(operation.toDocument(Aggregation.DEFAULT_CONTEXT), is(new Document(TO_STRING_OP, val))); + } + +}