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:
parent
4f7895800e
commit
e498b7d437
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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"));
|
||||||
|
|
Loading…
Reference in New Issue