Skip to content

Commit

Permalink
Core: Fix epoch millis java time formatter (elastic#33302)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
spinscale authored Sep 3, 2018
1 parent 978d1ed commit 246a7df
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,15 @@ public void testCustomTimeFormats() {

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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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)));
}
}

0 comments on commit 246a7df

Please sign in to comment.