Skip to content

Commit ad5b03d

Browse files
committed
Core: Abstract DateMathParser in an interface (elastic#33905)
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.
1 parent 8a9a815 commit ad5b03d

File tree

27 files changed

+480
-297
lines changed

27 files changed

+480
-297
lines changed

modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/RangeFieldQueryStringQueryBuilderTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
3232
import org.elasticsearch.common.Strings;
3333
import org.elasticsearch.common.compress.CompressedXContent;
34-
import org.elasticsearch.common.joda.DateMathParser;
3534
import org.elasticsearch.common.network.InetAddresses;
35+
import org.elasticsearch.common.time.DateMathParser;
3636
import org.elasticsearch.index.query.QueryShardContext;
3737
import org.elasticsearch.index.query.QueryStringQueryBuilder;
3838
import org.elasticsearch.plugins.Plugin;

server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727
import org.elasticsearch.common.Strings;
2828
import org.elasticsearch.common.collect.Tuple;
2929
import org.elasticsearch.common.component.AbstractComponent;
30-
import org.elasticsearch.common.joda.DateMathParser;
3130
import org.elasticsearch.common.joda.FormatDateTimeFormatter;
3231
import org.elasticsearch.common.regex.Regex;
3332
import org.elasticsearch.common.settings.Settings;
33+
import org.elasticsearch.common.time.DateMathParser;
34+
import org.elasticsearch.common.time.DateUtils;
3435
import org.elasticsearch.common.util.set.Sets;
3536
import org.elasticsearch.index.Index;
3637
import org.elasticsearch.index.IndexNotFoundException;
@@ -923,8 +924,9 @@ String resolveExpression(String expression, final Context context) {
923924
}
924925
DateTimeFormatter parser = dateFormatter.withZone(timeZone);
925926
FormatDateTimeFormatter formatter = new FormatDateTimeFormatter(dateFormatterPattern, parser, Locale.ROOT);
926-
DateMathParser dateMathParser = new DateMathParser(formatter);
927-
long millis = dateMathParser.parse(mathExpression, context::getStartTime, false, timeZone);
927+
DateMathParser dateMathParser = formatter.toDateMathParser();
928+
long millis = dateMathParser.parse(mathExpression, context::getStartTime, false,
929+
DateUtils.dateTimeZoneToZoneId(timeZone));
928930

929931
String time = formatter.printer().print(millis);
930932
beforePlaceHolderSb.append(time);

server/src/main/java/org/elasticsearch/common/joda/FormatDateTimeFormatter.java

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.common.joda;
2121

22+
import org.elasticsearch.common.time.DateMathParser;
2223
import org.joda.time.format.DateTimeFormatter;
2324

2425
import java.util.Locale;
@@ -64,4 +65,8 @@ public DateTimeFormatter printer() {
6465
public Locale locale() {
6566
return locale;
6667
}
68+
69+
public DateMathParser toDateMathParser() {
70+
return new JodaDateMathParser(this);
71+
}
6772
}

server/src/main/java/org/elasticsearch/common/joda/DateMathParser.java renamed to server/src/main/java/org/elasticsearch/common/joda/JodaDateMathParser.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
package org.elasticsearch.common.joda;
2121

2222
import org.elasticsearch.ElasticsearchParseException;
23+
import org.elasticsearch.common.time.DateMathParser;
24+
import org.elasticsearch.common.time.DateUtils;
2325
import org.joda.time.DateTimeZone;
2426
import org.joda.time.MutableDateTime;
2527
import org.joda.time.format.DateTimeFormatter;
2628

29+
import java.time.ZoneId;
2730
import java.util.Objects;
2831
import java.util.function.LongSupplier;
2932

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

3942
private final FormatDateTimeFormatter dateTimeFormatter;
4043

41-
public DateMathParser(FormatDateTimeFormatter dateTimeFormatter) {
44+
public JodaDateMathParser(FormatDateTimeFormatter dateTimeFormatter) {
4245
Objects.requireNonNull(dateTimeFormatter);
4346
this.dateTimeFormatter = dateTimeFormatter;
4447
}
4548

46-
public long parse(String text, LongSupplier now) {
47-
return parse(text, now, false, null);
48-
}
49-
5049
// Note: we take a callable here for the timestamp in order to be able to figure out
5150
// if it has been used. For instance, the request cache does not cache requests that make
5251
// use of `now`.
53-
public long parse(String text, LongSupplier now, boolean roundUp, DateTimeZone timeZone) {
52+
@Override
53+
public long parse(String text, LongSupplier now, boolean roundUp, ZoneId tz) {
54+
final DateTimeZone timeZone = tz == null ? null : DateUtils.zoneIdToDateTimeZone(tz);
5455
long time;
5556
String mathString;
5657
if (text.startsWith("now")) {

server/src/main/java/org/elasticsearch/common/time/DateMathParser.java

+17-210
Original file line numberDiff line numberDiff line change
@@ -19,56 +19,31 @@
1919

2020
package org.elasticsearch.common.time;
2121

22-
import org.elasticsearch.ElasticsearchParseException;
22+
import org.joda.time.DateTimeZone;
2323

24-
import java.time.DateTimeException;
25-
import java.time.DayOfWeek;
26-
import java.time.Instant;
27-
import java.time.LocalTime;
2824
import java.time.ZoneId;
29-
import java.time.ZoneOffset;
30-
import java.time.ZonedDateTime;
31-
import java.time.temporal.ChronoField;
32-
import java.time.temporal.TemporalAccessor;
33-
import java.time.temporal.TemporalAdjusters;
34-
import java.time.temporal.TemporalField;
35-
import java.time.temporal.TemporalQueries;
36-
import java.util.HashMap;
37-
import java.util.Map;
38-
import java.util.Objects;
3925
import java.util.function.LongSupplier;
4026

4127
/**
42-
* A parser for date/time formatted text with optional date math.
43-
*
44-
* The format of the datetime is configurable, and unix timestamps can also be used. Datemath
45-
* is appended to a datetime with the following syntax:
46-
* <code>||[+-/](\d+)?[yMwdhHms]</code>.
28+
* An abstraction over date math parsing to allow different implementation for joda and java time.
4729
*/
48-
public class DateMathParser {
30+
public interface DateMathParser {
4931

50-
// base fields which should be used for default parsing, when we round up
51-
private static final Map<TemporalField, Long> ROUND_UP_BASE_FIELDS = new HashMap<>(6);
52-
{
53-
ROUND_UP_BASE_FIELDS.put(ChronoField.MONTH_OF_YEAR, 1L);
54-
ROUND_UP_BASE_FIELDS.put(ChronoField.DAY_OF_MONTH, 1L);
55-
ROUND_UP_BASE_FIELDS.put(ChronoField.HOUR_OF_DAY, 23L);
56-
ROUND_UP_BASE_FIELDS.put(ChronoField.MINUTE_OF_HOUR, 59L);
57-
ROUND_UP_BASE_FIELDS.put(ChronoField.SECOND_OF_MINUTE, 59L);
58-
ROUND_UP_BASE_FIELDS.put(ChronoField.MILLI_OF_SECOND, 999L);
32+
/**
33+
* Parse a date math expression without timzeone info and rounding down.
34+
*/
35+
default long parse(String text, LongSupplier now) {
36+
return parse(text, now, false, (ZoneId) null);
5937
}
6038

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

64-
public DateMathParser(CompoundDateTimeFormatter formatter) {
65-
Objects.requireNonNull(formatter);
66-
this.formatter = formatter;
67-
this.roundUpFormatter = formatter.parseDefaulting(ROUND_UP_BASE_FIELDS);
68-
}
69-
70-
public long parse(String text, LongSupplier now) {
71-
return parse(text, now, false, null);
43+
// exists for backcompat, do not use!
44+
@Deprecated
45+
default long parse(String text, LongSupplier now, boolean roundUp, DateTimeZone tz) {
46+
return parse(text, now, roundUp, tz == null ? null : ZoneId.of(tz.getID()));
7247
}
7348

7449
/**
@@ -92,176 +67,8 @@ public long parse(String text, LongSupplier now) {
9267
* @param text the input
9368
* @param now a supplier to retrieve the current date in milliseconds, if needed for additions
9469
* @param roundUp should the result be rounded up
95-
* @param timeZone an optional timezone that should be applied before returning the milliseconds since the epoch
70+
* @param tz an optional timezone that should be applied before returning the milliseconds since the epoch
9671
* @return the parsed date in milliseconds since the epoch
9772
*/
98-
public long parse(String text, LongSupplier now, boolean roundUp, ZoneId timeZone) {
99-
long time;
100-
String mathString;
101-
if (text.startsWith("now")) {
102-
try {
103-
time = now.getAsLong();
104-
} catch (Exception e) {
105-
throw new ElasticsearchParseException("could not read the current timestamp", e);
106-
}
107-
mathString = text.substring("now".length());
108-
} else {
109-
int index = text.indexOf("||");
110-
if (index == -1) {
111-
return parseDateTime(text, timeZone, roundUp);
112-
}
113-
time = parseDateTime(text.substring(0, index), timeZone, false);
114-
mathString = text.substring(index + 2);
115-
}
116-
117-
return parseMath(mathString, time, roundUp, timeZone);
118-
}
119-
120-
private long parseMath(final String mathString, final long time, final boolean roundUp,
121-
ZoneId timeZone) throws ElasticsearchParseException {
122-
if (timeZone == null) {
123-
timeZone = ZoneOffset.UTC;
124-
}
125-
ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), timeZone);
126-
for (int i = 0; i < mathString.length(); ) {
127-
char c = mathString.charAt(i++);
128-
final boolean round;
129-
final int sign;
130-
if (c == '/') {
131-
round = true;
132-
sign = 1;
133-
} else {
134-
round = false;
135-
if (c == '+') {
136-
sign = 1;
137-
} else if (c == '-') {
138-
sign = -1;
139-
} else {
140-
throw new ElasticsearchParseException("operator not supported for date math [{}]", mathString);
141-
}
142-
}
143-
144-
if (i >= mathString.length()) {
145-
throw new ElasticsearchParseException("truncated date math [{}]", mathString);
146-
}
147-
148-
final int num;
149-
if (!Character.isDigit(mathString.charAt(i))) {
150-
num = 1;
151-
} else {
152-
int numFrom = i;
153-
while (i < mathString.length() && Character.isDigit(mathString.charAt(i))) {
154-
i++;
155-
}
156-
if (i >= mathString.length()) {
157-
throw new ElasticsearchParseException("truncated date math [{}]", mathString);
158-
}
159-
num = Integer.parseInt(mathString.substring(numFrom, i));
160-
}
161-
if (round) {
162-
if (num != 1) {
163-
throw new ElasticsearchParseException("rounding `/` can only be used on single unit types [{}]", mathString);
164-
}
165-
}
166-
char unit = mathString.charAt(i++);
167-
switch (unit) {
168-
case 'y':
169-
if (round) {
170-
dateTime = dateTime.withDayOfYear(1).with(LocalTime.MIN);
171-
} else {
172-
dateTime = dateTime.plusYears(sign * num);
173-
}
174-
if (roundUp) {
175-
dateTime = dateTime.plusYears(1);
176-
}
177-
break;
178-
case 'M':
179-
if (round) {
180-
dateTime = dateTime.withDayOfMonth(1).with(LocalTime.MIN);
181-
} else {
182-
dateTime = dateTime.plusMonths(sign * num);
183-
}
184-
if (roundUp) {
185-
dateTime = dateTime.plusMonths(1);
186-
}
187-
break;
188-
case 'w':
189-
if (round) {
190-
dateTime = dateTime.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).with(LocalTime.MIN);
191-
} else {
192-
dateTime = dateTime.plusWeeks(sign * num);
193-
}
194-
if (roundUp) {
195-
dateTime = dateTime.plusWeeks(1);
196-
}
197-
break;
198-
case 'd':
199-
if (round) {
200-
dateTime = dateTime.with(LocalTime.MIN);
201-
} else {
202-
dateTime = dateTime.plusDays(sign * num);
203-
}
204-
if (roundUp) {
205-
dateTime = dateTime.plusDays(1);
206-
}
207-
break;
208-
case 'h':
209-
case 'H':
210-
if (round) {
211-
dateTime = dateTime.withMinute(0).withSecond(0).withNano(0);
212-
} else {
213-
dateTime = dateTime.plusHours(sign * num);
214-
}
215-
if (roundUp) {
216-
dateTime = dateTime.plusHours(1);
217-
}
218-
break;
219-
case 'm':
220-
if (round) {
221-
dateTime = dateTime.withSecond(0).withNano(0);
222-
} else {
223-
dateTime = dateTime.plusMinutes(sign * num);
224-
}
225-
if (roundUp) {
226-
dateTime = dateTime.plusMinutes(1);
227-
}
228-
break;
229-
case 's':
230-
if (round) {
231-
dateTime = dateTime.withNano(0);
232-
} else {
233-
dateTime = dateTime.plusSeconds(sign * num);
234-
}
235-
if (roundUp) {
236-
dateTime = dateTime.plusSeconds(1);
237-
}
238-
break;
239-
default:
240-
throw new ElasticsearchParseException("unit [{}] not supported for date math [{}]", unit, mathString);
241-
}
242-
if (roundUp) {
243-
dateTime = dateTime.minus(1, ChronoField.MILLI_OF_SECOND.getBaseUnit());
244-
}
245-
}
246-
return dateTime.toInstant().toEpochMilli();
247-
}
248-
249-
private long parseDateTime(String value, ZoneId timeZone, boolean roundUpIfNoTime) {
250-
CompoundDateTimeFormatter formatter = roundUpIfNoTime ? this.roundUpFormatter : this.formatter;
251-
try {
252-
if (timeZone == null) {
253-
return DateFormatters.toZonedDateTime(formatter.parse(value)).toInstant().toEpochMilli();
254-
} else {
255-
TemporalAccessor accessor = formatter.parse(value);
256-
ZoneId zoneId = TemporalQueries.zone().queryFrom(accessor);
257-
if (zoneId != null) {
258-
timeZone = zoneId;
259-
}
260-
261-
return DateFormatters.toZonedDateTime(accessor).withZoneSameLocal(timeZone).toInstant().toEpochMilli();
262-
}
263-
} catch (IllegalArgumentException | DateTimeException e) {
264-
throw new ElasticsearchParseException("failed to parse date field [{}]: [{}]", e, value, e.getMessage());
265-
}
266-
}
73+
long parse(String text, LongSupplier now, boolean roundUp, ZoneId tz);
26774
}

0 commit comments

Comments
 (0)