19
19
20
20
package org .elasticsearch .common .time ;
21
21
22
- import org .elasticsearch . ElasticsearchParseException ;
22
+ import org .joda . time . DateTimeZone ;
23
23
24
- import java .time .DateTimeException ;
25
- import java .time .DayOfWeek ;
26
- import java .time .Instant ;
27
- import java .time .LocalTime ;
28
24
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 ;
39
25
import java .util .function .LongSupplier ;
40
26
41
27
/**
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.
47
29
*/
48
- public class DateMathParser {
30
+ public interface DateMathParser {
49
31
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 );
59
37
}
60
38
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`.
63
42
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 ()));
72
47
}
73
48
74
49
/**
@@ -92,176 +67,8 @@ public long parse(String text, LongSupplier now) {
92
67
* @param text the input
93
68
* @param now a supplier to retrieve the current date in milliseconds, if needed for additions
94
69
* @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
96
71
* @return the parsed date in milliseconds since the epoch
97
72
*/
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 );
267
74
}
0 commit comments