mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-02-17 10:25:15 +00:00
Core: Create java time based DateMathParser (#32131)
This adds a java time based date math parser class in order, which will replace the joda date based one in the future. For now the class also returns the date in milliseconds since the epoch.
This commit is contained in:
parent
93d4f84b21
commit
798fb546cb
@ -20,8 +20,14 @@ package org.elasticsearch.common.time;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.time.temporal.TemporalField;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* wrapper class around java.time.DateTimeFormatter that supports multiple formats for easier parsing,
|
||||
@ -29,6 +35,13 @@ import java.time.temporal.TemporalAccessor;
|
||||
*/
|
||||
public class CompoundDateTimeFormatter {
|
||||
|
||||
private static final Consumer<DateTimeFormatter[]> SAME_TIME_ZONE_VALIDATOR = (parsers) -> {
|
||||
long distinctZones = Arrays.stream(parsers).map(DateTimeFormatter::getZone).distinct().count();
|
||||
if (distinctZones > 1) {
|
||||
throw new IllegalArgumentException("formatters must have the same time zone");
|
||||
}
|
||||
};
|
||||
|
||||
final DateTimeFormatter printer;
|
||||
final DateTimeFormatter[] parsers;
|
||||
|
||||
@ -36,6 +49,7 @@ public class CompoundDateTimeFormatter {
|
||||
if (parsers.length == 0) {
|
||||
throw new IllegalArgumentException("at least one date time formatter is required");
|
||||
}
|
||||
SAME_TIME_ZONE_VALIDATOR.accept(parsers);
|
||||
this.printer = parsers[0];
|
||||
this.parsers = parsers;
|
||||
}
|
||||
@ -58,7 +72,18 @@ public class CompoundDateTimeFormatter {
|
||||
throw failure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a specific time zone for a date formatter
|
||||
*
|
||||
* @param zoneId The zoneId this formatter shoulduse
|
||||
* @return The new formatter with all parsers switched to the specified timezone
|
||||
*/
|
||||
public CompoundDateTimeFormatter withZone(ZoneId zoneId) {
|
||||
// shortcurt to not create new objects unnecessarily
|
||||
if (zoneId.equals(parsers[0].getZone())) {
|
||||
return this;
|
||||
}
|
||||
|
||||
final DateTimeFormatter[] parsersWithZone = new DateTimeFormatter[parsers.length];
|
||||
for (int i = 0; i < parsers.length; i++) {
|
||||
parsersWithZone[i] = parsers[i].withZone(zoneId);
|
||||
@ -67,6 +92,20 @@ public class CompoundDateTimeFormatter {
|
||||
return new CompoundDateTimeFormatter(parsersWithZone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure defaults for missing values in a parser, then return a new compound date formatter
|
||||
*/
|
||||
CompoundDateTimeFormatter parseDefaulting(Map<TemporalField, Long> fields) {
|
||||
final DateTimeFormatter[] parsersWithDefaulting = new DateTimeFormatter[parsers.length];
|
||||
for (int i = 0; i < parsers.length; i++) {
|
||||
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder().append(parsers[i]);
|
||||
fields.forEach(builder::parseDefaulting);
|
||||
parsersWithDefaulting[i] = builder.toFormatter(Locale.ROOT);
|
||||
}
|
||||
|
||||
return new CompoundDateTimeFormatter(parsersWithDefaulting);
|
||||
}
|
||||
|
||||
public String format(TemporalAccessor accessor) {
|
||||
return printer.format(accessor);
|
||||
}
|
||||
|
@ -701,11 +701,15 @@ public class DateFormatters {
|
||||
/////////////////////////////////////////
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder()
|
||||
.appendValue(ChronoField.YEAR, 1, 4, SignStyle.NORMAL)
|
||||
.appendValue(ChronoField.YEAR, 1, 5, SignStyle.NORMAL)
|
||||
.optionalStart()
|
||||
.appendLiteral('-')
|
||||
.appendValue(MONTH_OF_YEAR, 1, 2, SignStyle.NOT_NEGATIVE)
|
||||
.optionalStart()
|
||||
.appendLiteral('-')
|
||||
.appendValue(DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE)
|
||||
.optionalEnd()
|
||||
.optionalEnd()
|
||||
.toFormatter(Locale.ROOT);
|
||||
|
||||
private static final DateTimeFormatter HOUR_MINUTE_FORMATTER = new DateTimeFormatterBuilder()
|
||||
@ -723,7 +727,11 @@ public class DateFormatters {
|
||||
.append(DATE_FORMATTER)
|
||||
.optionalStart()
|
||||
.appendLiteral('T')
|
||||
.append(HOUR_MINUTE_FORMATTER)
|
||||
.optionalStart()
|
||||
.appendValue(HOUR_OF_DAY, 1, 2, SignStyle.NOT_NEGATIVE)
|
||||
.optionalStart()
|
||||
.appendLiteral(':')
|
||||
.appendValue(MINUTE_OF_HOUR, 1, 2, SignStyle.NOT_NEGATIVE)
|
||||
.optionalStart()
|
||||
.appendLiteral(':')
|
||||
.appendValue(SECOND_OF_MINUTE, 1, 2, SignStyle.NOT_NEGATIVE)
|
||||
@ -733,12 +741,18 @@ public class DateFormatters {
|
||||
.optionalEnd()
|
||||
.optionalStart().appendZoneOrOffsetId().optionalEnd()
|
||||
.optionalEnd()
|
||||
.optionalEnd()
|
||||
.optionalEnd()
|
||||
.toFormatter(Locale.ROOT),
|
||||
new DateTimeFormatterBuilder()
|
||||
.append(DATE_FORMATTER)
|
||||
.optionalStart()
|
||||
.appendLiteral('T')
|
||||
.append(HOUR_MINUTE_FORMATTER)
|
||||
.optionalStart()
|
||||
.appendValue(HOUR_OF_DAY, 1, 2, SignStyle.NOT_NEGATIVE)
|
||||
.optionalStart()
|
||||
.appendLiteral(':')
|
||||
.appendValue(MINUTE_OF_HOUR, 1, 2, SignStyle.NOT_NEGATIVE)
|
||||
.optionalStart()
|
||||
.appendLiteral(':')
|
||||
.appendValue(SECOND_OF_MINUTE, 1, 2, SignStyle.NOT_NEGATIVE)
|
||||
@ -748,6 +762,8 @@ public class DateFormatters {
|
||||
.optionalEnd()
|
||||
.optionalStart().appendOffset("+HHmm", "Z").optionalEnd()
|
||||
.optionalEnd()
|
||||
.optionalEnd()
|
||||
.optionalEnd()
|
||||
.toFormatter(Locale.ROOT));
|
||||
|
||||
private static final DateTimeFormatter HOUR_MINUTE_SECOND_FORMATTER = new DateTimeFormatterBuilder()
|
||||
|
@ -0,0 +1,267 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.time;
|
||||
|
||||
import org.elasticsearch.ElasticsearchParseException;
|
||||
|
||||
import java.time.DateTimeException;
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.time.temporal.TemporalField;
|
||||
import java.time.temporal.TemporalQueries;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
/**
|
||||
* A parser for date/time formatted text with optional date math.
|
||||
*
|
||||
* The format of the datetime is configurable, and unix timestamps can also be used. Datemath
|
||||
* is appended to a datetime with the following syntax:
|
||||
* <code>||[+-/](\d+)?[yMwdhHms]</code>.
|
||||
*/
|
||||
public class DateMathParser {
|
||||
|
||||
// base fields which should be used for default parsing, when we round up
|
||||
private static final Map<TemporalField, Long> ROUND_UP_BASE_FIELDS = new HashMap<>(6);
|
||||
{
|
||||
ROUND_UP_BASE_FIELDS.put(ChronoField.MONTH_OF_YEAR, 1L);
|
||||
ROUND_UP_BASE_FIELDS.put(ChronoField.DAY_OF_MONTH, 1L);
|
||||
ROUND_UP_BASE_FIELDS.put(ChronoField.HOUR_OF_DAY, 23L);
|
||||
ROUND_UP_BASE_FIELDS.put(ChronoField.MINUTE_OF_HOUR, 59L);
|
||||
ROUND_UP_BASE_FIELDS.put(ChronoField.SECOND_OF_MINUTE, 59L);
|
||||
ROUND_UP_BASE_FIELDS.put(ChronoField.MILLI_OF_SECOND, 999L);
|
||||
}
|
||||
|
||||
private final CompoundDateTimeFormatter formatter;
|
||||
private final CompoundDateTimeFormatter roundUpFormatter;
|
||||
|
||||
public DateMathParser(CompoundDateTimeFormatter formatter) {
|
||||
Objects.requireNonNull(formatter);
|
||||
this.formatter = formatter;
|
||||
this.roundUpFormatter = formatter.parseDefaulting(ROUND_UP_BASE_FIELDS);
|
||||
}
|
||||
|
||||
public long parse(String text, LongSupplier now) {
|
||||
return parse(text, now, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse text, that potentially contains date math into the milliseconds since the epoch
|
||||
*
|
||||
* Examples are
|
||||
*
|
||||
* <code>2014-11-18||-2y</code> substracts two years from the input date
|
||||
* <code>now/m</code> rounds the current time to minute granularity
|
||||
*
|
||||
* Supported rounding units are
|
||||
* y year
|
||||
* M month
|
||||
* w week (beginning on a monday)
|
||||
* d day
|
||||
* h/H hour
|
||||
* m minute
|
||||
* s second
|
||||
*
|
||||
*
|
||||
* @param text the input
|
||||
* @param now a supplier to retrieve the current date in milliseconds, if needed for additions
|
||||
* @param roundUp should the result be rounded up
|
||||
* @param timeZone an optional timezone that should be applied before returning the milliseconds since the epoch
|
||||
* @return the parsed date in milliseconds since the epoch
|
||||
*/
|
||||
public long parse(String text, LongSupplier now, boolean roundUp, ZoneId timeZone) {
|
||||
long time;
|
||||
String mathString;
|
||||
if (text.startsWith("now")) {
|
||||
try {
|
||||
time = now.getAsLong();
|
||||
} catch (Exception e) {
|
||||
throw new ElasticsearchParseException("could not read the current timestamp", e);
|
||||
}
|
||||
mathString = text.substring("now".length());
|
||||
} else {
|
||||
int index = text.indexOf("||");
|
||||
if (index == -1) {
|
||||
return parseDateTime(text, timeZone, roundUp);
|
||||
}
|
||||
time = parseDateTime(text.substring(0, index), timeZone, false);
|
||||
mathString = text.substring(index + 2);
|
||||
}
|
||||
|
||||
return parseMath(mathString, time, roundUp, timeZone);
|
||||
}
|
||||
|
||||
private long parseMath(final String mathString, final long time, final boolean roundUp,
|
||||
ZoneId timeZone) throws ElasticsearchParseException {
|
||||
if (timeZone == null) {
|
||||
timeZone = ZoneOffset.UTC;
|
||||
}
|
||||
ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), timeZone);
|
||||
for (int i = 0; i < mathString.length(); ) {
|
||||
char c = mathString.charAt(i++);
|
||||
final boolean round;
|
||||
final int sign;
|
||||
if (c == '/') {
|
||||
round = true;
|
||||
sign = 1;
|
||||
} else {
|
||||
round = false;
|
||||
if (c == '+') {
|
||||
sign = 1;
|
||||
} else if (c == '-') {
|
||||
sign = -1;
|
||||
} else {
|
||||
throw new ElasticsearchParseException("operator not supported for date math [{}]", mathString);
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= mathString.length()) {
|
||||
throw new ElasticsearchParseException("truncated date math [{}]", mathString);
|
||||
}
|
||||
|
||||
final int num;
|
||||
if (!Character.isDigit(mathString.charAt(i))) {
|
||||
num = 1;
|
||||
} else {
|
||||
int numFrom = i;
|
||||
while (i < mathString.length() && Character.isDigit(mathString.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
if (i >= mathString.length()) {
|
||||
throw new ElasticsearchParseException("truncated date math [{}]", mathString);
|
||||
}
|
||||
num = Integer.parseInt(mathString.substring(numFrom, i));
|
||||
}
|
||||
if (round) {
|
||||
if (num != 1) {
|
||||
throw new ElasticsearchParseException("rounding `/` can only be used on single unit types [{}]", mathString);
|
||||
}
|
||||
}
|
||||
char unit = mathString.charAt(i++);
|
||||
switch (unit) {
|
||||
case 'y':
|
||||
if (round) {
|
||||
dateTime = dateTime.withDayOfYear(1).with(LocalTime.MIN);
|
||||
} else {
|
||||
dateTime = dateTime.plusYears(sign * num);
|
||||
}
|
||||
if (roundUp) {
|
||||
dateTime = dateTime.plusYears(1);
|
||||
}
|
||||
break;
|
||||
case 'M':
|
||||
if (round) {
|
||||
dateTime = dateTime.withDayOfMonth(1).with(LocalTime.MIN);
|
||||
} else {
|
||||
dateTime = dateTime.plusMonths(sign * num);
|
||||
}
|
||||
if (roundUp) {
|
||||
dateTime = dateTime.plusMonths(1);
|
||||
}
|
||||
break;
|
||||
case 'w':
|
||||
if (round) {
|
||||
dateTime = dateTime.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).with(LocalTime.MIN);
|
||||
} else {
|
||||
dateTime = dateTime.plusWeeks(sign * num);
|
||||
}
|
||||
if (roundUp) {
|
||||
dateTime = dateTime.plusWeeks(1);
|
||||
}
|
||||
break;
|
||||
case 'd':
|
||||
if (round) {
|
||||
dateTime = dateTime.with(LocalTime.MIN);
|
||||
} else {
|
||||
dateTime = dateTime.plusDays(sign * num);
|
||||
}
|
||||
if (roundUp) {
|
||||
dateTime = dateTime.plusDays(1);
|
||||
}
|
||||
break;
|
||||
case 'h':
|
||||
case 'H':
|
||||
if (round) {
|
||||
dateTime = dateTime.withMinute(0).withSecond(0).withNano(0);
|
||||
} else {
|
||||
dateTime = dateTime.plusHours(sign * num);
|
||||
}
|
||||
if (roundUp) {
|
||||
dateTime = dateTime.plusHours(1);
|
||||
}
|
||||
break;
|
||||
case 'm':
|
||||
if (round) {
|
||||
dateTime = dateTime.withSecond(0).withNano(0);
|
||||
} else {
|
||||
dateTime = dateTime.plusMinutes(sign * num);
|
||||
}
|
||||
if (roundUp) {
|
||||
dateTime = dateTime.plusMinutes(1);
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
if (round) {
|
||||
dateTime = dateTime.withNano(0);
|
||||
} else {
|
||||
dateTime = dateTime.plusSeconds(sign * num);
|
||||
}
|
||||
if (roundUp) {
|
||||
dateTime = dateTime.plusSeconds(1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ElasticsearchParseException("unit [{}] not supported for date math [{}]", unit, mathString);
|
||||
}
|
||||
if (roundUp) {
|
||||
dateTime = dateTime.minus(1, ChronoField.MILLI_OF_SECOND.getBaseUnit());
|
||||
}
|
||||
}
|
||||
return dateTime.toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
private long parseDateTime(String value, ZoneId timeZone, boolean roundUpIfNoTime) {
|
||||
CompoundDateTimeFormatter formatter = roundUpIfNoTime ? this.roundUpFormatter : this.formatter;
|
||||
try {
|
||||
if (timeZone == null) {
|
||||
return DateFormatters.toZonedDateTime(formatter.parse(value)).toInstant().toEpochMilli();
|
||||
} else {
|
||||
TemporalAccessor accessor = formatter.parse(value);
|
||||
ZoneId zoneId = TemporalQueries.zone().queryFrom(accessor);
|
||||
if (zoneId != null) {
|
||||
timeZone = zoneId;
|
||||
}
|
||||
|
||||
return DateFormatters.toZonedDateTime(accessor).withZoneSameLocal(timeZone).toInstant().toEpochMilli();
|
||||
}
|
||||
} catch (IllegalArgumentException | DateTimeException e) {
|
||||
throw new ElasticsearchParseException("failed to parse date field [{}]: [{}]", e, value, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -208,7 +208,13 @@ public class DateMathParserTests extends ESTestCase {
|
||||
assertDateMathEquals("2014-11-18||/M", "2014-10-31T23:00:00.000Z", 0, false, DateTimeZone.forID("CET"));
|
||||
assertDateMathEquals("2014-11-18||/M", "2014-11-30T22:59:59.999Z", 0, true, DateTimeZone.forID("CET"));
|
||||
|
||||
assertDateMathEquals("2014-11-17T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-19T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-20T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-21T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-22T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-23T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14||/w", "2014-11-23T23:59:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11-18||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18||/w", "2014-11-23T23:59:59.999", 0, true, null);
|
||||
|
@ -94,6 +94,7 @@ public class JavaJodaTimeDuellingTests extends ESTestCase {
|
||||
|
||||
assertSameDate("2018-12-31", "date");
|
||||
assertSameDate("18-5-6", "date");
|
||||
assertSameDate("10000-5-6", "date");
|
||||
|
||||
assertSameDate("2018-12-31T12", "date_hour");
|
||||
assertSameDate("2018-12-31T8", "date_hour");
|
||||
@ -109,7 +110,16 @@ public class JavaJodaTimeDuellingTests extends ESTestCase {
|
||||
assertSameDate("2018-12-31T12:12:12.1", "date_hour_minute_second_millis");
|
||||
assertSameDate("2018-12-31T12:12:12.1", "date_hour_minute_second_fraction");
|
||||
|
||||
assertSameDate("2018-12-31", "date_optional_time");
|
||||
assertSameDate("10000", "date_optional_time");
|
||||
assertSameDate("10000T", "date_optional_time");
|
||||
assertSameDate("2018", "date_optional_time");
|
||||
assertSameDate("2018T", "date_optional_time");
|
||||
assertSameDate("2018-05", "date_optional_time");
|
||||
assertSameDate("2018-05-30", "date_optional_time");
|
||||
assertSameDate("2018-05-30T20", "date_optional_time");
|
||||
assertSameDate("2018-05-30T20:21", "date_optional_time");
|
||||
assertSameDate("2018-05-30T20:21:23", "date_optional_time");
|
||||
assertSameDate("2018-05-30T20:21:23.123", "date_optional_time");
|
||||
assertSameDate("2018-12-1", "date_optional_time");
|
||||
assertSameDate("2018-12-31T10:15:30", "date_optional_time");
|
||||
assertSameDate("2018-12-31T10:15:3", "date_optional_time");
|
||||
@ -230,6 +240,7 @@ public class JavaJodaTimeDuellingTests extends ESTestCase {
|
||||
assertParseException("2018W313T81212Z", "strict_basic_week_date_time_no_millis");
|
||||
assertParseException("2018W313T12812Z", "strict_basic_week_date_time_no_millis");
|
||||
assertSameDate("2018-12-31", "strict_date");
|
||||
assertParseException("10000-12-31", "strict_date");
|
||||
assertParseException("2018-8-31", "strict_date");
|
||||
assertSameDate("2018-12-31T12", "strict_date_hour");
|
||||
assertParseException("2018-12-31T8", "strict_date_hour");
|
||||
@ -246,6 +257,7 @@ public class JavaJodaTimeDuellingTests extends ESTestCase {
|
||||
assertSameDate("2018-12-31", "strict_date_optional_time");
|
||||
assertParseException("2018-12-1", "strict_date_optional_time");
|
||||
assertParseException("2018-1-31", "strict_date_optional_time");
|
||||
assertParseException("10000-01-31", "strict_date_optional_time");
|
||||
assertSameDate("2018-12-31T10:15:30", "strict_date_optional_time");
|
||||
assertParseException("2018-12-31T10:15:3", "strict_date_optional_time");
|
||||
assertParseException("2018-12-31T10:5:30", "strict_date_optional_time");
|
||||
|
@ -0,0 +1,309 @@
|
||||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.common.time;
|
||||
|
||||
import org.elasticsearch.ElasticsearchParseException;
|
||||
import org.elasticsearch.ExceptionsHelper;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
public class DateMathParserTests extends ESTestCase {
|
||||
|
||||
private final CompoundDateTimeFormatter formatter = DateFormatters.forPattern("dateOptionalTime||epoch_millis");
|
||||
private final DateMathParser parser = new DateMathParser(formatter);
|
||||
|
||||
public void testBasicDates() {
|
||||
assertDateMathEquals("2014", "2014-01-01T00:00:00.000");
|
||||
assertDateMathEquals("2014-05", "2014-05-01T00:00:00.000");
|
||||
assertDateMathEquals("2014-05-30", "2014-05-30T00:00:00.000");
|
||||
assertDateMathEquals("2014-05-30T20", "2014-05-30T20:00:00.000");
|
||||
assertDateMathEquals("2014-05-30T20:21", "2014-05-30T20:21:00.000");
|
||||
assertDateMathEquals("2014-05-30T20:21:35", "2014-05-30T20:21:35.000");
|
||||
assertDateMathEquals("2014-05-30T20:21:35.123", "2014-05-30T20:21:35.123");
|
||||
}
|
||||
|
||||
public void testRoundingDoesNotAffectExactDate() {
|
||||
assertDateMathEquals("2014-11-12T22:55:00.000Z", "2014-11-12T22:55:00.000Z", 0, true, null);
|
||||
assertDateMathEquals("2014-11-12T22:55:00.000Z", "2014-11-12T22:55:00.000Z", 0, false, null);
|
||||
|
||||
assertDateMathEquals("2014-11-12T22:55:00.000", "2014-11-12T21:55:00.000Z", 0, true, ZoneId.of("+01:00"));
|
||||
assertDateMathEquals("2014-11-12T22:55:00.000", "2014-11-12T21:55:00.000Z", 0, false, ZoneId.of("+01:00"));
|
||||
|
||||
assertDateMathEquals("2014-11-12T22:55:00.000+01:00", "2014-11-12T21:55:00.000Z", 0, true, null);
|
||||
assertDateMathEquals("2014-11-12T22:55:00.000+01:00", "2014-11-12T21:55:00.000Z", 0, false, null);
|
||||
}
|
||||
|
||||
public void testTimezone() {
|
||||
// timezone works within date format
|
||||
assertDateMathEquals("2014-05-30T20:21+02:00", "2014-05-30T18:21:00.000");
|
||||
|
||||
// test alternative ways of writing zero offsets, according to ISO 8601 +00:00, +00, +0000 should work.
|
||||
// joda also seems to allow for -00:00, -00, -0000
|
||||
assertDateMathEquals("2014-05-30T18:21+00:00", "2014-05-30T18:21:00.000");
|
||||
assertDateMathEquals("2014-05-30T18:21+00", "2014-05-30T18:21:00.000");
|
||||
assertDateMathEquals("2014-05-30T18:21+0000", "2014-05-30T18:21:00.000");
|
||||
assertDateMathEquals("2014-05-30T18:21-00:00", "2014-05-30T18:21:00.000");
|
||||
assertDateMathEquals("2014-05-30T18:21-00", "2014-05-30T18:21:00.000");
|
||||
assertDateMathEquals("2014-05-30T18:21-0000", "2014-05-30T18:21:00.000");
|
||||
|
||||
// but also externally
|
||||
assertDateMathEquals("2014-05-30T20:21", "2014-05-30T18:21:00.000", 0, false, ZoneId.of("+02:00"));
|
||||
assertDateMathEquals("2014-05-30T18:21", "2014-05-30T18:21:00.000", 0, false, ZoneId.of("+00:00"));
|
||||
assertDateMathEquals("2014-05-30T18:21", "2014-05-30T18:21:00.000", 0, false, ZoneId.of("+00:00"));
|
||||
assertDateMathEquals("2014-05-30T18:21", "2014-05-30T18:21:00.000", 0, false, ZoneId.of("+00"));
|
||||
assertDateMathEquals("2014-05-30T18:21", "2014-05-30T18:21:00.000", 0, false, ZoneId.of("+0000"));
|
||||
assertDateMathEquals("2014-05-30T18:21", "2014-05-30T18:21:00.000", 0, false, ZoneId.of("-00:00"));
|
||||
assertDateMathEquals("2014-05-30T18:21", "2014-05-30T18:21:00.000", 0, false, ZoneId.of("-00"));
|
||||
assertDateMathEquals("2014-05-30T18:21", "2014-05-30T18:21:00.000", 0, false, ZoneId.of("-0000"));
|
||||
|
||||
// and timezone in the date has priority
|
||||
assertDateMathEquals("2014-05-30T20:21+03:00", "2014-05-30T17:21:00.000", 0, false, ZoneId.of("-08:00"));
|
||||
assertDateMathEquals("2014-05-30T20:21Z", "2014-05-30T20:21:00.000", 0, false, ZoneId.of("-08:00"));
|
||||
}
|
||||
|
||||
public void testBasicMath() {
|
||||
assertDateMathEquals("2014-11-18||+y", "2015-11-18");
|
||||
assertDateMathEquals("2014-11-18||-2y", "2012-11-18");
|
||||
|
||||
assertDateMathEquals("2014-11-18||+3M", "2015-02-18");
|
||||
assertDateMathEquals("2014-11-18||-M", "2014-10-18");
|
||||
|
||||
assertDateMathEquals("2014-11-18||+1w", "2014-11-25");
|
||||
assertDateMathEquals("2014-11-18||-3w", "2014-10-28");
|
||||
|
||||
assertDateMathEquals("2014-11-18||+22d", "2014-12-10");
|
||||
assertDateMathEquals("2014-11-18||-423d", "2013-09-21");
|
||||
|
||||
assertDateMathEquals("2014-11-18T14||+13h", "2014-11-19T03");
|
||||
assertDateMathEquals("2014-11-18T14||-1h", "2014-11-18T13");
|
||||
assertDateMathEquals("2014-11-18T14||+13H", "2014-11-19T03");
|
||||
assertDateMathEquals("2014-11-18T14||-1H", "2014-11-18T13");
|
||||
|
||||
assertDateMathEquals("2014-11-18T14:27||+10240m", "2014-11-25T17:07");
|
||||
assertDateMathEquals("2014-11-18T14:27||-10m", "2014-11-18T14:17");
|
||||
|
||||
assertDateMathEquals("2014-11-18T14:27:32||+60s", "2014-11-18T14:28:32");
|
||||
assertDateMathEquals("2014-11-18T14:27:32||-3600s", "2014-11-18T13:27:32");
|
||||
}
|
||||
|
||||
public void testLenientEmptyMath() {
|
||||
assertDateMathEquals("2014-05-30T20:21||", "2014-05-30T20:21:00.000");
|
||||
}
|
||||
|
||||
public void testMultipleAdjustments() {
|
||||
assertDateMathEquals("2014-11-18||+1M-1M", "2014-11-18");
|
||||
assertDateMathEquals("2014-11-18||+1M-1m", "2014-12-17T23:59");
|
||||
assertDateMathEquals("2014-11-18||-1m+1M", "2014-12-17T23:59");
|
||||
assertDateMathEquals("2014-11-18||+1M/M", "2014-12-01");
|
||||
assertDateMathEquals("2014-11-18||+1M/M+1h", "2014-12-01T01");
|
||||
}
|
||||
|
||||
public void testNow() {
|
||||
final long now = parser.parse("2014-11-18T14:27:32", () -> 0, false, null);
|
||||
|
||||
assertDateMathEquals("now", "2014-11-18T14:27:32", now, false, null);
|
||||
assertDateMathEquals("now+M", "2014-12-18T14:27:32", now, false, null);
|
||||
assertDateMathEquals("now-2d", "2014-11-16T14:27:32", now, false, null);
|
||||
assertDateMathEquals("now/m", "2014-11-18T14:27", now, false, null);
|
||||
|
||||
// timezone does not affect now
|
||||
assertDateMathEquals("now/m", "2014-11-18T14:27", now, false, ZoneId.of("+02:00"));
|
||||
}
|
||||
|
||||
public void testRoundingPreservesEpochAsBaseDate() {
|
||||
// If a user only specifies times, then the date needs to always be 1970-01-01 regardless of rounding
|
||||
CompoundDateTimeFormatter formatter = DateFormatters.forPattern("HH:mm:ss");
|
||||
DateMathParser parser = new DateMathParser(formatter);
|
||||
ZonedDateTime zonedDateTime = DateFormatters.toZonedDateTime(formatter.parse("04:52:20"));
|
||||
assertThat(zonedDateTime.getYear(), is(1970));
|
||||
long millisStart = zonedDateTime.toInstant().toEpochMilli();
|
||||
assertEquals(millisStart, parser.parse("04:52:20", () -> 0, false, null));
|
||||
// due to rounding up, we have to add the number of milliseconds here manually
|
||||
long millisEnd = DateFormatters.toZonedDateTime(formatter.parse("04:52:20")).toInstant().toEpochMilli() + 999;
|
||||
assertEquals(millisEnd, parser.parse("04:52:20", () -> 0, true, null));
|
||||
}
|
||||
|
||||
// Implicit rounding happening when parts of the date are not specified
|
||||
public void testImplicitRounding() {
|
||||
assertDateMathEquals("2014-11-18", "2014-11-18", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18", "2014-11-18T23:59:59.999Z", 0, true, null);
|
||||
|
||||
assertDateMathEquals("2014-11-18T09:20", "2014-11-18T09:20", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T09:20", "2014-11-18T09:20:59.999Z", 0, true, null);
|
||||
|
||||
assertDateMathEquals("2014-11-18", "2014-11-17T23:00:00.000Z", 0, false, ZoneId.of("CET"));
|
||||
assertDateMathEquals("2014-11-18", "2014-11-18T22:59:59.999Z", 0, true, ZoneId.of("CET"));
|
||||
|
||||
assertDateMathEquals("2014-11-18T09:20", "2014-11-18T08:20:00.000Z", 0, false, ZoneId.of("CET"));
|
||||
assertDateMathEquals("2014-11-18T09:20", "2014-11-18T08:20:59.999Z", 0, true, ZoneId.of("CET"));
|
||||
|
||||
// implicit rounding with explicit timezone in the date format
|
||||
CompoundDateTimeFormatter formatter = DateFormatters.forPattern("yyyy-MM-ddXXX");
|
||||
DateMathParser parser = new DateMathParser(formatter);
|
||||
long time = parser.parse("2011-10-09+01:00", () -> 0, false, null);
|
||||
assertEquals(this.parser.parse("2011-10-09T00:00:00.000+01:00", () -> 0), time);
|
||||
time = parser.parse("2011-10-09+01:00", () -> 0, true, null);
|
||||
assertEquals(this.parser.parse("2011-10-09T23:59:59.999+01:00", () -> 0), time);
|
||||
}
|
||||
|
||||
// Explicit rounding using the || separator
|
||||
public void testExplicitRounding() {
|
||||
assertDateMathEquals("2014-11-18||/y", "2014-01-01", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18||/y", "2014-12-31T23:59:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014||/y", "2014-01-01", 0, false, null);
|
||||
assertDateMathEquals("2014-01-01T00:00:00.001||/y", "2014-12-31T23:59:59.999", 0, true, null);
|
||||
// rounding should also take into account time zone
|
||||
assertDateMathEquals("2014-11-18||/y", "2013-12-31T23:00:00.000Z", 0, false, ZoneId.of("CET"));
|
||||
assertDateMathEquals("2014-11-18||/y", "2014-12-31T22:59:59.999Z", 0, true, ZoneId.of("CET"));
|
||||
|
||||
assertDateMathEquals("2014-11-18||/M", "2014-11-01", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18||/M", "2014-11-30T23:59:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11||/M", "2014-11-01", 0, false, null);
|
||||
assertDateMathEquals("2014-11||/M", "2014-11-30T23:59:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11-18||/M", "2014-10-31T23:00:00.000Z", 0, false, ZoneId.of("CET"));
|
||||
assertDateMathEquals("2014-11-18||/M", "2014-11-30T22:59:59.999Z", 0, true, ZoneId.of("CET"));
|
||||
|
||||
assertDateMathEquals("2014-11-18T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14||/w", "2014-11-23T23:59:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11-17T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-19T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-20T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-21T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-22T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-23T14||/w", "2014-11-17", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18||/w", "2014-11-23T23:59:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11-18||/w", "2014-11-16T23:00:00.000Z", 0, false, ZoneId.of("+01:00"));
|
||||
assertDateMathEquals("2014-11-18||/w", "2014-11-17T01:00:00.000Z", 0, false, ZoneId.of("-01:00"));
|
||||
assertDateMathEquals("2014-11-18||/w", "2014-11-16T23:00:00.000Z", 0, false, ZoneId.of("CET"));
|
||||
assertDateMathEquals("2014-11-18||/w", "2014-11-23T22:59:59.999Z", 0, true, ZoneId.of("CET"));
|
||||
assertDateMathEquals("2014-07-22||/w", "2014-07-20T22:00:00.000Z", 0, false, ZoneId.of("CET")); // with DST
|
||||
|
||||
assertDateMathEquals("2014-11-18T14||/d", "2014-11-18", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14||/d", "2014-11-18T23:59:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11-18||/d", "2014-11-18", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18||/d", "2014-11-18T23:59:59.999", 0, true, null);
|
||||
|
||||
assertDateMathEquals("2014-11-18T14:27||/h", "2014-11-18T14", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14:27||/h", "2014-11-18T14:59:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11-18T14||/H", "2014-11-18T14", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14||/H", "2014-11-18T14:59:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11-18T14:27||/h", "2014-11-18T14", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14:27||/h", "2014-11-18T14:59:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11-18T14||/H", "2014-11-18T14", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14||/H", "2014-11-18T14:59:59.999", 0, true, null);
|
||||
|
||||
assertDateMathEquals("2014-11-18T14:27:32||/m", "2014-11-18T14:27", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14:27:32||/m", "2014-11-18T14:27:59.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11-18T14:27||/m", "2014-11-18T14:27", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14:27||/m", "2014-11-18T14:27:59.999", 0, true, null);
|
||||
|
||||
assertDateMathEquals("2014-11-18T14:27:32.123||/s", "2014-11-18T14:27:32", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14:27:32.123||/s", "2014-11-18T14:27:32.999", 0, true, null);
|
||||
assertDateMathEquals("2014-11-18T14:27:32||/s", "2014-11-18T14:27:32", 0, false, null);
|
||||
assertDateMathEquals("2014-11-18T14:27:32||/s", "2014-11-18T14:27:32.999", 0, true, null);
|
||||
}
|
||||
|
||||
public void testTimestamps() {
|
||||
assertDateMathEquals("1418248078000", "2014-12-10T21:47:58.000");
|
||||
assertDateMathEquals("32484216259000", "2999-05-20T17:24:19.000");
|
||||
assertDateMathEquals("253382837059000", "9999-05-20T17:24:19.000");
|
||||
|
||||
// datemath still works on timestamps
|
||||
assertDateMathEquals("1418248078000||/m", "2014-12-10T21:47:00.000");
|
||||
|
||||
// also check other time units
|
||||
DateMathParser parser = new DateMathParser(DateFormatters.forPattern("epoch_second||dateOptionalTime"));
|
||||
long datetime = parser.parse("1418248078", () -> 0);
|
||||
assertDateEquals(datetime, "1418248078", "2014-12-10T21:47:58.000");
|
||||
|
||||
// a timestamp before 10000 is a year
|
||||
assertDateMathEquals("9999", "9999-01-01T00:00:00.000");
|
||||
// 10000 is also a year, breaking bwc, used to be a timestamp
|
||||
assertDateMathEquals("10000", "10000-01-01T00:00:00.000");
|
||||
// but 10000 with T is still a date format
|
||||
assertDateMathEquals("10000T", "10000-01-01T00:00:00.000");
|
||||
}
|
||||
|
||||
void assertParseException(String msg, String date, String exc) {
|
||||
try {
|
||||
parser.parse(date, () -> 0);
|
||||
fail("Date: " + date + "\n" + msg);
|
||||
} catch (ElasticsearchParseException e) {
|
||||
assertThat(ExceptionsHelper.detailedMessage(e), containsString(exc));
|
||||
}
|
||||
}
|
||||
|
||||
public void testIllegalMathFormat() {
|
||||
assertParseException("Expected date math unsupported operator exception", "2014-11-18||*5", "operator not supported");
|
||||
assertParseException("Expected date math incompatible rounding exception", "2014-11-18||/2m", "rounding");
|
||||
assertParseException("Expected date math illegal unit type exception", "2014-11-18||+2a", "unit [a] not supported");
|
||||
assertParseException("Expected date math truncation exception", "2014-11-18||+12", "truncated");
|
||||
assertParseException("Expected date math truncation exception", "2014-11-18||-", "truncated");
|
||||
}
|
||||
|
||||
public void testIllegalDateFormat() {
|
||||
assertParseException("Expected bad timestamp exception", Long.toString(Long.MAX_VALUE) + "0", "failed to parse date field");
|
||||
assertParseException("Expected bad date format exception", "123bogus", "could not be parsed");
|
||||
}
|
||||
|
||||
public void testOnlyCallsNowIfNecessary() {
|
||||
final AtomicBoolean called = new AtomicBoolean();
|
||||
final LongSupplier now = () -> {
|
||||
called.set(true);
|
||||
return 42L;
|
||||
};
|
||||
parser.parse("2014-11-18T14:27:32", now, false, null);
|
||||
assertFalse(called.get());
|
||||
parser.parse("now/d", now, false, null);
|
||||
assertTrue(called.get());
|
||||
}
|
||||
|
||||
private void assertDateMathEquals(String toTest, String expected) {
|
||||
assertDateMathEquals(toTest, expected, 0, false, null);
|
||||
}
|
||||
|
||||
private void assertDateMathEquals(String toTest, String expected, final long now, boolean roundUp, ZoneId timeZone) {
|
||||
long gotMillis = parser.parse(toTest, () -> now, roundUp, timeZone);
|
||||
assertDateEquals(gotMillis, toTest, expected);
|
||||
}
|
||||
|
||||
private void assertDateEquals(long gotMillis, String original, String expected) {
|
||||
long expectedMillis = parser.parse(expected, () -> 0);
|
||||
if (gotMillis != expectedMillis) {
|
||||
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(gotMillis), ZoneOffset.UTC);
|
||||
fail("Date math not equal\n" +
|
||||
"Original : " + original + "\n" +
|
||||
"Parsed : " + formatter.format(zonedDateTime) + "\n" +
|
||||
"Expected : " + expected + "\n" +
|
||||
"Expected milliseconds : " + expectedMillis + "\n" +
|
||||
"Actual milliseconds : " + gotMillis + "\n");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user