Core: Parse floats in epoch millis parser (#34504)

In order to stay BWC compatible with joda time, the epoch millis date
formatter needs to parse dates with a dot like `123.45`. This
adds this functionality for the epoch millis parser in the same way as
for the epoch seconds parser. It also adds support for scientific
notations like `1.0e3` and fixes parsing of negative values for epoch
seconds and epoch millis.
This commit is contained in:
Alexander Reelsen 2018-10-19 10:02:45 +02:00 committed by GitHub
parent 4f7895800e
commit e498b7d437
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 86 additions and 4 deletions

View File

@ -19,6 +19,7 @@
package org.elasticsearch.common.time; package org.elasticsearch.common.time;
import java.math.BigDecimal;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
@ -27,6 +28,7 @@ import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField; import java.time.temporal.TemporalField;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern;
/** /**
* This is a special formatter to parse the milliseconds since the epoch. * This is a special formatter to parse the milliseconds since the epoch.
@ -39,7 +41,8 @@ import java.util.Map;
*/ */
class EpochMillisDateFormatter implements DateFormatter { class EpochMillisDateFormatter implements DateFormatter {
public static DateFormatter INSTANCE = new EpochMillisDateFormatter(); private static final Pattern SPLIT_BY_DOT_PATTERN = Pattern.compile("\\.");
static DateFormatter INSTANCE = new EpochMillisDateFormatter();
private EpochMillisDateFormatter() { private EpochMillisDateFormatter() {
} }
@ -47,12 +50,33 @@ class EpochMillisDateFormatter implements DateFormatter {
@Override @Override
public TemporalAccessor parse(String input) { public TemporalAccessor parse(String input) {
try { try {
return Instant.ofEpochMilli(Long.valueOf(input)).atZone(ZoneOffset.UTC); if (input.contains(".")) {
String[] inputs = SPLIT_BY_DOT_PATTERN.split(input, 2);
Long milliSeconds = Long.valueOf(inputs[0]);
if (inputs[1].length() == 0) {
// this is BWC compatible to joda time, nothing after the dot is allowed
return Instant.ofEpochMilli(milliSeconds).atZone(ZoneOffset.UTC);
}
// scientific notation it is!
if (inputs[1].contains("e")) {
return Instant.ofEpochMilli(Double.valueOf(input).longValue()).atZone(ZoneOffset.UTC);
}
if (inputs[1].length() > 6) {
throw new DateTimeParseException("too much granularity after dot [" + input + "]", input, 0);
}
Long nanos = new BigDecimal(inputs[1]).movePointRight(6 - inputs[1].length()).longValueExact();
if (milliSeconds < 0) {
nanos = nanos * -1;
}
return Instant.ofEpochMilli(milliSeconds).plusNanos(nanos).atZone(ZoneOffset.UTC);
} else {
return Instant.ofEpochMilli(Long.valueOf(input)).atZone(ZoneOffset.UTC);
}
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new DateTimeParseException("invalid number", input, 0, e); throw new DateTimeParseException("invalid number [" + input + "]", input, 0, e);
} }
} }
@Override @Override
public DateFormatter withZone(ZoneId zoneId) { public DateFormatter withZone(ZoneId zoneId) {
if (ZoneOffset.UTC.equals(zoneId) == false) { if (ZoneOffset.UTC.equals(zoneId) == false) {

View File

@ -47,10 +47,17 @@ public class EpochSecondsDateFormatter implements DateFormatter {
// this is BWC compatible to joda time, nothing after the dot is allowed // this is BWC compatible to joda time, nothing after the dot is allowed
return Instant.ofEpochSecond(seconds, 0).atZone(ZoneOffset.UTC); return Instant.ofEpochSecond(seconds, 0).atZone(ZoneOffset.UTC);
} }
// scientific notation it is!
if (inputs[1].contains("e")) {
return Instant.ofEpochSecond(Double.valueOf(input).longValue()).atZone(ZoneOffset.UTC);
}
if (inputs[1].length() > 9) { if (inputs[1].length() > 9) {
throw new DateTimeParseException("too much granularity after dot [" + input + "]", input, 0); throw new DateTimeParseException("too much granularity after dot [" + input + "]", input, 0);
} }
Long nanos = new BigDecimal(inputs[1]).movePointRight(9 - inputs[1].length()).longValueExact(); Long nanos = new BigDecimal(inputs[1]).movePointRight(9 - inputs[1].length()).longValueExact();
if (seconds < 0) {
nanos = nanos * -1;
}
return Instant.ofEpochSecond(seconds, nanos).atZone(ZoneOffset.UTC); return Instant.ofEpochSecond(seconds, nanos).atZone(ZoneOffset.UTC);
} else { } else {
return Instant.ofEpochSecond(Long.valueOf(input)).atZone(ZoneOffset.UTC); return Instant.ofEpochSecond(Long.valueOf(input)).atZone(ZoneOffset.UTC);

View File

@ -77,11 +77,13 @@ public class JavaJodaTimeDuellingTests extends ESTestCase {
assertSameDate("1", "epoch_second"); assertSameDate("1", "epoch_second");
assertSameDate("-1", "epoch_second"); assertSameDate("-1", "epoch_second");
assertSameDate("-1522332219", "epoch_second"); assertSameDate("-1522332219", "epoch_second");
assertSameDate("1.0e3", "epoch_second");
assertSameDate("1522332219321", "epoch_millis"); assertSameDate("1522332219321", "epoch_millis");
assertSameDate("0", "epoch_millis"); assertSameDate("0", "epoch_millis");
assertSameDate("1", "epoch_millis"); assertSameDate("1", "epoch_millis");
assertSameDate("-1", "epoch_millis"); assertSameDate("-1", "epoch_millis");
assertSameDate("-1522332219321", "epoch_millis"); assertSameDate("-1522332219321", "epoch_millis");
assertSameDate("1.0e3", "epoch_millis");
assertSameDate("20181126", "basic_date"); assertSameDate("20181126", "basic_date");
assertSameDate("20181126T121212.123Z", "basic_date_time"); assertSameDate("20181126T121212.123Z", "basic_date_time");

View File

@ -37,10 +37,55 @@ import static org.hamcrest.Matchers.sameInstance;
public class DateFormattersTests extends ESTestCase { public class DateFormattersTests extends ESTestCase {
// this is not in the duelling tests, because the epoch millis parser in joda time drops the milliseconds after the comma
// but is able to parse the rest
// as this feature is supported it also makes sense to make it exact
public void testEpochMillisParser() {
DateFormatter formatter = DateFormatters.forPattern("epoch_millis");
{
Instant instant = Instant.from(formatter.parse("12345.6789"));
assertThat(instant.getEpochSecond(), is(12L));
assertThat(instant.getNano(), is(345_678_900));
}
{
Instant instant = Instant.from(formatter.parse("12345"));
assertThat(instant.getEpochSecond(), is(12L));
assertThat(instant.getNano(), is(345_000_000));
}
{
Instant instant = Instant.from(formatter.parse("12345."));
assertThat(instant.getEpochSecond(), is(12L));
assertThat(instant.getNano(), is(345_000_000));
}
{
Instant instant = Instant.from(formatter.parse("-12345.6789"));
assertThat(instant.getEpochSecond(), is(-13L));
assertThat(instant.getNano(), is(1_000_000_000 - 345_678_900));
}
{
Instant instant = Instant.from(formatter.parse("-436134.241272"));
assertThat(instant.getEpochSecond(), is(-437L));
assertThat(instant.getNano(), is(1_000_000_000 - 134_241_272));
}
{
Instant instant = Instant.from(formatter.parse("-12345"));
assertThat(instant.getEpochSecond(), is(-13L));
assertThat(instant.getNano(), is(1_000_000_000 - 345_000_000));
}
{
Instant instant = Instant.from(formatter.parse("0"));
assertThat(instant.getEpochSecond(), is(0L));
assertThat(instant.getNano(), is(0));
}
}
public void testEpochMilliParser() { public void testEpochMilliParser() {
DateFormatter formatter = DateFormatters.forPattern("epoch_millis"); DateFormatter formatter = DateFormatters.forPattern("epoch_millis");
DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("invalid")); DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("invalid"));
assertThat(e.getMessage(), containsString("invalid number")); assertThat(e.getMessage(), containsString("invalid number"));
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("123.1234567"));
assertThat(e.getMessage(), containsString("too much granularity after dot [123.1234567]"));
} }
// this is not in the duelling tests, because the epoch second parser in joda time drops the milliseconds after the comma // this is not in the duelling tests, because the epoch second parser in joda time drops the milliseconds after the comma
@ -61,6 +106,10 @@ public class DateFormattersTests extends ESTestCase {
assertThat(Instant.from(formatter.parse("1234.1234567")).getNano(), is(123_456_700)); assertThat(Instant.from(formatter.parse("1234.1234567")).getNano(), is(123_456_700));
assertThat(Instant.from(formatter.parse("1234.12345678")).getNano(), is(123_456_780)); assertThat(Instant.from(formatter.parse("1234.12345678")).getNano(), is(123_456_780));
assertThat(Instant.from(formatter.parse("1234.123456789")).getNano(), is(123_456_789)); assertThat(Instant.from(formatter.parse("1234.123456789")).getNano(), is(123_456_789));
assertThat(Instant.from(formatter.parse("-1234.567")).toEpochMilli(), is(-1234567L));
assertThat(Instant.from(formatter.parse("-1234")).getNano(), is(0));
DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.1234567890")); DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.1234567890"));
assertThat(e.getMessage(), is("too much granularity after dot [1234.1234567890]")); assertThat(e.getMessage(), is("too much granularity after dot [1234.1234567890]"));
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.123456789013221")); e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.123456789013221"));