diff --git a/pom.xml b/pom.xml
index b688f3ee50..509dab4499 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.3.0-SNAPSHOT
+ 3.3.0-GH-3713-SNAPSHOT
pom
Spring Data MongoDB
diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml
index 0033bd11d5..ef421d97ae 100644
--- a/spring-data-mongodb-benchmarks/pom.xml
+++ b/spring-data-mongodb-benchmarks/pom.xml
@@ -7,7 +7,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.3.0-SNAPSHOT
+ 3.3.0-GH-3713-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index f62c8dc7f4..b56b9b8ccc 100644
--- a/spring-data-mongodb-distribution/pom.xml
+++ b/spring-data-mongodb-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.3.0-SNAPSHOT
+ 3.3.0-GH-3713-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index 1f157e75bc..aa0a783459 100644
--- a/spring-data-mongodb/pom.xml
+++ b/spring-data-mongodb/pom.xml
@@ -11,7 +11,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.3.0-SNAPSHOT
+ 3.3.0-GH-3713-SNAPSHOT
../pom.xml
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 15f10f7d6c..a97d64c52d 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
@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core.aggregation;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -45,6 +46,19 @@ public static DateOperatorFactory dateOf(String fieldReference) {
return new DateOperatorFactory(fieldReference);
}
+ /**
+ * Take the date referenced by given {@literal fieldReference}.
+ *
+ * @param fieldReference must not be {@literal null}.
+ * @return new instance of {@link DateOperatorFactory}.
+ * @since 3.3
+ */
+ public static DateOperatorFactory zonedDateOf(String fieldReference, Timezone timezone) {
+
+ Assert.notNull(fieldReference, "FieldReference must not be null!");
+ return new DateOperatorFactory(fieldReference).withTimezone(timezone);
+ }
+
/**
* Take the date resulting from the given {@link AggregationExpression}.
*
@@ -57,6 +71,19 @@ public static DateOperatorFactory dateOf(AggregationExpression expression) {
return new DateOperatorFactory(expression);
}
+ /**
+ * Take the date resulting from the given {@link AggregationExpression}.
+ *
+ * @param expression must not be {@literal null}.
+ * @return new instance of {@link DateOperatorFactory}.
+ * @since 3.3
+ */
+ public static DateOperatorFactory zonedDateOf(AggregationExpression expression, Timezone timezone) {
+
+ Assert.notNull(expression, "Expression must not be null!");
+ return new DateOperatorFactory(expression).withTimezone(timezone);
+ }
+
/**
* Take the given value as date.
*
@@ -156,7 +183,7 @@ public static Timezone none() {
* representing an Olson Timezone Identifier or UTC Offset.
*
* @param value the plain timezone {@link String}, a {@link Field} holding the timezone or an
- * {@link AggregationExpression} resulting in the timezone.
+ * {@link AggregationExpression} resulting in the timezone.
* @return new instance of {@link Timezone}.
*/
public static Timezone valueOf(Object value) {
@@ -274,6 +301,41 @@ public DateOperatorFactory withTimezone(Timezone timezone) {
return new DateOperatorFactory(fieldReference, expression, dateValue, timezone);
}
+ /**
+ * Creates new {@link AggregationExpression} that adds the value of the given {@link AggregationExpression
+ * expression} (in {@literal units). @param expression must not be {@literal null}.
+ *
+ * @param unit the unit of measure. Must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ * @since 3.3
+ */
+ public DateAdd addValueOf(AggregationExpression expression, String unit) {
+ return applyTimezone(DateAdd.addValueOf(expression, unit).toDate(dateReference()), timezone);
+ }
+
+ /**
+ * Creates new {@link AggregationExpression} that adds the value stored at the given {@literal field} (in
+ * {@literal units). @param fieldReference must not be {@literal null}.
+ *
+ * @param unit the unit of measure. Must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ * @since 3.3
+ */
+ public DateAdd addValueOf(String fieldReference, String unit) {
+ return applyTimezone(DateAdd.addValueOf(fieldReference, unit).toDate(dateReference()), timezone);
+ }
+
+ /**
+ * Creates new {@link AggregationExpression} that adds the given value (in {@literal units). @param value must not
+ * be {@literal null}. @param unit the unit of measure. Must not be {@literal null}.
+ *
+ * @return
+ * @since 3.3 new instance of {@link DateAdd}.
+ */
+ public DateAdd add(Object value, String unit) {
+ return applyTimezone(DateAdd.addValue(value, unit).toDate(dateReference()), timezone);
+ }
+
/**
* Creates new {@link AggregationExpression} that returns the day of the year for a date as a number between 1 and
* 366.
@@ -304,6 +366,42 @@ public DayOfWeek dayOfWeek() {
return applyTimezone(DayOfWeek.dayOfWeek(dateReference()), timezone);
}
+ /**
+ * Creates new {@link AggregationExpression} that calculates the difference (in {@literal units) to the date
+ * computed by the given {@link AggregationExpression expression}. @param expression must not be {@literal null}.
+ *
+ * @param unit the unit of measure. Must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ * @since 3.3
+ */
+ public DateDiff diffValueOf(AggregationExpression expression, String unit) {
+ return applyTimezone(DateDiff.diffValueOf(expression, unit).toDate(dateReference()), timezone);
+ }
+
+ /**
+ * Creates new {@link AggregationExpression} that calculates the difference (in {@literal units) to the date stored
+ * at the given {@literal field}. @param expression must not be {@literal null}.
+ *
+ * @param unit the unit of measure. Must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ * @since 3.3
+ */
+ public DateDiff diffValueOf(String fieldReference, String unit) {
+ return applyTimezone(DateDiff.diffValueOf(fieldReference, unit).toDate(dateReference()), timezone);
+ }
+
+ /**
+ * Creates new {@link AggregationExpression} that calculates the difference (in {@literal units) to the date given
+ * {@literal value}. @param value anything the resolves to a valid date. Must not be {@literal null}.
+ *
+ * @param unit the unit of measure. Must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ * @since 3.3
+ */
+ public DateDiff diff(Object value, String unit) {
+ return applyTimezone(DateDiff.diffValue(value, unit).toDate(dateReference()), timezone);
+ }
+
/**
* Creates new {@link AggregationExpression} that returns the year portion of a date.
*
@@ -1480,7 +1578,6 @@ protected java.util.Map append(String key, Object value) {
} else {
clone.put("timezone", ((Timezone) value).value);
}
-
} else {
clone.put(key, value);
}
@@ -1911,7 +2008,7 @@ default T millisecondOf(AggregationExpression expression) {
* @author Matt Morrissette
* @author Christoph Strobl
* @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/
+ * "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/
* @since 2.1
*/
public static class DateFromParts extends TimezonedDateAggregationExpression implements DateParts {
@@ -2086,7 +2183,7 @@ default DateFromParts yearOf(AggregationExpression expression) {
* @author Matt Morrissette
* @author Christoph Strobl
* @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/
+ * "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromParts/
* @since 2.1
*/
public static class IsoDateFromParts extends TimezonedDateAggregationExpression
@@ -2262,7 +2359,7 @@ default IsoDateFromParts isoWeekYearOf(AggregationExpression expression) {
* @author Matt Morrissette
* @author Christoph Strobl
* @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/
+ * "https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateToParts/
* @since 2.1
*/
public static class DateToParts extends TimezonedDateAggregationExpression {
@@ -2343,7 +2440,7 @@ protected String getMongoMethod() {
* @author Matt Morrissette
* @author Christoph Strobl
* @see https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/
+ * "https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/">https://docs.mongodb.com/manual/reference/operator/aggregation/dateFromString/
* @since 2.1
*/
public static class DateFromString extends TimezonedDateAggregationExpression {
@@ -2418,6 +2515,211 @@ protected String getMongoMethod() {
}
}
+ /**
+ * {@link AggregationExpression} for {@code $dateAdd}.
+ * NOTE: Requires MongoDB 5.0 or later.
+ *
+ * @author Christoph Strobl
+ * @since 3.3
+ */
+ public static class DateAdd extends TimezonedDateAggregationExpression {
+
+ private DateAdd(Object value) {
+ super(value);
+ }
+
+ /**
+ * Add the number of {@literal units} of the result of the given {@link AggregationExpression expression} to a
+ * {@link #toDate(Object) start date}.
+ *
+ * @param expression must not be {@literal null}.
+ * @param unit must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public static DateAdd addValueOf(AggregationExpression expression, String unit) {
+ return addValue(expression, unit);
+ }
+
+ /**
+ * Add the number of {@literal units} from a {@literal field} to a {@link #toDate(Object) start date}.
+ *
+ * @param fieldReference must not be {@literal null}.
+ * @param unit must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public static DateAdd addValueOf(String fieldReference, String unit) {
+ return addValue(Fields.field(fieldReference), unit);
+ }
+
+ /**
+ * Add the number of {@literal units} to a {@link #toDate(Object) start date}.
+ *
+ * @param value must not be {@literal null}.
+ * @param unit must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public static DateAdd addValue(Object value, String unit) {
+
+ Map args = new HashMap<>();
+ args.put("unit", unit);
+ args.put("amount", value);
+ return new DateAdd(args);
+ }
+
+ /**
+ * Define the start date, in UTC, for the addition operation.
+ *
+ * @param expression must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public DateAdd toDateOf(AggregationExpression expression) {
+ return toDate(expression);
+ }
+
+ /**
+ * Define the start date, in UTC, for the addition operation.
+ *
+ * @param fieldReference must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public DateAdd toDateOf(String fieldReference) {
+ return toDate(Fields.field(fieldReference));
+ }
+
+ /**
+ * Define the start date, in UTC, for the addition operation.
+ *
+ * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public DateAdd toDate(Object dateExpression) {
+ return new DateAdd(append("startDate", dateExpression));
+ }
+
+ /**
+ * Optionally set the {@link Timezone} to use. If not specified {@literal UTC} is used.
+ *
+ * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead.
+ * @return new instance of {@link DateAdd}.
+ */
+ public DateAdd withTimezone(Timezone timezone) {
+ return new DateAdd(appendTimezone(argumentMap(), timezone));
+ }
+
+ @Override
+ protected String getMongoMethod() {
+ return "$dateAdd";
+ }
+ }
+
+ /**
+ * {@link AggregationExpression} for {@code $dateDiff}.
+ * NOTE: Requires MongoDB 5.0 or later.
+ *
+ * @author Christoph Strobl
+ * @since 3.3
+ */
+ public static class DateDiff extends TimezonedDateAggregationExpression {
+
+ private DateDiff(Object value) {
+ super(value);
+ }
+
+ /**
+ * Add the number of {@literal units} of the result of the given {@link AggregationExpression expression} to a
+ * {@link #toDate(Object) start date}.
+ *
+ * @param expression must not be {@literal null}.
+ * @param unit must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public static DateDiff diffValueOf(AggregationExpression expression, String unit) {
+ return diffValue(expression, unit);
+ }
+
+ /**
+ * Add the number of {@literal units} from a {@literal field} to a {@link #toDate(Object) start date}.
+ *
+ * @param fieldReference must not be {@literal null}.
+ * @param unit must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public static DateDiff diffValueOf(String fieldReference, String unit) {
+ return diffValue(Fields.field(fieldReference), unit);
+ }
+
+ /**
+ * Add the number of {@literal units} to a {@link #toDate(Object) start date}.
+ *
+ * @param value must not be {@literal null}.
+ * @param unit must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public static DateDiff diffValue(Object value, String unit) {
+
+ Map args = new HashMap<>();
+ args.put("unit", unit);
+ args.put("endDate", value);
+ return new DateDiff(args);
+ }
+
+ /**
+ * Define the start date, in UTC, for the addition operation.
+ *
+ * @param expression must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public DateDiff toDateOf(AggregationExpression expression) {
+ return toDate(expression);
+ }
+
+ /**
+ * Define the start date, in UTC, for the addition operation.
+ *
+ * @param fieldReference must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public DateDiff toDateOf(String fieldReference) {
+ return toDate(Fields.field(fieldReference));
+ }
+
+ /**
+ * Define the start date, in UTC, for the addition operation.
+ *
+ * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}.
+ * @return new instance of {@link DateAdd}.
+ */
+ public DateDiff toDate(Object dateExpression) {
+ return new DateDiff(append("startDate", dateExpression));
+ }
+
+ /**
+ * Optionally set the {@link Timezone} to use. If not specified {@literal UTC} is used.
+ *
+ * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead.
+ * @return new instance of {@link DateAdd}.
+ */
+ public DateDiff withTimezone(Timezone timezone) {
+ return new DateDiff(appendTimezone(argumentMap(), timezone));
+ }
+
+ /**
+ * Set the start day of the week if the unit if measure is set to {@literal week}. Uses {@literal Sunday} by
+ * default.
+ *
+ * @param day must not be {@literal null}.
+ * @return new instance of {@link DateDiff}.
+ */
+ public DateDiff startOfWeek(Object day) {
+ return new DateDiff(append("startOfWeek", day));
+ }
+
+ @Override
+ protected String getMongoMethod() {
+ return "$dateDiff";
+ }
+ }
+
@SuppressWarnings("unchecked")
private static T applyTimezone(T instance, Timezone timezone) {
return !ObjectUtils.nullSafeEquals(Timezone.none(), timezone) && !instance.hasTimezone()
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java
index 5a2c48bc20..2eb1c8a1b3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/MethodReferenceNode.java
@@ -130,6 +130,8 @@ public class MethodReferenceNode extends ExpressionNode {
map.put("literal", singleArgRef().forOperator("$literal"));
// DATE OPERATORS
+ map.put("dateAdd", mapArgRef().forOperator("$dateAdd").mappingParametersTo("startDate", "unit", "amount", "timezone"));
+ map.put("dateDiff", mapArgRef().forOperator("$dateDiff").mappingParametersTo("startDate", "endDate", "unit","timezone", "startOfWeek"));
map.put("dayOfYear", singleArgRef().forOperator("$dayOfYear"));
map.put("dayOfMonth", singleArgRef().forOperator("$dayOfMonth"));
map.put("dayOfWeek", singleArgRef().forOperator("$dayOfWeek"));
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 0000000000..95f977ed73
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DateOperatorsUnitTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2021. 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
+ *
+ * https://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.assertj.core.api.Assertions.*;
+
+import org.bson.Document;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.mongodb.core.aggregation.DateOperators.Timezone;
+
+/**
+ * @author Christoph Strobl
+ */
+class DateOperatorsUnitTests {
+
+ @Test // GH-3713
+ void rendersDateAdd() {
+
+ assertThat(DateOperators.dateOf("purchaseDate").add(3, "day").toDocument(Aggregation.DEFAULT_CONTEXT))
+ .isEqualTo(Document.parse("{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3 } }"));
+ }
+
+ @Test // GH-3713
+ void rendersDateAddWithTimezone() {
+
+ assertThat(DateOperators.zonedDateOf("purchaseDate", Timezone.valueOf("America/Chicago")).add(3, "day")
+ .toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo(Document.parse(
+ "{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3, timezone : \"America/Chicago\" } }"));
+ }
+
+ @Test // GH-3713
+ void rendersDateDiff() {
+
+ assertThat(
+ DateOperators.dateOf("purchaseDate").diffValueOf("delivered", "day").toDocument(Aggregation.DEFAULT_CONTEXT))
+ .isEqualTo(Document
+ .parse("{ $dateDiff: { startDate: \"$purchaseDate\", endDate: \"$delivered\", unit: \"day\" } }"));
+ }
+
+ @Test // GH-3713
+ void rendersDateDiffWithTimezone() {
+
+ assertThat(DateOperators.zonedDateOf("purchaseDate", Timezone.valueOf("America/Chicago"))
+ .diffValueOf("delivered", "day").toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo(Document.parse(
+ "{ $dateDiff: { startDate: \"$purchaseDate\", endDate: \"$delivered\", unit: \"day\", timezone : \"America/Chicago\" } }"));
+ }
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java
index b67beed126..532b58dce2 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformerUnitTests.java
@@ -946,6 +946,16 @@ public void shouldRenderRoundWithPlace() {
assertThat(transform("round(field, 2)")).isEqualTo(Document.parse("{ \"$round\" : [\"$field\", 2]}"));
}
+ @Test // GH-3713
+ void shouldRenderDateAdd() {
+ assertThat(transform("dateAdd(purchaseDate, 'day', 3)")).isEqualTo(Document.parse("{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3 } }"));
+ }
+
+ @Test // GH-3713
+ void shouldRenderDateDiff() {
+ assertThat(transform("dateDiff(purchaseDate, delivered, 'day')")).isEqualTo(Document.parse("{ $dateDiff: { startDate: \"$purchaseDate\", endDate: \"$delivered\", unit: \"day\" } }"));
+ }
+
private Object transform(String expression, Object... params) {
Object result = transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params);
return result == null ? null : (!(result instanceof org.bson.Document) ? result.toString() : result);