Skip to content

Commit

Permalink
Core: Abstract DateMathParser in an interface (#33905)
Browse files Browse the repository at this point in the history
This commits creates a DateMathParser interface, which is already
implemented for both joda and java time. While currently the java time
DateMathParser is not used, this change will allow a followup which will
create a DateMathParser from a DateFormatter, so the caller does not
need to know the internals of the DateFormatter they have.
  • Loading branch information
rjernst authored Sep 26, 2018
1 parent ff2bbdf commit 7800b4f
Show file tree
Hide file tree
Showing 26 changed files with 476 additions and 293 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.joda.DateMathParser;
import org.elasticsearch.common.joda.FormatDateTimeFormatter;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.time.DateMathParser;
import org.elasticsearch.common.time.DateUtils;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.IndexNotFoundException;
Expand Down Expand Up @@ -923,8 +924,9 @@ String resolveExpression(String expression, final Context context) {
}
DateTimeFormatter parser = dateFormatter.withZone(timeZone);
FormatDateTimeFormatter formatter = new FormatDateTimeFormatter(dateFormatterPattern, parser, Locale.ROOT);
DateMathParser dateMathParser = new DateMathParser(formatter);
long millis = dateMathParser.parse(mathExpression, context::getStartTime, false, timeZone);
DateMathParser dateMathParser = formatter.toDateMathParser();
long millis = dateMathParser.parse(mathExpression, context::getStartTime, false,
DateUtils.dateTimeZoneToZoneId(timeZone));

String time = formatter.printer().print(millis);
beforePlaceHolderSb.append(time);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.elasticsearch.common.joda;

import org.elasticsearch.common.time.DateMathParser;
import org.joda.time.format.DateTimeFormatter;

import java.util.Locale;
Expand Down Expand Up @@ -64,4 +65,8 @@ public DateTimeFormatter printer() {
public Locale locale() {
return locale;
}

public DateMathParser toDateMathParser() {
return new JodaDateMathParser(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
package org.elasticsearch.common.joda;

import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.time.DateMathParser;
import org.elasticsearch.common.time.DateUtils;
import org.joda.time.DateTimeZone;
import org.joda.time.MutableDateTime;
import org.joda.time.format.DateTimeFormatter;

import java.time.ZoneId;
import java.util.Objects;
import java.util.function.LongSupplier;

Expand All @@ -34,23 +37,21 @@
* is appended to a datetime with the following syntax:
* <code>||[+-/](\d+)?[yMwdhHms]</code>.
*/
public class DateMathParser {
public class JodaDateMathParser implements DateMathParser {

private final FormatDateTimeFormatter dateTimeFormatter;

public DateMathParser(FormatDateTimeFormatter dateTimeFormatter) {
public JodaDateMathParser(FormatDateTimeFormatter dateTimeFormatter) {
Objects.requireNonNull(dateTimeFormatter);
this.dateTimeFormatter = dateTimeFormatter;
}

public long parse(String text, LongSupplier now) {
return parse(text, now, false, null);
}

// Note: we take a callable here for the timestamp in order to be able to figure out
// if it has been used. For instance, the request cache does not cache requests that make
// use of `now`.
public long parse(String text, LongSupplier now, boolean roundUp, DateTimeZone timeZone) {
@Override
public long parse(String text, LongSupplier now, boolean roundUp, ZoneId tz) {
final DateTimeZone timeZone = tz == null ? null : DateUtils.zoneIdToDateTimeZone(tz);
long time;
String mathString;
if (text.startsWith("now")) {
Expand Down
227 changes: 17 additions & 210 deletions server/src/main/java/org/elasticsearch/common/time/DateMathParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,56 +19,31 @@

package org.elasticsearch.common.time;

import org.elasticsearch.ElasticsearchParseException;
import org.joda.time.DateTimeZone;

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>.
* An abstraction over date math parsing to allow different implementation for joda and java time.
*/
public class DateMathParser {
public interface 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);
/**
* Parse a date math expression without timzeone info and rounding down.
*/
default long parse(String text, LongSupplier now) {
return parse(text, now, false, (ZoneId) null);
}

private final DateFormatter formatter;
private final DateFormatter roundUpFormatter;
// Note: we take a callable here for the timestamp in order to be able to figure out
// if it has been used. For instance, the request cache does not cache requests that make
// use of `now`.

public DateMathParser(DateFormatter 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);
// exists for backcompat, do not use!
@Deprecated
default long parse(String text, LongSupplier now, boolean roundUp, DateTimeZone tz) {
return parse(text, now, roundUp, tz == null ? null : ZoneId.of(tz.getID()));
}

/**
Expand All @@ -92,176 +67,8 @@ public long parse(String text, LongSupplier now) {
* @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
* @param tz 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) {
DateFormatter 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());
}
}
long parse(String text, LongSupplier now, boolean roundUp, ZoneId tz);
}
Loading

0 comments on commit 7800b4f

Please sign in to comment.