Skip to content

Commit 6fbeeb3

Browse files
committed
Fix java time epoch date formatters (elastic#37829)
The self written epoch date formatters were not properly able to format an Instant to a string due to a misconfiguration. This also adds some tests for fractional formatters
1 parent a427d4b commit 6fbeeb3

File tree

2 files changed

+61
-34
lines changed

2 files changed

+61
-34
lines changed

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

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public TemporalAccessor resolve(Map<TemporalField,Long> fieldValues,
7070
private static final EpochField NANOS_OF_SECOND = new EpochField(ChronoUnit.NANOS, ChronoUnit.SECONDS, ValueRange.of(0, 999_999_999)) {
7171
@Override
7272
public boolean isSupportedBy(TemporalAccessor temporal) {
73-
return temporal.isSupported(ChronoField.NANO_OF_SECOND) && temporal.getLong(ChronoField.NANO_OF_SECOND) != 0;
73+
return temporal.isSupported(ChronoField.NANO_OF_SECOND);
7474
}
7575
@Override
7676
public long getFrom(TemporalAccessor temporal) {
@@ -111,55 +111,45 @@ public boolean isSupportedBy(TemporalAccessor temporal) {
111111
}
112112
@Override
113113
public long getFrom(TemporalAccessor temporal) {
114-
return temporal.getLong(ChronoField.NANO_OF_SECOND);
114+
return temporal.getLong(ChronoField.NANO_OF_SECOND) % 1_000_000;
115115
}
116116
};
117117

118118
// this supports seconds without any fraction
119119
private static final DateTimeFormatter SECONDS_FORMATTER1 = new DateTimeFormatterBuilder()
120120
.appendValue(SECONDS, 1, 19, SignStyle.NORMAL)
121+
.optionalStart() // optional is used so isSupported will be called when printing
122+
.appendFraction(NANOS_OF_SECOND, 0, 9, true)
123+
.optionalEnd()
121124
.toFormatter(Locale.ROOT);
122125

123126
// this supports seconds ending in dot
124127
private static final DateTimeFormatter SECONDS_FORMATTER2 = new DateTimeFormatterBuilder()
125-
.append(SECONDS_FORMATTER1)
128+
.appendValue(SECONDS, 1, 19, SignStyle.NORMAL)
126129
.appendLiteral('.')
127130
.toFormatter(Locale.ROOT);
128131

129-
// this supports seconds with a fraction and is also used for printing
130-
private static final DateTimeFormatter SECONDS_FORMATTER3 = new DateTimeFormatterBuilder()
131-
.append(SECONDS_FORMATTER1)
132-
.optionalStart() // optional is used so isSupported will be called when printing
133-
.appendFraction(NANOS_OF_SECOND, 1, 9, true)
134-
.optionalEnd()
135-
.toFormatter(Locale.ROOT);
136-
137132
// this supports milliseconds without any fraction
138133
private static final DateTimeFormatter MILLISECONDS_FORMATTER1 = new DateTimeFormatterBuilder()
139134
.appendValue(MILLIS, 1, 19, SignStyle.NORMAL)
135+
.optionalStart()
136+
.appendFraction(NANOS_OF_MILLI, 0, 6, true)
137+
.optionalEnd()
140138
.toFormatter(Locale.ROOT);
141139

142140
// this supports milliseconds ending in dot
143141
private static final DateTimeFormatter MILLISECONDS_FORMATTER2 = new DateTimeFormatterBuilder()
144-
.append(MILLISECONDS_FORMATTER1)
142+
.appendValue(MILLIS, 1, 19, SignStyle.NORMAL)
145143
.appendLiteral('.')
146144
.toFormatter(Locale.ROOT);
147145

148-
// this supports milliseconds with a fraction and is also used for printing
149-
private static final DateTimeFormatter MILLISECONDS_FORMATTER3 = new DateTimeFormatterBuilder()
150-
.append(MILLISECONDS_FORMATTER1)
151-
.optionalStart() // optional is used so isSupported will be called when printing
152-
.appendFraction(NANOS_OF_MILLI, 1, 6, true)
153-
.optionalEnd()
154-
.toFormatter(Locale.ROOT);
155-
156-
static final DateFormatter SECONDS_FORMATTER = new JavaDateFormatter("epoch_second", SECONDS_FORMATTER3,
146+
static final DateFormatter SECONDS_FORMATTER = new JavaDateFormatter("epoch_second", SECONDS_FORMATTER1,
157147
builder -> builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L),
158-
SECONDS_FORMATTER1, SECONDS_FORMATTER2, SECONDS_FORMATTER3);
148+
SECONDS_FORMATTER1, SECONDS_FORMATTER2);
159149

160-
static final DateFormatter MILLIS_FORMATTER = new JavaDateFormatter("epoch_millis", MILLISECONDS_FORMATTER3,
150+
static final DateFormatter MILLIS_FORMATTER = new JavaDateFormatter("epoch_millis", MILLISECONDS_FORMATTER1,
161151
builder -> builder.parseDefaulting(EpochTime.NANOS_OF_MILLI, 999_999L),
162-
MILLISECONDS_FORMATTER1, MILLISECONDS_FORMATTER2, MILLISECONDS_FORMATTER3);
152+
MILLISECONDS_FORMATTER1, MILLISECONDS_FORMATTER2);
163153

164154
private abstract static class EpochField implements TemporalField {
165155

server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import static org.hamcrest.Matchers.not;
3838
import static org.hamcrest.Matchers.nullValue;
3939
import static org.hamcrest.Matchers.sameInstance;
40+
import static org.hamcrest.Matchers.startsWith;
4041

4142
public class DateFormattersTests extends ESTestCase {
4243

@@ -55,35 +56,50 @@ public void testEpochMillisParser() {
5556
assertThat(instant.getEpochSecond(), is(0L));
5657
assertThat(instant.getNano(), is(0));
5758
}
59+
{
60+
Instant instant = Instant.from(formatter.parse("123.123456"));
61+
assertThat(instant.getEpochSecond(), is(0L));
62+
assertThat(instant.getNano(), is(123123456));
63+
}
5864
}
5965

6066
public void testEpochMilliParser() {
61-
DateFormatter formatter = DateFormatters.forPattern("epoch_millis");
67+
DateFormatter formatter = DateFormatter.forPattern("8epoch_millis");
6268
DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("invalid"));
6369
assertThat(e.getMessage(), containsString("could not be parsed"));
6470

6571
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("123.1234567"));
66-
assertThat(e.getMessage(), containsString("unparsed text found at index 3"));
72+
assertThat(e.getMessage(), containsString("unparsed text found"));
6773
}
6874

6975
// this is not in the duelling tests, because the epoch second parser in joda time drops the milliseconds after the comma
7076
// but is able to parse the rest
7177
// as this feature is supported it also makes sense to make it exact
72-
public void testEpochSecondParser() {
78+
public void testEpochSecondParserWithFraction() {
7379
DateFormatter formatter = DateFormatters.forPattern("epoch_second");
7480

75-
DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.1"));
76-
assertThat(e.getMessage(), is("Text '1234.1' could not be parsed, unparsed text found at index 4"));
77-
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234."));
78-
assertThat(e.getMessage(), is("Text '1234.' could not be parsed, unparsed text found at index 4"));
79-
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("abc"));
81+
TemporalAccessor accessor = formatter.parse("1234.1");
82+
Instant instant = DateFormatters.toZonedDateTime(accessor).toInstant();
83+
assertThat(instant.getEpochSecond(), is(1234L));
84+
assertThat(DateFormatters.toZonedDateTime(accessor).toInstant().getNano(), is(100_000_000));
85+
86+
accessor = formatter.parse("1234");
87+
instant = DateFormatters.toZonedDateTime(accessor).toInstant();
88+
assertThat(instant.getEpochSecond(), is(1234L));
89+
assertThat(instant.getNano(), is(0));
90+
91+
DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("abc"));
8092
assertThat(e.getMessage(), is("Text 'abc' could not be parsed, unparsed text found at index 0"));
93+
8194
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.abc"));
82-
assertThat(e.getMessage(), is("Text '1234.abc' could not be parsed, unparsed text found at index 4"));
95+
assertThat(e.getMessage(), is("Text '1234.abc' could not be parsed, unparsed text found at index 5"));
96+
97+
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.1234567890"));
98+
assertThat(e.getMessage(), is("Text '1234.1234567890' could not be parsed, unparsed text found at index 14"));
8399
}
84100

85101
public void testEpochMilliParsersWithDifferentFormatters() {
86-
DateFormatter formatter = DateFormatter.forPattern("strict_date_optional_time||epoch_millis");
102+
DateFormatter formatter = DateFormatter.forPattern("8strict_date_optional_time||epoch_millis");
87103
TemporalAccessor accessor = formatter.parse("123");
88104
assertThat(DateFormatters.toZonedDateTime(accessor).toInstant().toEpochMilli(), is(123L));
89105
assertThat(formatter.pattern(), is("strict_date_optional_time||epoch_millis"));
@@ -148,6 +164,26 @@ public void testForceJava8() {
148164
assertThat(formatter, instanceOf(JavaDateFormatter.class));
149165
}
150166

167+
public void testEpochFormatting() {
168+
long seconds = randomLongBetween(0, 130L * 365 * 86400); // from 1970 epoch till around 2100
169+
long nanos = randomLongBetween(0, 999_999_999L);
170+
Instant instant = Instant.ofEpochSecond(seconds, nanos);
171+
172+
DateFormatter millisFormatter = DateFormatter.forPattern("8epoch_millis");
173+
String millis = millisFormatter.format(instant);
174+
Instant millisInstant = Instant.from(millisFormatter.parse(millis));
175+
assertThat(millisInstant.toEpochMilli(), is(instant.toEpochMilli()));
176+
assertThat(millisFormatter.format(Instant.ofEpochSecond(42, 0)), is("42000"));
177+
assertThat(millisFormatter.format(Instant.ofEpochSecond(42, 123456789L)), is("42123.456789"));
178+
179+
DateFormatter secondsFormatter = DateFormatter.forPattern("8epoch_second");
180+
String formattedSeconds = secondsFormatter.format(instant);
181+
Instant secondsInstant = Instant.from(secondsFormatter.parse(formattedSeconds));
182+
assertThat(secondsInstant.getEpochSecond(), is(instant.getEpochSecond()));
183+
184+
assertThat(secondsFormatter.format(Instant.ofEpochSecond(42, 0)), is("42"));
185+
}
186+
151187
public void testParsingStrictNanoDates() {
152188
DateFormatter formatter = DateFormatters.forPattern("strict_date_optional_time_nanos");
153189
formatter.format(formatter.parse("2016-01-01T00:00:00.000"));
@@ -185,6 +221,7 @@ public void testRoundupFormatterWithEpochDates() {
185221
}
186222

187223
private void assertRoundupFormatter(String format, String input, long expectedMilliSeconds) {
224+
assertThat(format, startsWith("8"));
188225
JavaDateFormatter dateFormatter = (JavaDateFormatter) DateFormatter.forPattern(format);
189226
dateFormatter.parse(input);
190227
DateTimeFormatter roundUpFormatter = dateFormatter.getRoundupParser();

0 commit comments

Comments
 (0)