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 a97d64c52..029b994f2 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 @@ -15,10 +15,16 @@ */ package org.springframework.data.mongodb.core.aggregation; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -157,6 +163,7 @@ public class DateOperators { * NOTE: Support for timezones in aggregations Requires MongoDB 3.6 or later. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.1 */ public static class Timezone { @@ -192,6 +199,61 @@ public class DateOperators { return new Timezone(value); } + /** + * Create a {@link Timezone} for the given {@link TimeZone} rendering the offset as UTC offset. + * + * @param timeZone {@link TimeZone} rendering the offset as UTC offset. + * @return new instance of {@link Timezone}. + * @since 3.3 + */ + public static Timezone fromOffset(TimeZone timeZone) { + + Assert.notNull(timeZone, "TimeZone must not be null!"); + + return fromOffset( + ZoneOffset.ofTotalSeconds(Math.toIntExact(TimeUnit.MILLISECONDS.toSeconds(timeZone.getRawOffset())))); + } + + /** + * Create a {@link Timezone} for the given {@link ZoneOffset} rendering the offset as UTC offset. + * + * @param offset {@link ZoneOffset} rendering the offset as UTC offset. + * @return new instance of {@link Timezone}. + * @since 3.3 + */ + public static Timezone fromOffset(ZoneOffset offset) { + + Assert.notNull(offset, "ZoneOffset must not be null!"); + return new Timezone(offset.toString()); + } + + /** + * Create a {@link Timezone} for the given {@link TimeZone} rendering the offset as UTC offset. + * + * @param timeZone {@link Timezone} rendering the offset as zone identifier. + * @return new instance of {@link Timezone}. + * @since 3.3 + */ + public static Timezone fromZone(TimeZone timeZone) { + + Assert.notNull(timeZone, "TimeZone must not be null!"); + + return valueOf(timeZone.getID()); + } + + /** + * Create a {@link Timezone} for the given {@link java.time.ZoneId} rendering the offset as UTC offset. + * + * @param zoneId {@link ZoneId} rendering the offset as zone identifier. + * @return new instance of {@link Timezone}. + * @since 3.3 + */ + public static Timezone fromZone(ZoneId zoneId) { + + Assert.notNull(zoneId, "ZoneId must not be null!"); + return new Timezone(zoneId.toString()); + } + /** * Create a {@link Timezone} for the {@link Field} reference holding the Olson Timezone Identifier or UTC Offset. * @@ -212,6 +274,11 @@ public class DateOperators { public static Timezone ofExpression(AggregationExpression expression) { return valueOf(expression); } + + @Nullable + Object getValue() { + return value; + } } /** @@ -303,32 +370,64 @@ public class DateOperators { /** * Creates new {@link AggregationExpression} that adds the value of the given {@link AggregationExpression - * expression} (in {@literal units). @param expression must not be {@literal null}. - * + * 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 + * @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}. - * + * 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 + * @return new instance of {@link DateAdd}. @since 3.3 + */ + public DateAdd addValueOf(AggregationExpression expression, TemporalUnit unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + return applyTimezone(DateAdd.addValueOf(expression, unit.name().toLowerCase(Locale.ROOT)).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}. - * + * 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, TemporalUnit unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + + return applyTimezone( + DateAdd.addValueOf(fieldReference, unit.name().toLowerCase(Locale.ROOT)).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}. */ @@ -336,6 +435,22 @@ public class DateOperators { return applyTimezone(DateAdd.addValue(value, 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, TemporalUnit unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + + return applyTimezone(DateAdd.addValue(value, unit.name().toLowerCase(Locale.ROOT)).toDate(dateReference()), + timezone); + } + /** * Creates new {@link AggregationExpression} that returns the day of the year for a date as a number between 1 and * 366. @@ -367,41 +482,89 @@ public class DateOperators { } /** - * 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}. - * + * 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 + * @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}. - * + * 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 + * @return new instance of {@link DateAdd}. @since 3.3 + */ + public DateDiff diffValueOf(AggregationExpression expression, TemporalUnit unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + + return applyTimezone( + DateDiff.diffValueOf(expression, unit.name().toLowerCase(Locale.ROOT)).toDate(dateReference()), timezone); + } + + /** + * Creates new {@link AggregationExpression} that calculates the difference (in {@literal units}) to the date stored + * at the given {@literal field}. + * + * @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 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}. - * + * Creates new {@link AggregationExpression} that calculates the difference (in {@literal units}) to the date stored + * at the given {@literal field}. + * + * @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 + * @return new instance of {@link DateAdd}. @since 3.3 + */ + public DateDiff diffValueOf(String fieldReference, TemporalUnit unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + + return applyTimezone( + DateDiff.diffValueOf(fieldReference, unit.name().toLowerCase(Locale.ROOT)).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 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, TemporalUnit unit) { + + Assert.notNull(unit, "TemporalUnit must not be null"); + + return applyTimezone(DateDiff.diffValue(value, unit.name().toLowerCase(Locale.ROOT)).toDate(dateReference()), + timezone); + } + /** * Creates new {@link AggregationExpression} that returns the year portion of a date. * @@ -2720,6 +2883,85 @@ public class DateOperators { } } + /** + * Interface defining a temporal unit for date operators. + * + * @author Mark Paluch + * @since 3.3 + */ + public interface TemporalUnit { + + String name(); + + /** + * Converts the given time unit into a {@link TemporalUnit}. Supported units are: days, hours, minutes, seconds, and + * milliseconds. + * + * @param timeUnit the time unit to convert, must not be {@literal null}. + * @return + * @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion. + */ + static TemporalUnit from(TimeUnit timeUnit) { + + Assert.notNull(timeUnit, "TimeUnit must not be null"); + + switch (timeUnit) { + case DAYS: + return TemporalUnits.DAY; + case HOURS: + return TemporalUnits.HOUR; + case MINUTES: + return TemporalUnits.MINUTE; + case SECONDS: + return TemporalUnits.SECOND; + case MILLISECONDS: + return TemporalUnits.MILLISECOND; + } + + throw new IllegalArgumentException(String.format("Cannot create TemporalUnit from %s", timeUnit)); + } + + /** + * Converts the given chrono unit into a {@link TemporalUnit}. Supported units are: years, weeks, months, days, + * hours, minutes, seconds, and millis. + * + * @param chronoUnit the chrono unit to convert, must not be {@literal null}. + * @return + * @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion. + */ + static TemporalUnit from(ChronoUnit chronoUnit) { + + switch (chronoUnit) { + case YEARS: + return TemporalUnits.YEAR; + case WEEKS: + return TemporalUnits.WEEK; + case MONTHS: + return TemporalUnits.MONTH; + case DAYS: + return TemporalUnits.DAY; + case HOURS: + return TemporalUnits.HOUR; + case MINUTES: + return TemporalUnits.MINUTE; + case SECONDS: + return TemporalUnits.SECOND; + case MILLIS: + return TemporalUnits.MILLISECOND; + } + + throw new IllegalArgumentException(String.format("Cannot create TemporalUnit from %s", chronoUnit)); + } + } + + /** + * Supported temporal units. + */ + enum TemporalUnits implements TemporalUnit { + YEAR, QUARTER, WEEK, MONTH, DAY, HOUR, MINUTE, SECOND, MILLISECOND + + } + @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/aggregation/SetWindowFieldsOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java index 9c40a0b64..fa01b02b9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java @@ -15,9 +15,11 @@ */ package org.springframework.data.mongodb.core.aggregation; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.TimeUnit; import org.bson.Document; import org.springframework.data.domain.Sort; @@ -626,7 +628,68 @@ public class SetWindowFieldsOperation * The actual time unit to apply to a {@link Window}. */ public interface WindowUnit { + String name(); + + /** + * Converts the given time unit into a {@link WindowUnit}. Supported units are: days, hours, minutes, seconds, and + * milliseconds. + * + * @param timeUnit the time unit to convert, must not be {@literal null}. + * @return + * @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion. + */ + static WindowUnit from(TimeUnit timeUnit) { + + Assert.notNull(timeUnit, "TimeUnit must not be null"); + + switch (timeUnit) { + case DAYS: + return WindowUnits.DAY; + case HOURS: + return WindowUnits.HOUR; + case MINUTES: + return WindowUnits.MINUTE; + case SECONDS: + return WindowUnits.SECOND; + case MILLISECONDS: + return WindowUnits.MILLISECOND; + } + + throw new IllegalArgumentException(String.format("Cannot create WindowUnit from %s", timeUnit)); + } + + /** + * Converts the given chrono unit into a {@link WindowUnit}. Supported units are: years, weeks, months, days, hours, + * minutes, seconds, and millis. + * + * @param chronoUnit the chrono unit to convert, must not be {@literal null}. + * @return + * @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion. + */ + static WindowUnit from(ChronoUnit chronoUnit) { + + switch (chronoUnit) { + case YEARS: + return WindowUnits.YEAR; + case WEEKS: + return WindowUnits.WEEK; + case MONTHS: + return WindowUnits.MONTH; + case DAYS: + return WindowUnits.DAY; + case HOURS: + return WindowUnits.HOUR; + case MINUTES: + return WindowUnits.MINUTE; + case SECONDS: + return WindowUnits.SECOND; + case MILLIS: + return WindowUnits.MILLISECOND; + } + + throw new IllegalArgumentException(String.format("Cannot create WindowUnit from %s", chronoUnit)); + } } /** 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 index 95f977ed7..6d63b954f 100644 --- 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 @@ -15,14 +15,22 @@ */ package org.springframework.data.mongodb.core.aggregation; -import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; + +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.TimeZone; -import org.bson.Document; import org.junit.jupiter.api.Test; + import org.springframework.data.mongodb.core.aggregation.DateOperators.Timezone; /** + * Unit tests for {@link DateOperators}. + * * @author Christoph Strobl + * @author Mark Paluch */ class DateOperatorsUnitTests { @@ -30,15 +38,15 @@ class DateOperatorsUnitTests { void rendersDateAdd() { assertThat(DateOperators.dateOf("purchaseDate").add(3, "day").toDocument(Aggregation.DEFAULT_CONTEXT)) - .isEqualTo(Document.parse("{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3 } }")); + .isEqualTo("{ $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\" } }")); + .toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo( + "{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3, timezone : \"America/Chicago\" } }"); } @Test // GH-3713 @@ -46,15 +54,37 @@ class DateOperatorsUnitTests { assertThat( DateOperators.dateOf("purchaseDate").diffValueOf("delivered", "day").toDocument(Aggregation.DEFAULT_CONTEXT)) - .isEqualTo(Document - .parse("{ $dateDiff: { startDate: \"$purchaseDate\", endDate: \"$delivered\", unit: \"day\" } }")); + .isEqualTo("{ $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\" } }")); + .diffValueOf("delivered", DateOperators.TemporalUnit.from(ChronoUnit.DAYS)) + .toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo( + "{ $dateDiff: { startDate: \"$purchaseDate\", endDate: \"$delivered\", unit: \"day\", timezone : \"America/Chicago\" } }"); + } + + @Test // GH-3713 + void rendersTimezoneFromZoneOffset() { + assertThat(DateOperators.Timezone.fromOffset(ZoneOffset.ofHoursMinutes(3, 30)).getValue()).isEqualTo("+03:30"); + } + + @Test // GH-3713 + void rendersTimezoneFromTimeZoneOffset() { + assertThat(DateOperators.Timezone.fromOffset(TimeZone.getTimeZone("America/Chicago")).getValue()) + .isEqualTo("-06:00"); + } + + @Test // GH-3713 + void rendersTimezoneFromTimeZoneId() { + assertThat(DateOperators.Timezone.fromZone(TimeZone.getTimeZone("America/Chicago")).getValue()) + .isEqualTo("America/Chicago"); + } + + @Test // GH-3713 + void rendersTimezoneFromZoneId() { + assertThat(DateOperators.Timezone.fromZone(ZoneId.of("America/Chicago")).getValue()).isEqualTo("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 33edab1d5..bc8da0a3c 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 @@ -1034,11 +1034,6 @@ public class SpelExpressionTransformerUnitTests { assertThat(transform("tanh(angle)")).isEqualTo("{ \"$tanh\" : \"$angle\"}"); } - private Document transform(String expression, Object... params) { - return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); - } - - private Object transformValue(String expression, Object... params) { @Test // GH-3713 void shouldRenderDateAdd() { assertThat(transform("dateAdd(purchaseDate, 'day', 3)")).isEqualTo(Document.parse("{ $dateAdd: { startDate: \"$purchaseDate\", unit: \"day\", amount: 3 } }")); @@ -1049,7 +1044,11 @@ public class SpelExpressionTransformerUnitTests { assertThat(transform("dateDiff(purchaseDate, delivered, 'day')")).isEqualTo(Document.parse("{ $dateDiff: { startDate: \"$purchaseDate\", endDate: \"$delivered\", unit: \"day\" } }")); } - private Object transform(String expression, Object... params) { + private Document transform(String expression, Object... params) { + return (Document) transformer.transform(expression, Aggregation.DEFAULT_CONTEXT, params); + } + + private Object transformValue(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); } diff --git a/src/main/asciidoc/reference/aggregation-framework.adoc b/src/main/asciidoc/reference/aggregation-framework.adoc index f23b29069..bc7a032e7 100644 --- a/src/main/asciidoc/reference/aggregation-framework.adoc +++ b/src/main/asciidoc/reference/aggregation-framework.adoc @@ -100,7 +100,7 @@ At the time of this writing, we provide support for the following Aggregation Op | `literal` | Date Aggregation Operators -| `dayOfYear`, `dayOfMonth`, `dayOfWeek`, `year`, `month`, `week`, `hour`, `minute`, `second`, `millisecond`, `dateToString`, `dateFromString`, `dateFromParts`, `dateToParts`, `isoDayOfWeek`, `isoWeek`, `isoWeekYear` +| `dayOfYear`, `dayOfMonth`, `dayOfWeek`, `year`, `month`, `week`, `hour`, `minute`, `second`, `millisecond`, `dateAdd`, `dateDiff`, `dateToString`, `dateFromString`, `dateFromParts`, `dateToParts`, `isoDayOfWeek`, `isoWeek`, `isoWeekYear` | Variable Operators | `map`