From 75df484f07f412a7b4cda2abff3ca7f43dadd9a3 Mon Sep 17 00:00:00 2001 From: Przemyslaw Gomulka Date: Tue, 4 Oct 2022 14:10:31 +0200 Subject: [PATCH] [7.17] Fix date rounding for date math parsing backport(#90458) (#90635) * Fix date rounding for date math parsing (#90458) in #89693 the rounding logic was only applied when a field was present on a pattern. This is incorrect as for dates like "2020" we want to default to "2020-01-01T23:59:59.999..." when rounding is enabled. This commit always applies monthOfYear or dayofMonth defaulting (when rounding enabled) except when the dayOfYear is set closes #90187 backport(#90458) (cherry picked from commit 3f3a95e2dc2ca3c6e6a546a1c4787581ca0c316b) --- docs/changelog/90458.yaml | 6 + .../test/search/500_date_range.yml | 121 ++++++++++++++++++ .../common/time/JavaDateFormatter.java | 12 +- .../common/time/DateFormattersTests.java | 20 +++ 4 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 docs/changelog/90458.yaml create mode 100644 rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml diff --git a/docs/changelog/90458.yaml b/docs/changelog/90458.yaml new file mode 100644 index 0000000000000..e3b69e9ef327a --- /dev/null +++ b/docs/changelog/90458.yaml @@ -0,0 +1,6 @@ +pr: 90458 +summary: Fix date rounding for date math parsing +area: Infra/Core +type: bug +issues: + - 90187 diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml new file mode 100644 index 0000000000000..9ffb697348576 --- /dev/null +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search/500_date_range.yml @@ -0,0 +1,121 @@ +setup: + - skip: + version: " - 8.5.99" + reason: awaits backports + - do: + indices.create: + index: dates_year_only + body: + mappings: + properties: + date: + type: date + format: uuuu + + - do: + bulk: + refresh: true + body: + - '{ "index" : { "_index" : "dates_year_only", "_id" : "first" } }' + - '{"date" : "1900", "field" : 1 }' + - '{ "index" : { "_index" : "dates_year_only", "_id" : "second" } }' + - '{"date" : "2022", "field" : 1 }' + - '{ "index" : { "_index" : "dates_year_only", "_id" : "third" } }' + - '{"date" : "2022", "field" : 2 }' + - '{ "index" : { "_index" : "dates_year_only", "_id" : "fourth" } }' + - '{"date" : "1500", "field" : 2 }' + + - do: + indices.create: + index: dates + body: + mappings: + properties: + date: + type: date + + - do: + bulk: + refresh: true + body: + - '{ "index" : { "_index" : "dates", "_id" : "first" } }' + - '{"date" : "1900-01-01T12:12:12.123456789Z", "field" : 1 }' + - '{ "index" : { "_index" : "dates", "_id" : "second" } }' + - '{"date" : "2022-01-01T12:12:12.123456789Z", "field" : 1 }' + - '{ "index" : { "_index" : "dates", "_id" : "third" } }' + - '{"date" : "2022-01-03T12:12:12.123456789Z", "field" : 2 }' + - '{ "index" : { "_index" : "dates", "_id" : "fourth" } }' + - '{"date" : "1500-01-01T12:12:12.123456789Z", "field" : 2 }' + - '{ "index" : { "_index" : "dates", "_id" : "fifth" } }' + - '{"date" : "1500-01-05T12:12:12.123456789Z", "field" : 2 }' + +--- +"test range query for all docs with year uuuu": + - do: + search: + rest_total_hits_as_int: true + index: dates + body: + query: + range: + date: + gte: 1000 + lte: 2023 + format: uuuu + + - match: { hits.total: 5 } + - length: { hits.hits: 5 } + +--- +"test match query gte and lt for single result with year uuuu": + - do: + search: + rest_total_hits_as_int: true + index: dates + body: + query: + range: + date: + gte: 1500 #1500-01-01T00:00:00 + lte: 1500 #1500-01-01T23:59:59 + format: uuuu + + - match: { hits.total: 1 } + - length: { hits.hits: 1 } + - match: { hits.hits.0._id: "fourth" } + +--- +"test match query gte and lte with year uuuu": + - do: + search: + rest_total_hits_as_int: true + index: dates + body: + query: + range: + date: + gte: 1500 + lte: 2000 + format: uuuu + + - match: { hits.total: 3 } + - length: { hits.hits: 3 } + - match: { hits.hits.0._id: "first" } + - match: { hits.hits.1._id: "fourth" } + - match: { hits.hits.2._id: "fifth" } + +--- +"test match query with year uuuu": + - do: + search: + rest_total_hits_as_int: true + index: dates_year_only + body: + query: + match: + date: + query: "1500" + + - match: { hits.total: 1 } + - length: { hits.hits: 1 } + - match: { hits.hits.0._id: "fourth" } 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 dc619aa5e8aba..fefa9dfa7b32a 100644 --- a/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java @@ -16,6 +16,7 @@ import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoField; +import java.time.temporal.IsoFields; import java.time.temporal.TemporalAccessor; import java.util.ArrayList; import java.util.Collections; @@ -41,12 +42,17 @@ class JavaDateFormatter implements DateFormatter { */ private static final BiConsumer DEFAULT_ROUND_UP = (builder, parser) -> { String parserAsString = parser.toString(); - if (parserAsString.contains(ChronoField.MONTH_OF_YEAR.toString())) { + if (parserAsString.contains(ChronoField.DAY_OF_YEAR.toString())) { + builder.parseDefaulting(ChronoField.DAY_OF_YEAR, 1L); + // TODO ideally we should make defaulting for weekbased year here too, + // but this will not work when locale is changed + // weekbased rounding relies on DateFormatters#localDateFromWeekBasedDate + // Applying month of year or dayOfMonth when weekbased fields are used will result in a conflict + } else if (parserAsString.contains(IsoFields.WEEK_BASED_YEAR.toString()) == false) { builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1L); - } - if (parserAsString.contains(ChronoField.DAY_OF_MONTH.toString())) { builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1L); } + if (parserAsString.contains(ChronoField.CLOCK_HOUR_OF_AMPM.toString())) { builder.parseDefaulting(ChronoField.CLOCK_HOUR_OF_AMPM, 11L); builder.parseDefaulting(ChronoField.AMPM_OF_DAY, 1L); 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 283e6d06cd61a..250f2ad63e70d 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java @@ -324,6 +324,26 @@ public void testRoundupFormatterWithEpochDates() { assertRoundupFormatter("uuuu-MM-dd'T'HH:mm:ss.SSS||epoch_second", "1234567890", 1234567890999L); } + public void testYearWithoutMonthRoundUp() { + assertRoundupFormatter("1500", "1500-01-01T23:59:59.999", "uuuu"); + assertRoundupFormatter("2022", "2022-01-01T23:59:59.999", "uuuu"); + assertRoundupFormatter("2022", "2022-01-01T23:59:59.999", "yyyy"); + assumeFalse( + "won't work in jdk8 " + "because SPI mechanism is not looking at classpath - needs ISOCalendarDataProvider in jre's ext/libs", + JavaVersion.current().equals(JavaVersion.parse("8")) + ); + // cannot reliably default week based years due to locale changing. This is always using the same locale anyway + // See JavaDateFormatter javadocs + assertRoundupFormatter("2022", "2022-01-03T23:59:59.999", "YYYY"); + } + + private void assertRoundupFormatter(String input, String expectedDate, String format) { + long expectedMillis = DateFormatters.from(DateFormatter.forPattern("strict_date_optional_time").parse(expectedDate)) + .toInstant() + .toEpochMilli(); + assertRoundupFormatter(format, input, expectedMillis); + } + private void assertRoundupFormatter(String format, String input, long expectedMilliSeconds) { JavaDateFormatter dateFormatter = (JavaDateFormatter) DateFormatter.forPattern(format); dateFormatter.parse(input);