From c0799ec64379f36501638ba3568894d949c66a78 Mon Sep 17 00:00:00 2001 From: Ryan Kophs Date: Wed, 23 Mar 2022 20:31:39 -0400 Subject: [PATCH] Add support for negtive epoch timestamps (#80208) * support negative epoch_timestamps ES 7.0 removed support for negative timestamps in date parsing. This was due to internally migrating to Java 8's datetime classes, in particular the representation of a date as a set of integer fields, Instant. This change adds back the support for negative epoch timestamps, with both epoch millis and epoch seconds parsers. The implementation separates parsing of negative values into a separate Instant field, holding only the "negative". By having this separate from the seconds portion of the timestamp, the negation can be held onto, even when the seconds or milliseconds are 0, where Java's parser would have thrown away the negative. Co-authored-by: Ryan Kophs --- docs/changelog/80208.yaml | 8 + .../elasticsearch/common/time/EpochTime.java | 151 ++++++++++-- .../common/time/DateFormattersTests.java | 227 +++++++++++++++++- .../index/mapper/DateFieldMapperTests.java | 5 - .../search/DocValueFormatTests.java | 4 +- 5 files changed, 362 insertions(+), 33 deletions(-) create mode 100644 docs/changelog/80208.yaml diff --git a/docs/changelog/80208.yaml b/docs/changelog/80208.yaml new file mode 100644 index 0000000000000..f567b2b1533dc --- /dev/null +++ b/docs/changelog/80208.yaml @@ -0,0 +1,8 @@ +pr: 80208 +summary: Add support for negtive epoch timestamps +area: Infra/Core +type: enhancement +issues: + - 79135 + - 72123 + - 40983 diff --git a/server/src/main/java/org/elasticsearch/common/time/EpochTime.java b/server/src/main/java/org/elasticsearch/common/time/EpochTime.java index ff107dd9e0612..2bb3cde3ab9ef 100644 --- a/server/src/main/java/org/elasticsearch/common/time/EpochTime.java +++ b/server/src/main/java/org/elasticsearch/common/time/EpochTime.java @@ -28,13 +28,31 @@ * The seconds formatter is provided by {@link #SECONDS_FORMATTER}. * The milliseconds formatter is provided by {@link #MILLIS_FORMATTER}. *

- * Both formatters support fractional time, up to nanosecond precision. Values must be positive numbers. + * Both formatters support fractional time, up to nanosecond precision. */ class EpochTime { - private static final ValueRange LONG_POSITIVE_RANGE = ValueRange.of(0, Long.MAX_VALUE); + private static final ValueRange POSITIVE_LONG_INTEGER_RANGE = ValueRange.of(0, Long.MAX_VALUE); - private static final EpochField SECONDS = new EpochField(ChronoUnit.SECONDS, ChronoUnit.FOREVER, LONG_POSITIVE_RANGE) { + // TemporalField is only present in the presence of a negative (potentially fractional) timestamp. + private static final long NEGATIVE_SIGN_PLACEHOLDER = -1; + private static final EpochField NEGATIVE_SIGN_FIELD = new EpochField( + ChronoUnit.FOREVER, + ChronoUnit.FOREVER, + ValueRange.of(NEGATIVE_SIGN_PLACEHOLDER, NEGATIVE_SIGN_PLACEHOLDER) + ) { + @Override + public boolean isSupportedBy(TemporalAccessor temporal) { + return temporal.isSupported(ChronoField.INSTANT_SECONDS) && temporal.getLong(ChronoField.INSTANT_SECONDS) < 0; + } + + @Override + public long getFrom(TemporalAccessor temporal) { + return NEGATIVE_SIGN_PLACEHOLDER; + } + }; + + private static final EpochField UNSIGNED_SECONDS = new EpochField(ChronoUnit.SECONDS, ChronoUnit.FOREVER, POSITIVE_LONG_INTEGER_RANGE) { @Override public boolean isSupportedBy(TemporalAccessor temporal) { return temporal.isSupported(ChronoField.INSTANT_SECONDS); @@ -42,7 +60,22 @@ public boolean isSupportedBy(TemporalAccessor temporal) { @Override public long getFrom(TemporalAccessor temporal) { - return temporal.getLong(ChronoField.INSTANT_SECONDS); + long seconds = temporal.getLong(ChronoField.INSTANT_SECONDS); + if (seconds >= 0) { + return seconds; + } else { + long nanos = temporal.getLong(ChronoField.NANO_OF_SECOND); + if (nanos != 0) { + // Fractional negative timestamp. + // This increases the seconds magnitude by 1 in the formatted value due to a rounding error when + // the nanos value is not evenly converted into a seconds value. Java 8's date-time API represents + // values as a negative seconds value + a positive nanos value. However, in this case there is + // precision loss. Thus, to account for this precision loss, we must use a seconds value that is + // rounded towards the epoch (i.e. to a higher magnitude). + seconds += 1; + } + return -seconds; // positive for formatting; sign handled by NEGATIVE_SIGN_FIELD + } } @Override @@ -51,9 +84,21 @@ public TemporalAccessor resolve( TemporalAccessor partialTemporal, ResolverStyle resolverStyle ) { + Long isNegative = fieldValues.remove(NEGATIVE_SIGN_FIELD); long seconds = fieldValues.remove(this); - fieldValues.put(ChronoField.INSTANT_SECONDS, seconds); Long nanos = fieldValues.remove(NANOS_OF_SECOND); + if (isNegative != null) { + seconds = -seconds; + if (nanos != null) { + // nanos must be positive. B/c the timestamp is represented by the + // (seconds, nanos) tuple, seconds moves 1s toward negative-infinity + // and nanos moves 1s toward positive-infinity + seconds -= 1; + nanos = 1_000_000_000 - nanos; + } + } + + fieldValues.put(ChronoField.INSTANT_SECONDS, seconds); if (nanos != null) { fieldValues.put(ChronoField.NANO_OF_SECOND, nanos); } @@ -69,19 +114,40 @@ public boolean isSupportedBy(TemporalAccessor temporal) { @Override public long getFrom(TemporalAccessor temporal) { - return temporal.getLong(ChronoField.NANO_OF_SECOND); + if (temporal.getLong(ChronoField.INSTANT_SECONDS) < 0) { + return (1_000_000_000 - temporal.getLong(ChronoField.NANO_OF_SECOND)) % 1_000_000_000; + } else { + return temporal.getLong(ChronoField.NANO_OF_SECOND); + } } }; - private static final EpochField MILLIS = new EpochField(ChronoUnit.MILLIS, ChronoUnit.FOREVER, LONG_POSITIVE_RANGE) { + private static final EpochField UNSIGNED_MILLIS = new EpochField(ChronoUnit.MILLIS, ChronoUnit.FOREVER, POSITIVE_LONG_INTEGER_RANGE) { @Override public boolean isSupportedBy(TemporalAccessor temporal) { - return temporal.isSupported(ChronoField.INSTANT_SECONDS) && temporal.isSupported(ChronoField.MILLI_OF_SECOND); + return temporal.isSupported(ChronoField.INSTANT_SECONDS) + && (temporal.isSupported(ChronoField.NANO_OF_SECOND) || temporal.isSupported(ChronoField.MILLI_OF_SECOND)); } @Override public long getFrom(TemporalAccessor temporal) { - return temporal.getLong(ChronoField.INSTANT_SECONDS) * 1_000 + temporal.getLong(ChronoField.MILLI_OF_SECOND); + long millis = temporal.getLong(ChronoField.INSTANT_SECONDS) * 1_000; + if (millis >= 0 || temporal.isSupported(ChronoField.NANO_OF_SECOND) == false) { + return millis + temporal.getLong(ChronoField.MILLI_OF_SECOND); + } else { + long nanos = temporal.getLong(ChronoField.NANO_OF_SECOND); + if (nanos % 1_000_000 != 0) { + // Fractional negative timestamp. + // This increases the millis magnitude by 1 in the formatted value due to a rounding error when + // the nanos value is not evenly converted into a millis value. Java 8's date-time API represents + // values as a negative seconds value + a positive nanos value. However, in this case there is + // precision loss when converting the nanos to millis. Thus, to account for this precision loss, + // we must use a millis value that is rounded towards the epoch (i.e. to a higher magnitude). + millis += 1; + } + millis += (nanos / 1_000_000); + return -millis; // positive for formatting; sign handled by NEGATIVE_SIGN_FIELD + } } @Override @@ -90,13 +156,38 @@ public TemporalAccessor resolve( TemporalAccessor partialTemporal, ResolverStyle resolverStyle ) { - long secondsAndMillis = fieldValues.remove(this); - long seconds = secondsAndMillis / 1_000; - long nanos = secondsAndMillis % 1000 * 1_000_000; + Long isNegative = fieldValues.remove(NEGATIVE_SIGN_FIELD); Long nanosOfMilli = fieldValues.remove(NANOS_OF_MILLI); - if (nanosOfMilli != null) { - nanos += nanosOfMilli; + long secondsAndMillis = fieldValues.remove(this); + + long seconds; + long nanos; + if (isNegative != null) { + secondsAndMillis = -secondsAndMillis; + seconds = secondsAndMillis / 1_000; + nanos = secondsAndMillis % 1000 * 1_000_000; + // `secondsAndMillis < 0` implies negative timestamp; so `nanos < 0` + if (nanosOfMilli != null) { + // aggregate fractional part of the input; subtract b/c `nanos < 0` + nanos -= nanosOfMilli; + } + if (nanos != 0) { + // nanos must be positive. B/c the timestamp is represented by the + // (seconds, nanos) tuple, seconds moves 1s toward negative-infinity + // and nanos moves 1s toward positive-infinity + seconds -= 1; + nanos = 1_000_000_000 + nanos; + } + } else { + seconds = secondsAndMillis / 1_000; + nanos = secondsAndMillis % 1000 * 1_000_000; + + if (nanosOfMilli != null) { + // aggregate fractional part of the input + nanos += nanosOfMilli; + } } + fieldValues.put(ChronoField.INSTANT_SECONDS, seconds); fieldValues.put(ChronoField.NANO_OF_SECOND, nanos); // if there is already a milli of second, we need to overwrite it @@ -117,29 +208,41 @@ public boolean isSupportedBy(TemporalAccessor temporal) { @Override public long getFrom(TemporalAccessor temporal) { - return temporal.getLong(ChronoField.NANO_OF_SECOND) % 1_000_000; + if (temporal.getLong(ChronoField.INSTANT_SECONDS) < 0) { + return (1_000_000_000 - temporal.getLong(ChronoField.NANO_OF_SECOND)) % 1_000_000; + } else { + return temporal.getLong(ChronoField.NANO_OF_SECOND) % 1_000_000; + } } }; // this supports seconds without any fraction - private static final DateTimeFormatter SECONDS_FORMATTER1 = new DateTimeFormatterBuilder().appendValue(SECONDS, 1, 19, SignStyle.NORMAL) + private static final DateTimeFormatter SECONDS_FORMATTER1 = new DateTimeFormatterBuilder().optionalStart() + .appendText(NEGATIVE_SIGN_FIELD, Map.of(-1L, "-")) // field is only created in the presence of a '-' char. + .optionalEnd() + .appendValue(UNSIGNED_SECONDS, 1, 19, SignStyle.NOT_NEGATIVE) .optionalStart() // optional is used so isSupported will be called when printing .appendFraction(NANOS_OF_SECOND, 0, 9, true) .optionalEnd() .toFormatter(Locale.ROOT); // this supports seconds ending in dot - private static final DateTimeFormatter SECONDS_FORMATTER2 = new DateTimeFormatterBuilder().appendValue(SECONDS, 1, 19, SignStyle.NORMAL) + private static final DateTimeFormatter SECONDS_FORMATTER2 = new DateTimeFormatterBuilder().optionalStart() + .appendText(NEGATIVE_SIGN_FIELD, Map.of(-1L, "-")) // field is only created in the presence of a '-' char. + .optionalEnd() + .appendValue(UNSIGNED_SECONDS, 1, 19, SignStyle.NOT_NEGATIVE) .appendLiteral('.') .toFormatter(Locale.ROOT); - // this supports milliseconds without any fraction - private static final DateTimeFormatter MILLISECONDS_FORMATTER1 = new DateTimeFormatterBuilder().appendValue( - MILLIS, - 1, - 19, - SignStyle.NORMAL - ).optionalStart().appendFraction(NANOS_OF_MILLI, 0, 6, true).optionalEnd().toFormatter(Locale.ROOT); + // this supports milliseconds + public static final DateTimeFormatter MILLISECONDS_FORMATTER1 = new DateTimeFormatterBuilder().optionalStart() + .appendText(NEGATIVE_SIGN_FIELD, Map.of(-1L, "-")) // field is only created in the presence of a '-' char. + .optionalEnd() + .appendValue(UNSIGNED_MILLIS, 1, 19, SignStyle.NOT_NEGATIVE) + .optionalStart() + .appendFraction(NANOS_OF_MILLI, 0, 6, true) + .optionalEnd() + .toFormatter(Locale.ROOT); // this supports milliseconds ending in dot private static final DateTimeFormatter MILLISECONDS_FORMATTER2 = new DateTimeFormatterBuilder().append(MILLISECONDS_FORMATTER1) 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 df323e6bc625c..46cd145359344 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java @@ -92,16 +92,127 @@ public void testEpochMillisParser() { Instant instant = Instant.from(formatter.parse("12345")); assertThat(instant.getEpochSecond(), is(12L)); assertThat(instant.getNano(), is(345_000_000)); + assertThat(formatter.format(instant), is("12345")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); } { Instant instant = Instant.from(formatter.parse("0")); assertThat(instant.getEpochSecond(), is(0L)); assertThat(instant.getNano(), is(0)); + assertThat(formatter.format(instant), is("0")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("0.1")); + assertThat(instant.getEpochSecond(), is(0L)); + assertThat(instant.getNano(), is(100_000)); + assertThat(formatter.format(instant), is("0.1")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); } { Instant instant = Instant.from(formatter.parse("123.123456")); assertThat(instant.getEpochSecond(), is(0L)); assertThat(instant.getNano(), is(123123456)); + assertThat(formatter.format(instant), is("123.123456")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-123.123456")); + assertThat(instant.getEpochSecond(), is(-1L)); + assertThat(instant.getNano(), is(876876544)); + assertThat(formatter.format(instant), is("-123.123456")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-6789123.123456")); + assertThat(instant.getEpochSecond(), is(-6790L)); + assertThat(instant.getNano(), is(876876544)); + assertThat(formatter.format(instant), is("-6789123.123456")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("6789123.123456")); + assertThat(instant.getEpochSecond(), is(6789L)); + assertThat(instant.getNano(), is(123123456)); + assertThat(formatter.format(instant), is("6789123.123456")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-6250000430768.25")); + assertThat(instant.getEpochSecond(), is(-6250000431L)); + assertThat(instant.getNano(), is(231750000)); + assertThat(formatter.format(instant), is("-6250000430768.25")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-6250000430768.75")); + assertThat(instant.getEpochSecond(), is(-6250000431L)); + assertThat(instant.getNano(), is(231250000)); + assertThat(formatter.format(instant), is("-6250000430768.75")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-6250000430768.00")); + assertThat(instant.getEpochSecond(), is(-6250000431L)); + assertThat(instant.getNano(), is(232000000)); + assertThat(formatter.format(instant), is("-6250000430768")); // remove .00 precision + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-6250000431000.250000")); + assertThat(instant.getEpochSecond(), is(-6250000432L)); + assertThat(instant.getNano(), is(999750000)); + assertThat(formatter.format(instant), is("-6250000431000.25")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-6250000431000.000001")); + assertThat(instant.getEpochSecond(), is(-6250000432L)); + assertThat(instant.getNano(), is(999999999)); + assertThat(formatter.format(instant), is("-6250000431000.000001")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-6250000431000.75")); + assertThat(instant.getEpochSecond(), is(-6250000432L)); + assertThat(instant.getNano(), is(999250000)); + assertThat(formatter.format(instant), is("-6250000431000.75")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-6250000431000.00")); + assertThat(instant.getEpochSecond(), is(-6250000431L)); + assertThat(instant.getNano(), is(0)); + assertThat(formatter.format(instant), is("-6250000431000")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-6250000431000")); + assertThat(instant.getEpochSecond(), is(-6250000431L)); + assertThat(instant.getNano(), is(0)); + assertThat(formatter.format(instant), is("-6250000431000")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-6250000430768")); + assertThat(instant.getEpochSecond(), is(-6250000431L)); + assertThat(instant.getNano(), is(232000000)); + assertThat(formatter.format(instant), is("-6250000430768")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("1680000430768")); + assertThat(instant.getEpochSecond(), is(1680000430L)); + assertThat(instant.getNano(), is(768000000)); + assertThat(formatter.format(instant), is("1680000430768")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + } + { + Instant instant = Instant.from(formatter.parse("-0.12345")); + assertThat(instant.getEpochSecond(), is(-1L)); + assertThat(instant.getNano(), is(999876550)); + assertThat(formatter.format(instant), is("-0.12345")); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); } } @@ -132,12 +243,68 @@ public void testEpochSecondParserWithFraction() { TemporalAccessor accessor = formatter.parse("1234.1"); Instant instant = DateFormatters.from(accessor).toInstant(); assertThat(instant.getEpochSecond(), is(1234L)); - assertThat(DateFormatters.from(accessor).toInstant().getNano(), is(100_000_000)); + assertThat(instant.getNano(), is(100_000_000)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); accessor = formatter.parse("1234"); instant = DateFormatters.from(accessor).toInstant(); assertThat(instant.getEpochSecond(), is(1234L)); assertThat(instant.getNano(), is(0)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + + accessor = formatter.parse("1234.890"); + instant = DateFormatters.from(accessor).toInstant(); + assertThat(instant.getEpochSecond(), is(1234L)); + assertThat(instant.getNano(), is(890_000_000)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + + accessor = formatter.parse("0.1"); + instant = DateFormatters.from(accessor).toInstant(); + assertThat(instant.getEpochSecond(), is(0L)); + assertThat(instant.getNano(), is(100_000_000)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + + accessor = formatter.parse("0.890"); + instant = DateFormatters.from(accessor).toInstant(); + assertThat(instant.getEpochSecond(), is(0L)); + assertThat(instant.getNano(), is(890_000_000)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + + accessor = formatter.parse("0"); + instant = DateFormatters.from(accessor).toInstant(); + assertThat(instant.getEpochSecond(), is(0L)); + assertThat(instant.getNano(), is(0)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + + accessor = formatter.parse("-1234.1"); + instant = DateFormatters.from(accessor).toInstant(); + assertThat(instant.getEpochSecond(), is(-1235L)); + assertThat(instant.getNano(), is(900_000_000)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + + accessor = formatter.parse("-1234"); + instant = DateFormatters.from(accessor).toInstant(); + assertThat(instant.getEpochSecond(), is(-1234L)); + assertThat(instant.getNano(), is(0)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + + accessor = formatter.parse("-1234.890"); + instant = DateFormatters.from(accessor).toInstant(); + assertThat(instant.getEpochSecond(), is(-1235L)); + assertThat(instant.getNano(), is(110_000_000)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + + accessor = formatter.parse("-0.1"); + instant = DateFormatters.from(accessor).toInstant(); + assertThat(instant.getEpochSecond(), is(-1L)); + assertThat(instant.getNano(), is(900_000_000)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); + + accessor = formatter.parse("-0.890"); + instant = DateFormatters.from(accessor).toInstant(); + assertThat(instant.getEpochSecond(), is(-1L)); + assertThat(instant.getNano(), is(110_000_000)); + assertThat(Instant.from(formatter.parse(formatter.format(instant))), is(instant)); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> formatter.parse("abc")); assertThat(e.getMessage(), is("failed to parse date field [abc] with format [epoch_second]")); @@ -229,7 +396,7 @@ public void testSupportBackwardsJava8Format() { assertThat(formatter, instanceOf(JavaDateFormatter.class)); } - public void testEpochFormatting() { + public void testEpochFormattingPositiveEpoch() { long seconds = randomLongBetween(0, 130L * 365 * 86400); // from 1970 epoch till around 2100 long nanos = randomLongBetween(0, 999_999_999L); Instant instant = Instant.ofEpochSecond(seconds, nanos); @@ -249,6 +416,62 @@ public void testEpochFormatting() { assertThat(secondsFormatter.format(Instant.ofEpochSecond(42, 0)), is("42")); } + public void testEpochFormattingNegativeEpoch() { + long seconds = randomLongBetween(-130L * 365 * 86400, 0); // around 1840 till 1970 epoch + long nanos = randomLongBetween(0, 999_999_999L); + Instant instant = Instant.ofEpochSecond(seconds, nanos); + + DateFormatter millisFormatter = DateFormatter.forPattern("epoch_millis"); + String millis = millisFormatter.format(instant); + Instant millisInstant = Instant.from(millisFormatter.parse(millis)); + assertThat(millisInstant.toEpochMilli(), is(instant.toEpochMilli())); + assertThat(millisFormatter.format(Instant.ofEpochSecond(-42, 0)), is("-42000")); + assertThat(millisFormatter.format(Instant.ofEpochSecond(-42, 123456789L)), is("-41876.543211")); + + DateFormatter secondsFormatter = DateFormatter.forPattern("epoch_second"); + String formattedSeconds = secondsFormatter.format(instant); + Instant secondsInstant = Instant.from(secondsFormatter.parse(formattedSeconds)); + assertThat(secondsInstant.getEpochSecond(), is(instant.getEpochSecond())); + + assertThat(secondsFormatter.format(Instant.ofEpochSecond(42, 0)), is("42")); + } + + public void testEpochAndIso8601RoundTripNegative() { + long seconds = randomLongBetween(-130L * 365 * 86400, 0); // around 1840 till 1970 epoch + long nanos = randomLongBetween(0, 999_999_999L); + Instant instant = Instant.ofEpochSecond(seconds, nanos); + + DateFormatter isoFormatter = DateFormatters.forPattern("strict_date_optional_time_nanos"); + DateFormatter millisFormatter = DateFormatter.forPattern("epoch_millis"); + String millis = millisFormatter.format(instant); + String iso8601 = isoFormatter.format(instant); + + Instant millisInstant = Instant.from(millisFormatter.parse(millis)); + Instant isoInstant = Instant.from(isoFormatter.parse(iso8601)); + + assertThat(millisInstant.toEpochMilli(), is(isoInstant.toEpochMilli())); + assertThat(millisInstant.getEpochSecond(), is(isoInstant.getEpochSecond())); + assertThat(millisInstant.getNano(), is(isoInstant.getNano())); + } + + public void testEpochAndIso8601RoundTripPositive() { + long seconds = randomLongBetween(0, 130L * 365 * 86400); // from 1970 epoch till around 2100 + long nanos = randomLongBetween(0, 999_999_999L); + Instant instant = Instant.ofEpochSecond(seconds, nanos); + + DateFormatter isoFormatter = DateFormatters.forPattern("strict_date_optional_time_nanos"); + DateFormatter millisFormatter = DateFormatter.forPattern("epoch_millis"); + String millis = millisFormatter.format(instant); + String iso8601 = isoFormatter.format(instant); + + Instant millisInstant = Instant.from(millisFormatter.parse(millis)); + Instant isoInstant = Instant.from(isoFormatter.parse(iso8601)); + + assertThat(millisInstant.toEpochMilli(), is(isoInstant.toEpochMilli())); + assertThat(millisInstant.getEpochSecond(), is(isoInstant.getEpochSecond())); + assertThat(millisInstant.getNano(), is(isoInstant.getNano())); + } + public void testParsingStrictNanoDates() { DateFormatter formatter = DateFormatters.forPattern("strict_date_optional_time_nanos"); formatter.format(formatter.parse("2016-01-01T00:00:00.000")); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java index bb8492f73cfcb..7ad83ef301b9e 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java @@ -131,11 +131,6 @@ public void testIgnoreMalformed() throws IOException { "failed to parse date field [2016-03-99] with format [strict_date_optional_time||epoch_millis]", "strict_date_optional_time||epoch_millis" ); - testIgnoreMalformedForValue( - "-2147483648", - "failed to parse date field [-2147483648] with format [strict_date_optional_time||epoch_millis]", - "strict_date_optional_time||epoch_millis" - ); testIgnoreMalformedForValue("-522000000", "long overflow", "date_optional_time"); } diff --git a/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java b/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java index 76c32556c1b3f..742119d77556a 100644 --- a/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java +++ b/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java @@ -267,7 +267,7 @@ public void testParseEpochSecondsTimezone() { zone, Resolution.MILLISECONDS ); - long millis = randomNonNegativeLong(); + long millis = randomLong(); // Convert to seconds millis -= (millis % 1000); assertEquals( @@ -284,7 +284,7 @@ public void testParseEpochMillisTimezone() { zone, Resolution.MILLISECONDS ); - long millis = randomNonNegativeLong(); + long millis = randomLong(); assertEquals( "failed formatting for tz " + zone, millis,