From e498b7d4373eb5696d45ed35df6fd1cfc3f1b38e Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Fri, 19 Oct 2018 10:02:45 +0200 Subject: [PATCH] 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. --- .../common/time/EpochMillisDateFormatter.java | 32 ++++++++++-- .../time/EpochSecondsDateFormatter.java | 7 +++ .../joda/JavaJodaTimeDuellingTests.java | 2 + .../common/time/DateFormattersTests.java | 49 +++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java index d059f358ccb..4fce6351002 100644 --- a/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java @@ -19,6 +19,7 @@ package org.elasticsearch.common.time; +import java.math.BigDecimal; import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; @@ -27,6 +28,7 @@ import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalField; import java.util.Locale; import java.util.Map; +import java.util.regex.Pattern; /** * This is a special formatter to parse the milliseconds since the epoch. @@ -39,7 +41,8 @@ import java.util.Map; */ 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() { } @@ -47,12 +50,33 @@ class EpochMillisDateFormatter implements DateFormatter { @Override public TemporalAccessor parse(String input) { 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) { - throw new DateTimeParseException("invalid number", input, 0, e); + throw new DateTimeParseException("invalid number [" + input + "]", input, 0, e); } } - @Override public DateFormatter withZone(ZoneId zoneId) { if (ZoneOffset.UTC.equals(zoneId) == false) { diff --git a/server/src/main/java/org/elasticsearch/common/time/EpochSecondsDateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/EpochSecondsDateFormatter.java index a5d702bbda5..218542f817b 100644 --- a/server/src/main/java/org/elasticsearch/common/time/EpochSecondsDateFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/EpochSecondsDateFormatter.java @@ -47,10 +47,17 @@ public class EpochSecondsDateFormatter implements DateFormatter { // this is BWC compatible to joda time, nothing after the dot is allowed 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) { throw new DateTimeParseException("too much granularity after dot [" + input + "]", input, 0); } 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); } else { return Instant.ofEpochSecond(Long.valueOf(input)).atZone(ZoneOffset.UTC); diff --git a/server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java b/server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java index 61aa7dc4a46..c4efa78378d 100644 --- a/server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java +++ b/server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java @@ -77,11 +77,13 @@ public class JavaJodaTimeDuellingTests extends ESTestCase { assertSameDate("1", "epoch_second"); assertSameDate("-1", "epoch_second"); assertSameDate("-1522332219", "epoch_second"); + assertSameDate("1.0e3", "epoch_second"); assertSameDate("1522332219321", "epoch_millis"); assertSameDate("0", "epoch_millis"); assertSameDate("1", "epoch_millis"); assertSameDate("-1", "epoch_millis"); assertSameDate("-1522332219321", "epoch_millis"); + assertSameDate("1.0e3", "epoch_millis"); assertSameDate("20181126", "basic_date"); assertSameDate("20181126T121212.123Z", "basic_date_time"); diff --git a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java index df9aa6da83e..6a263b7db42 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java @@ -37,10 +37,55 @@ import static org.hamcrest.Matchers.sameInstance; 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() { DateFormatter formatter = DateFormatters.forPattern("epoch_millis"); DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("invalid")); 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 @@ -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.12345678")).getNano(), is(123_456_780)); 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")); assertThat(e.getMessage(), is("too much granularity after dot [1234.1234567890]")); e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.123456789013221"));