From 9f3da013d899f458ff56ded1a57fab4818f90e47 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Fri, 11 Jan 2019 09:25:05 +0100 Subject: [PATCH] Date/Time parsing: Use java time API instead of exception handling (#37222) * Add benchmark * Use java time API instead of exception handling when several formatters are used, the existing way of parsing those is to throw an exception catch it, and try the next one. This is is considerably slower than the approach taken in joda time, so that indexing is reduced when a date format like `x||y` is used and y is the date format being used. This commit now uses the java API to parse the date by appending the date time formatters to each other and does not rely on exception handling. * fix benchmark * fix tests by changing formatter, also expose printer * restore optional printing logic to fix tests * fix tests * incorporate review comments --- .../time/DateFormatterBenchmark.java | 58 +++++++ .../common/time/DateFormatter.java | 2 +- .../common/time/DateFormatters.java | 158 ++++-------------- .../common/time/JavaDateFormatter.java | 70 +++----- .../joda/JavaJodaTimeDuellingTests.java | 2 - .../common/time/DateFormattersTests.java | 47 ++---- 6 files changed, 129 insertions(+), 208 deletions(-) create mode 100644 benchmarks/src/main/java/org/elasticsearch/benchmark/time/DateFormatterBenchmark.java diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/DateFormatterBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/DateFormatterBenchmark.java new file mode 100644 index 0000000000000..b30b3ada0ab64 --- /dev/null +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/DateFormatterBenchmark.java @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.elasticsearch.benchmark.time; + +import org.elasticsearch.common.joda.Joda; +import org.elasticsearch.common.time.DateFormatter; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +import java.time.temporal.TemporalAccessor; +import java.util.concurrent.TimeUnit; + +@Fork(3) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@SuppressWarnings("unused") //invoked by benchmarking framework +public class DateFormatterBenchmark { + + private final DateFormatter javaFormatter = DateFormatter.forPattern("8year_month_day||ordinal_date||epoch_millis"); + private final DateFormatter jodaFormatter = Joda.forPattern("year_month_day||ordinal_date||epoch_millis"); + + @Benchmark + public TemporalAccessor parseJavaDate() { + return javaFormatter.parse("1234567890"); + } + + @Benchmark + public TemporalAccessor parseJodaDate() { + return jodaFormatter.parse("1234567890"); + } +} + diff --git a/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java index 49c5e7626072b..e89317ad288c0 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java @@ -145,7 +145,7 @@ static DateFormatter forPattern(String input) { if (formatters.size() == 1) { return formatters.get(0); } - return new DateFormatters.MergedDateFormatter(input, formatters); + return DateFormatters.merge(input, formatters); } } diff --git a/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java b/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java index d5a8f4b7a5112..d3bf5eb2a641c 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java @@ -19,7 +19,6 @@ package org.elasticsearch.common.time; -import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.Strings; import java.time.DateTimeException; @@ -31,7 +30,6 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.time.format.DateTimeParseException; import java.time.format.ResolverStyle; import java.time.format.SignStyle; import java.time.temporal.ChronoField; @@ -40,10 +38,9 @@ import java.time.temporal.TemporalAdjusters; import java.time.temporal.TemporalQueries; import java.time.temporal.WeekFields; -import java.util.Collections; +import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.stream.Collectors; import static java.time.temporal.ChronoField.DAY_OF_MONTH; import static java.time.temporal.ChronoField.DAY_OF_WEEK; @@ -77,21 +74,17 @@ public class DateFormatters { .appendValue(SECOND_OF_MINUTE, 2, 2, SignStyle.NOT_NEGATIVE) .toFormatter(Locale.ROOT); - private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_1 = new DateTimeFormatterBuilder() + private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_PRINTER = new DateTimeFormatterBuilder() .append(STRICT_YEAR_MONTH_DAY_FORMATTER) - .optionalStart() .appendLiteral('T') .append(STRICT_HOUR_MINUTE_SECOND_FORMATTER) - .optionalStart() - .appendFraction(MILLI_OF_SECOND, 3, 3, true) - .optionalEnd() + .appendFraction(NANO_OF_SECOND, 3, 9, true) .optionalStart() .appendZoneOrOffsetId() .optionalEnd() - .optionalEnd() .toFormatter(Locale.ROOT); - private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_2 = new DateTimeFormatterBuilder() + private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER = new DateTimeFormatterBuilder() .append(STRICT_YEAR_MONTH_DAY_FORMATTER) .optionalStart() .appendLiteral('T') @@ -100,7 +93,10 @@ public class DateFormatters { .appendFraction(MILLI_OF_SECOND, 3, 3, true) .optionalEnd() .optionalStart() - .appendOffset("+HHmm", "Z") + .appendZoneOrOffsetId() + .optionalEnd() + .optionalStart() + .append(TIME_ZONE_FORMATTER_NO_COLON) .optionalEnd() .optionalEnd() .toFormatter(Locale.ROOT); @@ -109,10 +105,9 @@ public class DateFormatters { * Returns a generic ISO datetime parser where the date is mandatory and the time is optional. */ private static final DateFormatter STRICT_DATE_OPTIONAL_TIME = - new JavaDateFormatter("strict_date_optional_time", STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, - STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_2); + new JavaDateFormatter("strict_date_optional_time", STRICT_DATE_OPTIONAL_TIME_PRINTER, STRICT_DATE_OPTIONAL_TIME_FORMATTER); - private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1 = new DateTimeFormatterBuilder() + private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS = new DateTimeFormatterBuilder() .append(STRICT_YEAR_MONTH_DAY_FORMATTER) .optionalStart() .appendLiteral('T') @@ -124,26 +119,17 @@ public class DateFormatters { .appendZoneOrOffsetId() .optionalEnd() .optionalStart() - .appendOffset("+HHmm", "Z") + .append(TIME_ZONE_FORMATTER_NO_COLON) .optionalEnd() .optionalEnd() .toFormatter(Locale.ROOT); - private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_PRINTER = new DateTimeFormatterBuilder() - .append(STRICT_YEAR_MONTH_DAY_FORMATTER) - .appendLiteral('T') - .append(STRICT_HOUR_MINUTE_SECOND_FORMATTER) - .appendFraction(NANO_OF_SECOND, 3, 9, true) - .optionalStart() - .appendZoneOrOffsetId() - .optionalEnd() - .toFormatter(Locale.ROOT); - /** * Returns a generic ISO datetime parser where the date is mandatory and the time is optional with nanosecond resolution. */ private static final DateFormatter STRICT_DATE_OPTIONAL_TIME_NANOS = new JavaDateFormatter("strict_date_optional_time_nanos", - STRICT_DATE_OPTIONAL_TIME_PRINTER, STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1); + STRICT_DATE_OPTIONAL_TIME_PRINTER, STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS); + ///////////////////////////////////////// // // BEGIN basic time formatters @@ -818,7 +804,7 @@ public class DateFormatters { * yyyy-MM-dd'T'HH:mm:ss.SSSZ */ private static final DateFormatter DATE_OPTIONAL_TIME = new JavaDateFormatter("date_optional_time", - STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, + STRICT_DATE_OPTIONAL_TIME_PRINTER, new DateTimeFormatterBuilder() .append(DATE_FORMATTER) .optionalStart() @@ -836,26 +822,6 @@ public class DateFormatters { .appendFraction(MILLI_OF_SECOND, 1, 3, true) .optionalEnd() .optionalStart().appendZoneOrOffsetId().optionalEnd() - .optionalEnd() - .optionalEnd() - .optionalEnd() - .toFormatter(Locale.ROOT), - new DateTimeFormatterBuilder() - .append(DATE_FORMATTER) - .optionalStart() - .appendLiteral('T') - .optionalStart() - .appendValue(HOUR_OF_DAY, 1, 2, SignStyle.NOT_NEGATIVE) - .optionalStart() - .appendLiteral(':') - .appendValue(MINUTE_OF_HOUR, 1, 2, SignStyle.NOT_NEGATIVE) - .optionalStart() - .appendLiteral(':') - .appendValue(SECOND_OF_MINUTE, 1, 2, SignStyle.NOT_NEGATIVE) - .optionalEnd() - .optionalStart() - .appendFraction(MILLI_OF_SECOND, 1, 3, true) - .optionalEnd() .optionalStart().appendOffset("+HHmm", "Z").optionalEnd() .optionalEnd() .optionalEnd() @@ -1006,7 +972,7 @@ public class DateFormatters { * (yyyy-MM-dd'T'HH:mm:ss.SSSZZ). */ private static final DateFormatter DATE_TIME = new JavaDateFormatter("date_time", - STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, + STRICT_DATE_OPTIONAL_TIME_PRINTER, new DateTimeFormatterBuilder().append(DATE_TIME_FORMATTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(DATE_TIME_FORMATTER).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) ); @@ -1483,90 +1449,22 @@ public static DateFormatter forPattern(String input) { } } - static class MergedDateFormatter implements DateFormatter { - - private final String pattern; - // package private for tests - final List formatters; - private final List dateMathParsers; - - MergedDateFormatter(String pattern, List formatters) { - assert formatters.size() > 0; - this.pattern = pattern; - this.formatters = Collections.unmodifiableList(formatters); - this.dateMathParsers = formatters.stream().map(DateFormatter::toDateMathParser).collect(Collectors.toList()); - } - - @Override - public TemporalAccessor parse(String input) { - IllegalArgumentException failure = null; - for (DateFormatter formatter : formatters) { - try { - return formatter.parse(input); - // TODO: remove DateTimeParseException when JavaDateFormatter throws IAE - } catch (IllegalArgumentException | DateTimeParseException e) { - if (failure == null) { - // wrap so the entire multi format is in the message - failure = new IllegalArgumentException("failed to parse date field [" + input + "] with format [" + pattern + "]", - e); - } else { - failure.addSuppressed(e); - } - } + static JavaDateFormatter merge(String pattern, List formatters) { + assert formatters.size() > 0; + + List dateTimeFormatters = new ArrayList<>(formatters.size()); + DateTimeFormatter printer = null; + for (DateFormatter formatter : formatters) { + assert formatter instanceof JavaDateFormatter; + JavaDateFormatter javaDateFormatter = (JavaDateFormatter) formatter; + DateTimeFormatter dateTimeFormatter = javaDateFormatter.getParser(); + if (printer == null) { + printer = javaDateFormatter.getPrinter(); } - throw failure; - } - - @Override - public DateFormatter withZone(ZoneId zoneId) { - return new MergedDateFormatter(pattern, formatters.stream().map(f -> f.withZone(zoneId)).collect(Collectors.toList())); - } - - @Override - public DateFormatter withLocale(Locale locale) { - return new MergedDateFormatter(pattern, formatters.stream().map(f -> f.withLocale(locale)).collect(Collectors.toList())); - } - - @Override - public String format(TemporalAccessor accessor) { - return formatters.get(0).format(accessor); + dateTimeFormatters.add(dateTimeFormatter); } - @Override - public String pattern() { - return pattern; - } - - @Override - public Locale locale() { - return formatters.get(0).locale(); - } - - @Override - public ZoneId zone() { - return formatters.get(0).zone(); - } - - @Override - public DateMathParser toDateMathParser() { - return (text, now, roundUp, tz) -> { - ElasticsearchParseException failure = null; - for (DateMathParser parser : dateMathParsers) { - try { - return parser.parse(text, now, roundUp, tz); - } catch (ElasticsearchParseException e) { - if (failure == null) { - // wrap so the entire multi format is in the message - failure = new ElasticsearchParseException("failed to parse date field [" + text + "] with format [" - + pattern + "]", e); - } else { - failure.addSuppressed(e); - } - } - } - throw failure; - }; - } + return new JavaDateFormatter(pattern, printer, dateTimeFormatters.toArray(new DateTimeFormatter[0])); } private static final ZonedDateTime EPOCH_ZONED_DATE_TIME = Instant.EPOCH.atZone(ZoneOffset.UTC); diff --git a/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java index 68e2cfd4fe317..0fce14b764ef1 100644 --- a/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java @@ -19,10 +19,11 @@ package org.elasticsearch.common.time; +import org.elasticsearch.common.Strings; + import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.time.format.DateTimeParseException; import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalField; @@ -47,7 +48,7 @@ class JavaDateFormatter implements DateFormatter { private final String format; private final DateTimeFormatter printer; - private final DateTimeFormatter[] parsers; + private final DateTimeFormatter parser; JavaDateFormatter(String format, DateTimeFormatter printer, DateTimeFormatter... parsers) { if (printer == null) { @@ -62,61 +63,54 @@ class JavaDateFormatter implements DateFormatter { throw new IllegalArgumentException("formatters must have the same locale"); } if (parsers.length == 0) { - this.parsers = new DateTimeFormatter[]{printer}; + this.parser = printer; + } else if (parsers.length == 1) { + this.parser = parsers[0]; } else { - this.parsers = parsers; + DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); + for (DateTimeFormatter parser : parsers) { + builder.appendOptional(parser); + } + this.parser = builder.toFormatter(Locale.ROOT); } this.format = format; this.printer = printer; } + DateTimeFormatter getParser() { + return parser; + } + + DateTimeFormatter getPrinter() { + return printer; + } + @Override public TemporalAccessor parse(String input) { - DateTimeParseException failure = null; - for (int i = 0; i < parsers.length; i++) { - try { - return parsers[i].parse(input); - } catch (DateTimeParseException e) { - if (failure == null) { - failure = e; - } else { - failure.addSuppressed(e); - } - } + if (Strings.isNullOrEmpty(input)) { + throw new IllegalArgumentException("cannot parse empty date"); } - - // ensure that all parsers exceptions are returned instead of only the last one - throw failure; + return parser.parse(input); } @Override public DateFormatter withZone(ZoneId zoneId) { // shortcurt to not create new objects unnecessarily - if (zoneId.equals(parsers[0].getZone())) { + if (zoneId.equals(parser.getZone())) { return this; } - final DateTimeFormatter[] parsersWithZone = new DateTimeFormatter[parsers.length]; - for (int i = 0; i < parsers.length; i++) { - parsersWithZone[i] = parsers[i].withZone(zoneId); - } - - return new JavaDateFormatter(format, printer.withZone(zoneId), parsersWithZone); + return new JavaDateFormatter(format, printer.withZone(zoneId), parser.withZone(zoneId)); } @Override public DateFormatter withLocale(Locale locale) { // shortcurt to not create new objects unnecessarily - if (locale.equals(parsers[0].getLocale())) { + if (locale.equals(parser.getLocale())) { return this; } - final DateTimeFormatter[] parsersWithZone = new DateTimeFormatter[parsers.length]; - for (int i = 0; i < parsers.length; i++) { - parsersWithZone[i] = parsers[i].withLocale(locale); - } - - return new JavaDateFormatter(format, printer.withLocale(locale), parsersWithZone); + return new JavaDateFormatter(format, printer.withLocale(locale), parser.withLocale(locale)); } @Override @@ -132,17 +126,7 @@ public String pattern() { JavaDateFormatter parseDefaulting(Map fields) { final DateTimeFormatterBuilder parseDefaultingBuilder = new DateTimeFormatterBuilder().append(printer); fields.forEach(parseDefaultingBuilder::parseDefaulting); - if (parsers.length == 1 && parsers[0].equals(printer)) { - return new JavaDateFormatter(format, parseDefaultingBuilder.toFormatter(Locale.ROOT)); - } else { - final DateTimeFormatter[] parsersWithDefaulting = new DateTimeFormatter[parsers.length]; - for (int i = 0; i < parsers.length; i++) { - DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder().append(parsers[i]); - fields.forEach(builder::parseDefaulting); - parsersWithDefaulting[i] = builder.toFormatter(Locale.ROOT); - } - return new JavaDateFormatter(format, parseDefaultingBuilder.toFormatter(Locale.ROOT), parsersWithDefaulting); - } + return new JavaDateFormatter(format, parseDefaultingBuilder.toFormatter(Locale.ROOT)); } @Override 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 a96fee5b6333e..b2370dadb604c 100644 --- a/server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java +++ b/server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java @@ -72,8 +72,6 @@ public void testCustomTimeFormats() { public void testDuellingFormatsValidParsing() { assertSameDate("1522332219", "epoch_second"); - assertSameDate("1522332219.", "epoch_second"); - assertSameDate("1522332219.0", "epoch_second"); assertSameDate("0", "epoch_second"); assertSameDate("1", "epoch_second"); assertSameDate("1522332219321", "epoch_millis"); 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 8d79f9d3600e7..a2858284593d1 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java @@ -23,6 +23,7 @@ import java.time.Instant; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; import java.time.temporal.TemporalAccessor; import java.util.Locale; @@ -42,21 +43,11 @@ public class DateFormattersTests extends ESTestCase { // 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("0")); assertThat(instant.getEpochSecond(), is(0L)); @@ -79,25 +70,12 @@ public void testEpochMilliParser() { public void testEpochSecondParser() { DateFormatter formatter = DateFormatters.forPattern("epoch_second"); - assertThat(Instant.from(formatter.parse("1234.567")).toEpochMilli(), is(1234567L)); - assertThat(Instant.from(formatter.parse("1234.")).getNano(), is(0)); - assertThat(Instant.from(formatter.parse("1234.")).getEpochSecond(), is(1234L)); - assertThat(Instant.from(formatter.parse("1234.1")).getNano(), is(100_000_000)); - assertThat(Instant.from(formatter.parse("1234.12")).getNano(), is(120_000_000)); - assertThat(Instant.from(formatter.parse("1234.123")).getNano(), is(123_000_000)); - assertThat(Instant.from(formatter.parse("1234.1234")).getNano(), is(123_400_000)); - assertThat(Instant.from(formatter.parse("1234.12345")).getNano(), is(123_450_000)); - assertThat(Instant.from(formatter.parse("1234.123456")).getNano(), is(123_456_000)); - 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)); - - DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.1234567890")); - assertThat(e.getMessage(), is("Text '1234.1234567890' could not be parsed, unparsed text found at index 4")); - e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.123456789013221")); - assertThat(e.getMessage(), is("Text '1234.123456789013221' could not be parsed, unparsed text found at index 4")); + DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.1")); + assertThat(e.getMessage(), is("Text '1234.1' could not be parsed, unparsed text found at index 4")); + e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.")); + assertThat(e.getMessage(), is("Text '1234.' could not be parsed, unparsed text found at index 4")); e = expectThrows(DateTimeParseException.class, () -> formatter.parse("abc")); - assertThat(e.getMessage(), is("Text 'abc' could not be parsed at index 0")); + assertThat(e.getMessage(), is("Text 'abc' could not be parsed, unparsed text found at index 0")); e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.abc")); assertThat(e.getMessage(), is("Text '1234.abc' could not be parsed, unparsed text found at index 4")); } @@ -109,6 +87,14 @@ public void testEpochMilliParsersWithDifferentFormatters() { assertThat(formatter.pattern(), is("strict_date_optional_time||epoch_millis")); } + public void testParsersWithMultipleInternalFormats() throws Exception { + ZonedDateTime first = DateFormatters.toZonedDateTime( + DateFormatters.forPattern("strict_date_optional_time_nanos").parse("2018-05-15T17:14:56+0100")); + ZonedDateTime second = DateFormatters.toZonedDateTime( + DateFormatters.forPattern("strict_date_optional_time_nanos").parse("2018-05-15T17:14:56+01:00")); + assertThat(first, is(second)); + } + public void testLocales() { assertThat(DateFormatters.forPattern("strict_date_optional_time").locale(), is(Locale.ROOT)); Locale locale = randomLocale(random()); @@ -157,10 +143,7 @@ public void testForceJava8() { assertThat(DateFormatter.forPattern("8date_optional_time"), instanceOf(JavaDateFormatter.class)); // named formats too DateFormatter formatter = DateFormatter.forPattern("8date_optional_time||ww-MM-dd"); - assertThat(formatter, instanceOf(DateFormatters.MergedDateFormatter.class)); - DateFormatters.MergedDateFormatter mergedFormatter = (DateFormatters.MergedDateFormatter) formatter; - assertThat(mergedFormatter.formatters.get(0), instanceOf(JavaDateFormatter.class)); - assertThat(mergedFormatter.formatters.get(1), instanceOf(JavaDateFormatter.class)); + assertThat(formatter, instanceOf(JavaDateFormatter.class)); } public void testParsingStrictNanoDates() {