Core: Fix epoch millis java time formatter (#33302)

The existing implemention could not deal with negative numbers as well
as +- 999 milliseconds around the epoch.

This commit uses Instant.ofEpochMilli() and parses the input to
a number instead of using a date formatter.
This commit is contained in:
Alexander Reelsen 2018-09-03 13:13:19 +02:00 committed by GitHub
parent 978d1ed257
commit 246a7df8c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 121 additions and 2 deletions

View File

@ -25,10 +25,12 @@ import java.time.DateTimeException;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.format.SignStyle;
import java.time.temporal.ChronoField;
@ -879,11 +881,47 @@ public class DateFormatters {
/*
* Returns a formatter for parsing the milliseconds since the epoch
* This one needs a custom implementation, because the standard date formatter can not parse negative values
* or anything +- 999 milliseconds around the epoch
*
* This implementation just resorts to parsing the input directly to an Instant by trying to parse a number.
*/
private static final CompoundDateTimeFormatter EPOCH_MILLIS = new CompoundDateTimeFormatter(new DateTimeFormatterBuilder()
private static final DateTimeFormatter EPOCH_MILLIS_FORMATTER = new DateTimeFormatterBuilder()
.appendValue(ChronoField.INSTANT_SECONDS, 1, 19, SignStyle.NEVER)
.appendValue(ChronoField.MILLI_OF_SECOND, 3)
.toFormatter(Locale.ROOT));
.toFormatter(Locale.ROOT);
private static final class EpochDateTimeFormatter extends CompoundDateTimeFormatter {
private EpochDateTimeFormatter() {
super(EPOCH_MILLIS_FORMATTER);
}
private EpochDateTimeFormatter(ZoneId zoneId) {
super(EPOCH_MILLIS_FORMATTER.withZone(zoneId));
}
@Override
public TemporalAccessor parse(String input) {
try {
return Instant.ofEpochMilli(Long.valueOf(input)).atZone(ZoneOffset.UTC);
} catch (NumberFormatException e) {
throw new DateTimeParseException("invalid number", input, 0, e);
}
}
@Override
public CompoundDateTimeFormatter withZone(ZoneId zoneId) {
return new EpochDateTimeFormatter(zoneId);
}
@Override
public String format(TemporalAccessor accessor) {
return String.valueOf(Instant.from(accessor).toEpochMilli());
}
}
private static final CompoundDateTimeFormatter EPOCH_MILLIS = new EpochDateTimeFormatter();
/*
* Returns a formatter that combines a full date and two digit hour of

View File

@ -71,7 +71,15 @@ public class JavaJodaTimeDuellingTests extends ESTestCase {
public void testDuellingFormatsValidParsing() {
assertSameDate("1522332219", "epoch_second");
assertSameDate("0", "epoch_second");
assertSameDate("1", "epoch_second");
assertSameDate("-1", "epoch_second");
assertSameDate("-1522332219", "epoch_second");
assertSameDate("1522332219321", "epoch_millis");
assertSameDate("0", "epoch_millis");
assertSameDate("1", "epoch_millis");
assertSameDate("-1", "epoch_millis");
assertSameDate("-1522332219321", "epoch_millis");
assertSameDate("20181126", "basic_date");
assertSameDate("20181126T121212.123Z", "basic_date_time");

View File

@ -0,0 +1,73 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.common.time;
import org.elasticsearch.test.ESTestCase;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
public class DateFormattersTests extends ESTestCase {
// the epoch milli parser is a bit special, as it does not use date formatter, see comments in DateFormatters
public void testEpochMilliParser() {
CompoundDateTimeFormatter formatter = DateFormatters.forPattern("epoch_millis");
DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("invalid"));
assertThat(e.getMessage(), containsString("invalid number"));
// different zone, should still yield the same output, as epoch is time zoned independent
ZoneId zoneId = randomZone();
CompoundDateTimeFormatter zonedFormatter = formatter.withZone(zoneId);
assertThat(zonedFormatter.printer.getZone(), is(zoneId));
// test with negative and non negative values
assertThatSameDateTime(formatter, zonedFormatter, randomNonNegativeLong() * -1);
assertThatSameDateTime(formatter, zonedFormatter, randomNonNegativeLong());
assertThatSameDateTime(formatter, zonedFormatter, 0);
assertThatSameDateTime(formatter, zonedFormatter, -1);
assertThatSameDateTime(formatter, zonedFormatter, 1);
// format() output should be equal as well
assertSameFormat(formatter, randomNonNegativeLong() * -1);
assertSameFormat(formatter, randomNonNegativeLong());
assertSameFormat(formatter, 0);
assertSameFormat(formatter, -1);
assertSameFormat(formatter, 1);
}
private void assertThatSameDateTime(CompoundDateTimeFormatter formatter, CompoundDateTimeFormatter zonedFormatter, long millis) {
String millisAsString = String.valueOf(millis);
ZonedDateTime formatterZonedDateTime = DateFormatters.toZonedDateTime(formatter.parse(millisAsString));
ZonedDateTime zonedFormatterZonedDateTime = DateFormatters.toZonedDateTime(zonedFormatter.parse(millisAsString));
assertThat(formatterZonedDateTime.toInstant().toEpochMilli(), is(zonedFormatterZonedDateTime.toInstant().toEpochMilli()));
}
private void assertSameFormat(CompoundDateTimeFormatter formatter, long millis) {
String millisAsString = String.valueOf(millis);
TemporalAccessor accessor = formatter.parse(millisAsString);
assertThat(millisAsString, is(formatter.format(accessor)));
}
}