Polishing.

Introduce factory methods to convert TimeZone/ZoneId/ZoneOffset into Mongo Timezone. Introduce TemporalUnit abstraction and converters to convert ChronoUnit and TimeUnit into TemporalUnit for date operators accepting a unit parameter.

See #3713
Original pull request: #3748.
This commit is contained in:
Mark Paluch
2021-08-25 10:59:26 +02:00
parent 456c1ad26a
commit 24171b3ae2
5 changed files with 377 additions and 43 deletions

View File

@@ -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 {
* <strong>NOTE: </strong>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 extends TimezonedDateAggregationExpression> T applyTimezone(T instance, Timezone timezone) {
return !ObjectUtils.nullSafeEquals(Timezone.none(), timezone) && !instance.hasTimezone()

View File

@@ -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));
}
}
/**

View File

@@ -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");
}
}

View File

@@ -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);
}

View File

@@ -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`