Skip to content

Commit 55d3ca3

Browse files
authored
Core: Rework epoch time parsing for java time (#36914)
This commit converts the epoch time parsing implementation which uses the java time api to create DateTimeFormatters instead of DateFormatter implementations. This will allow multi formats for java time to be implemented in a single DateTimeFormatter in a future change.
1 parent 56e472b commit 55d3ca3

File tree

7 files changed

+239
-168
lines changed

7 files changed

+239
-168
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,9 +1366,9 @@ public static DateFormatter forPattern(String input) {
13661366
} else if ("yearMonthDay".equals(input) || "year_month_day".equals(input)) {
13671367
return YEAR_MONTH_DAY;
13681368
} else if ("epoch_second".equals(input)) {
1369-
return EpochSecondsDateFormatter.INSTANCE;
1369+
return EpochTime.SECONDS_FORMATTER;
13701370
} else if ("epoch_millis".equals(input)) {
1371-
return EpochMillisDateFormatter.INSTANCE;
1371+
return EpochTime.MILLIS_FORMATTER;
13721372
// strict date formats here, must be at least 4 digits for year and two for months and two for day
13731373
} else if ("strictBasicWeekDate".equals(input) || "strict_basic_week_date".equals(input)) {
13741374
return STRICT_BASIC_WEEK_DATE;

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

Lines changed: 0 additions & 113 deletions
This file was deleted.
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common.time;
21+
22+
import java.time.format.DateTimeFormatter;
23+
import java.time.format.DateTimeFormatterBuilder;
24+
import java.time.format.ResolverStyle;
25+
import java.time.format.SignStyle;
26+
import java.time.temporal.ChronoField;
27+
import java.time.temporal.ChronoUnit;
28+
import java.time.temporal.Temporal;
29+
import java.time.temporal.TemporalAccessor;
30+
import java.time.temporal.TemporalField;
31+
import java.time.temporal.TemporalUnit;
32+
import java.time.temporal.ValueRange;
33+
import java.util.Locale;
34+
import java.util.Map;
35+
36+
/**
37+
* This class provides {@link DateTimeFormatter}s capable of parsing epoch seconds and milliseconds.
38+
* <p>
39+
* The seconds formatter is provided by {@link #SECONDS_FORMATTER}.
40+
* The milliseconds formatter is provided by {@link #MILLIS_FORMATTER}.
41+
* <p>
42+
* Both formatters support fractional time, up to nanosecond precision. Values must be positive numbers.
43+
*/
44+
class EpochTime {
45+
46+
private static final ValueRange LONG_POSITIVE_RANGE = ValueRange.of(0, Long.MAX_VALUE);
47+
48+
private static final EpochField SECONDS = new EpochField(ChronoUnit.SECONDS, ChronoUnit.FOREVER, LONG_POSITIVE_RANGE) {
49+
@Override
50+
public boolean isSupportedBy(TemporalAccessor temporal) {
51+
return temporal.isSupported(ChronoField.INSTANT_SECONDS);
52+
}
53+
@Override
54+
public long getFrom(TemporalAccessor temporal) {
55+
return temporal.getLong(ChronoField.INSTANT_SECONDS);
56+
}
57+
@Override
58+
public TemporalAccessor resolve(Map<TemporalField,Long> fieldValues,
59+
TemporalAccessor partialTemporal, ResolverStyle resolverStyle) {
60+
long seconds = fieldValues.remove(this);
61+
fieldValues.put(ChronoField.INSTANT_SECONDS, seconds);
62+
Long nanos = fieldValues.remove(NANOS_OF_SECOND);
63+
if (nanos != null) {
64+
fieldValues.put(ChronoField.NANO_OF_SECOND, nanos);
65+
}
66+
return null;
67+
}
68+
};
69+
70+
private static final EpochField NANOS_OF_SECOND = new EpochField(ChronoUnit.NANOS, ChronoUnit.SECONDS, ValueRange.of(0, 999_999_999)) {
71+
@Override
72+
public boolean isSupportedBy(TemporalAccessor temporal) {
73+
return temporal.isSupported(ChronoField.NANO_OF_SECOND) && temporal.getLong(ChronoField.NANO_OF_SECOND) != 0;
74+
}
75+
@Override
76+
public long getFrom(TemporalAccessor temporal) {
77+
return temporal.getLong(ChronoField.NANO_OF_SECOND);
78+
}
79+
};
80+
81+
private static final EpochField MILLIS = new EpochField(ChronoUnit.MILLIS, ChronoUnit.FOREVER, LONG_POSITIVE_RANGE) {
82+
@Override
83+
public boolean isSupportedBy(TemporalAccessor temporal) {
84+
return temporal.isSupported(ChronoField.INSTANT_SECONDS) && temporal.isSupported(ChronoField.MILLI_OF_SECOND);
85+
}
86+
@Override
87+
public long getFrom(TemporalAccessor temporal) {
88+
return temporal.getLong(ChronoField.INSTANT_SECONDS) * 1_000 + temporal.getLong(ChronoField.MILLI_OF_SECOND);
89+
}
90+
@Override
91+
public TemporalAccessor resolve(Map<TemporalField,Long> fieldValues,
92+
TemporalAccessor partialTemporal, ResolverStyle resolverStyle) {
93+
long secondsAndMillis = fieldValues.remove(this);
94+
long seconds = secondsAndMillis / 1_000;
95+
long nanos = secondsAndMillis % 1000 * 1_000_000;
96+
Long nanosOfMilli = fieldValues.remove(NANOS_OF_MILLI);
97+
if (nanosOfMilli != null) {
98+
nanos += nanosOfMilli;
99+
}
100+
fieldValues.put(ChronoField.INSTANT_SECONDS, seconds);
101+
fieldValues.put(ChronoField.NANO_OF_SECOND, nanos);
102+
return null;
103+
}
104+
};
105+
106+
private static final EpochField NANOS_OF_MILLI = new EpochField(ChronoUnit.NANOS, ChronoUnit.MILLIS, ValueRange.of(0, 999_999)) {
107+
@Override
108+
public boolean isSupportedBy(TemporalAccessor temporal) {
109+
return temporal.isSupported(ChronoField.NANO_OF_SECOND) && temporal.getLong(ChronoField.NANO_OF_SECOND) % 1_000_000 != 0;
110+
}
111+
@Override
112+
public long getFrom(TemporalAccessor temporal) {
113+
return temporal.getLong(ChronoField.NANO_OF_SECOND);
114+
}
115+
};
116+
117+
// this supports seconds without any fraction
118+
private static final DateTimeFormatter SECONDS_FORMATTER1 = new DateTimeFormatterBuilder()
119+
.appendValue(SECONDS, 1, 19, SignStyle.NORMAL)
120+
.toFormatter(Locale.ROOT);
121+
122+
// this supports seconds ending in dot
123+
private static final DateTimeFormatter SECONDS_FORMATTER2 = new DateTimeFormatterBuilder()
124+
.append(SECONDS_FORMATTER1)
125+
.appendLiteral('.')
126+
.toFormatter(Locale.ROOT);
127+
128+
// this supports seconds with a fraction and is also used for printing
129+
private static final DateTimeFormatter SECONDS_FORMATTER3 = new DateTimeFormatterBuilder()
130+
.append(SECONDS_FORMATTER1)
131+
.optionalStart() // optional is used so isSupported will be called when printing
132+
.appendFraction(NANOS_OF_SECOND, 1, 9, true)
133+
.optionalEnd()
134+
.toFormatter(Locale.ROOT);
135+
136+
// this supports milliseconds without any fraction
137+
private static final DateTimeFormatter MILLISECONDS_FORMATTER1 = new DateTimeFormatterBuilder()
138+
.appendValue(MILLIS, 1, 19, SignStyle.NORMAL)
139+
.toFormatter(Locale.ROOT);
140+
141+
// this supports milliseconds ending in dot
142+
private static final DateTimeFormatter MILLISECONDS_FORMATTER2 = new DateTimeFormatterBuilder()
143+
.append(MILLISECONDS_FORMATTER1)
144+
.appendLiteral('.')
145+
.toFormatter(Locale.ROOT);
146+
147+
// this supports milliseconds with a fraction and is also used for printing
148+
private static final DateTimeFormatter MILLISECONDS_FORMATTER3 = new DateTimeFormatterBuilder()
149+
.append(MILLISECONDS_FORMATTER1)
150+
.optionalStart() // optional is used so isSupported will be called when printing
151+
.appendFraction(NANOS_OF_MILLI, 1, 6, true)
152+
.optionalEnd()
153+
.toFormatter(Locale.ROOT);
154+
155+
static final DateFormatter SECONDS_FORMATTER = new JavaDateFormatter("epoch_second", SECONDS_FORMATTER3,
156+
SECONDS_FORMATTER1, SECONDS_FORMATTER2, SECONDS_FORMATTER3);
157+
158+
static final DateFormatter MILLIS_FORMATTER = new JavaDateFormatter("epoch_millis", MILLISECONDS_FORMATTER3,
159+
MILLISECONDS_FORMATTER1, MILLISECONDS_FORMATTER2, MILLISECONDS_FORMATTER3);
160+
161+
private abstract static class EpochField implements TemporalField {
162+
163+
private final TemporalUnit baseUnit;
164+
private final TemporalUnit rangeUnit;
165+
private final ValueRange range;
166+
167+
private EpochField(TemporalUnit baseUnit, TemporalUnit rangeUnit, ValueRange range) {
168+
this.baseUnit = baseUnit;
169+
this.rangeUnit = rangeUnit;
170+
this.range = range;
171+
}
172+
173+
@Override
174+
public String getDisplayName(Locale locale) {
175+
return toString();
176+
}
177+
178+
@Override
179+
public String toString() {
180+
return "Epoch" + baseUnit.toString() + (rangeUnit != ChronoUnit.FOREVER ? "Of" + rangeUnit.toString() : "");
181+
}
182+
183+
@Override
184+
public TemporalUnit getBaseUnit() {
185+
return baseUnit;
186+
}
187+
188+
@Override
189+
public TemporalUnit getRangeUnit() {
190+
return rangeUnit;
191+
}
192+
193+
@Override
194+
public ValueRange range() {
195+
return range;
196+
}
197+
198+
@Override
199+
public boolean isDateBased() {
200+
return false;
201+
}
202+
203+
@Override
204+
public boolean isTimeBased() {
205+
return true;
206+
}
207+
208+
@Override
209+
public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
210+
return range();
211+
}
212+
213+
@SuppressWarnings("unchecked")
214+
@Override
215+
public <R extends Temporal> R adjustInto(R temporal, long newValue) {
216+
return (R) temporal.with(this, newValue);
217+
}
218+
}
219+
}

server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.common.joda;
2121

22+
import org.elasticsearch.bootstrap.JavaVersion;
2223
import org.elasticsearch.common.time.DateFormatter;
2324
import org.elasticsearch.common.time.DateFormatters;
2425
import org.elasticsearch.test.ESTestCase;
@@ -384,6 +385,7 @@ public void testSamePrinterOutput() {
384385

385386
ZonedDateTime javaDate = ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneOffset.UTC);
386387
DateTime jodaDate = new DateTime(year, month, day, hour, minute, second, DateTimeZone.UTC);
388+
assertSamePrinterOutput("epoch_second", javaDate, jodaDate);
387389

388390
assertSamePrinterOutput("basicDate", javaDate, jodaDate);
389391
assertSamePrinterOutput("basicDateTime", javaDate, jodaDate);
@@ -428,7 +430,7 @@ public void testSamePrinterOutput() {
428430
assertSamePrinterOutput("year", javaDate, jodaDate);
429431
assertSamePrinterOutput("yearMonth", javaDate, jodaDate);
430432
assertSamePrinterOutput("yearMonthDay", javaDate, jodaDate);
431-
assertSamePrinterOutput("epoch_second", javaDate, jodaDate);
433+
432434
assertSamePrinterOutput("epoch_millis", javaDate, jodaDate);
433435
assertSamePrinterOutput("strictBasicWeekDate", javaDate, jodaDate);
434436
assertSamePrinterOutput("strictBasicWeekDateTime", javaDate, jodaDate);
@@ -476,6 +478,12 @@ private void assertSamePrinterOutput(String format, ZonedDateTime javaDate, Date
476478
assertThat(jodaDate.getMillis(), is(javaDate.toInstant().toEpochMilli()));
477479
String javaTimeOut = DateFormatters.forPattern(format).format(javaDate);
478480
String jodaTimeOut = DateFormatter.forPattern(format).formatJoda(jodaDate);
481+
if (JavaVersion.current().getVersion().get(0) == 8 && javaTimeOut.endsWith(".0")
482+
&& (format.equals("epoch_second") || format.equals("epoch_millis"))) {
483+
// java 8 has a bug in DateTimeFormatter usage when printing dates that rely on isSupportedBy for fields, which is
484+
// what we use for epoch time. This change accounts for that bug. It should be removed when java 8 support is removed
485+
jodaTimeOut += ".0";
486+
}
479487
String message = String.format(Locale.ROOT, "expected string representation to be equal for format [%s]: joda [%s], java [%s]",
480488
format, jodaTimeOut, javaTimeOut);
481489
assertThat(message, javaTimeOut, is(jodaTimeOut));
@@ -484,7 +492,6 @@ private void assertSamePrinterOutput(String format, ZonedDateTime javaDate, Date
484492
private void assertSameDate(String input, String format) {
485493
DateFormatter jodaFormatter = Joda.forPattern(format);
486494
DateFormatter javaFormatter = DateFormatters.forPattern(format);
487-
488495
assertSameDate(input, format, jodaFormatter, javaFormatter);
489496
}
490497

server/src/test/java/org/elasticsearch/common/joda/JodaTests.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
public class JodaTests extends ESTestCase {
3131

32-
3332
public void testBasicTTimePattern() {
3433
DateFormatter formatter1 = DateFormatter.forPattern("basic_t_time");
3534
assertEquals(formatter1.pattern(), "basic_t_time");

0 commit comments

Comments
 (0)