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:
parent
978d1ed257
commit
246a7df8c2
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue