Skip to content

Commit e498b7d

Browse files
authored
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.
1 parent 4f78958 commit e498b7d

File tree

4 files changed

+86
-4
lines changed

4 files changed

+86
-4
lines changed

server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.common.time;
2121

22+
import java.math.BigDecimal;
2223
import java.time.Instant;
2324
import java.time.ZoneId;
2425
import java.time.ZoneOffset;
@@ -27,6 +28,7 @@
2728
import java.time.temporal.TemporalField;
2829
import java.util.Locale;
2930
import java.util.Map;
31+
import java.util.regex.Pattern;
3032

3133
/**
3234
* This is a special formatter to parse the milliseconds since the epoch.
@@ -39,20 +41,42 @@
3941
*/
4042
class EpochMillisDateFormatter implements DateFormatter {
4143

42-
public static DateFormatter INSTANCE = new EpochMillisDateFormatter();
44+
private static final Pattern SPLIT_BY_DOT_PATTERN = Pattern.compile("\\.");
45+
static DateFormatter INSTANCE = new EpochMillisDateFormatter();
4346

4447
private EpochMillisDateFormatter() {
4548
}
4649

4750
@Override
4851
public TemporalAccessor parse(String input) {
4952
try {
50-
return Instant.ofEpochMilli(Long.valueOf(input)).atZone(ZoneOffset.UTC);
53+
if (input.contains(".")) {
54+
String[] inputs = SPLIT_BY_DOT_PATTERN.split(input, 2);
55+
Long milliSeconds = Long.valueOf(inputs[0]);
56+
if (inputs[1].length() == 0) {
57+
// this is BWC compatible to joda time, nothing after the dot is allowed
58+
return Instant.ofEpochMilli(milliSeconds).atZone(ZoneOffset.UTC);
59+
}
60+
// scientific notation it is!
61+
if (inputs[1].contains("e")) {
62+
return Instant.ofEpochMilli(Double.valueOf(input).longValue()).atZone(ZoneOffset.UTC);
63+
}
64+
65+
if (inputs[1].length() > 6) {
66+
throw new DateTimeParseException("too much granularity after dot [" + input + "]", input, 0);
67+
}
68+
Long nanos = new BigDecimal(inputs[1]).movePointRight(6 - inputs[1].length()).longValueExact();
69+
if (milliSeconds < 0) {
70+
nanos = nanos * -1;
71+
}
72+
return Instant.ofEpochMilli(milliSeconds).plusNanos(nanos).atZone(ZoneOffset.UTC);
73+
} else {
74+
return Instant.ofEpochMilli(Long.valueOf(input)).atZone(ZoneOffset.UTC);
75+
}
5176
} catch (NumberFormatException e) {
52-
throw new DateTimeParseException("invalid number", input, 0, e);
77+
throw new DateTimeParseException("invalid number [" + input + "]", input, 0, e);
5378
}
5479
}
55-
5680
@Override
5781
public DateFormatter withZone(ZoneId zoneId) {
5882
if (ZoneOffset.UTC.equals(zoneId) == false) {

server/src/main/java/org/elasticsearch/common/time/EpochSecondsDateFormatter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,17 @@ public TemporalAccessor parse(String input) {
4747
// this is BWC compatible to joda time, nothing after the dot is allowed
4848
return Instant.ofEpochSecond(seconds, 0).atZone(ZoneOffset.UTC);
4949
}
50+
// scientific notation it is!
51+
if (inputs[1].contains("e")) {
52+
return Instant.ofEpochSecond(Double.valueOf(input).longValue()).atZone(ZoneOffset.UTC);
53+
}
5054
if (inputs[1].length() > 9) {
5155
throw new DateTimeParseException("too much granularity after dot [" + input + "]", input, 0);
5256
}
5357
Long nanos = new BigDecimal(inputs[1]).movePointRight(9 - inputs[1].length()).longValueExact();
58+
if (seconds < 0) {
59+
nanos = nanos * -1;
60+
}
5461
return Instant.ofEpochSecond(seconds, nanos).atZone(ZoneOffset.UTC);
5562
} else {
5663
return Instant.ofEpochSecond(Long.valueOf(input)).atZone(ZoneOffset.UTC);

server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@ public void testDuellingFormatsValidParsing() {
7777
assertSameDate("1", "epoch_second");
7878
assertSameDate("-1", "epoch_second");
7979
assertSameDate("-1522332219", "epoch_second");
80+
assertSameDate("1.0e3", "epoch_second");
8081
assertSameDate("1522332219321", "epoch_millis");
8182
assertSameDate("0", "epoch_millis");
8283
assertSameDate("1", "epoch_millis");
8384
assertSameDate("-1", "epoch_millis");
8485
assertSameDate("-1522332219321", "epoch_millis");
86+
assertSameDate("1.0e3", "epoch_millis");
8587

8688
assertSameDate("20181126", "basic_date");
8789
assertSameDate("20181126T121212.123Z", "basic_date_time");

server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,55 @@
3737

3838
public class DateFormattersTests extends ESTestCase {
3939

40+
// this is not in the duelling tests, because the epoch millis parser in joda time drops the milliseconds after the comma
41+
// but is able to parse the rest
42+
// as this feature is supported it also makes sense to make it exact
43+
public void testEpochMillisParser() {
44+
DateFormatter formatter = DateFormatters.forPattern("epoch_millis");
45+
{
46+
Instant instant = Instant.from(formatter.parse("12345.6789"));
47+
assertThat(instant.getEpochSecond(), is(12L));
48+
assertThat(instant.getNano(), is(345_678_900));
49+
}
50+
{
51+
Instant instant = Instant.from(formatter.parse("12345"));
52+
assertThat(instant.getEpochSecond(), is(12L));
53+
assertThat(instant.getNano(), is(345_000_000));
54+
}
55+
{
56+
Instant instant = Instant.from(formatter.parse("12345."));
57+
assertThat(instant.getEpochSecond(), is(12L));
58+
assertThat(instant.getNano(), is(345_000_000));
59+
}
60+
{
61+
Instant instant = Instant.from(formatter.parse("-12345.6789"));
62+
assertThat(instant.getEpochSecond(), is(-13L));
63+
assertThat(instant.getNano(), is(1_000_000_000 - 345_678_900));
64+
}
65+
{
66+
Instant instant = Instant.from(formatter.parse("-436134.241272"));
67+
assertThat(instant.getEpochSecond(), is(-437L));
68+
assertThat(instant.getNano(), is(1_000_000_000 - 134_241_272));
69+
}
70+
{
71+
Instant instant = Instant.from(formatter.parse("-12345"));
72+
assertThat(instant.getEpochSecond(), is(-13L));
73+
assertThat(instant.getNano(), is(1_000_000_000 - 345_000_000));
74+
}
75+
{
76+
Instant instant = Instant.from(formatter.parse("0"));
77+
assertThat(instant.getEpochSecond(), is(0L));
78+
assertThat(instant.getNano(), is(0));
79+
}
80+
}
81+
4082
public void testEpochMilliParser() {
4183
DateFormatter formatter = DateFormatters.forPattern("epoch_millis");
4284
DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("invalid"));
4385
assertThat(e.getMessage(), containsString("invalid number"));
86+
87+
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("123.1234567"));
88+
assertThat(e.getMessage(), containsString("too much granularity after dot [123.1234567]"));
4489
}
4590

4691
// 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 void testEpochSecondParser() {
61106
assertThat(Instant.from(formatter.parse("1234.1234567")).getNano(), is(123_456_700));
62107
assertThat(Instant.from(formatter.parse("1234.12345678")).getNano(), is(123_456_780));
63108
assertThat(Instant.from(formatter.parse("1234.123456789")).getNano(), is(123_456_789));
109+
110+
assertThat(Instant.from(formatter.parse("-1234.567")).toEpochMilli(), is(-1234567L));
111+
assertThat(Instant.from(formatter.parse("-1234")).getNano(), is(0));
112+
64113
DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.1234567890"));
65114
assertThat(e.getMessage(), is("too much granularity after dot [1234.1234567890]"));
66115
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.123456789013221"));

0 commit comments

Comments
 (0)