diff --git a/docs/changelog/89693.yaml b/docs/changelog/89693.yaml new file mode 100644 index 0000000000000..661eefd4c153b --- /dev/null +++ b/docs/changelog/89693.yaml @@ -0,0 +1,7 @@ +pr: 89693 +summary: Extend the date rounding logic to be conditional +area: Infra/Core +type: bug +issues: + - 89096 + - 58986 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 17cd3bfab4954..dc73dc77c71af 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java @@ -574,7 +574,7 @@ public class DateFormatters { /* * Returns a basic formatter for a full date as four digit weekyear, two - * digit week of weekyear, and one digit day of week (xxxx'W'wwe). + * digit week of weekyear, and one digit day of week (YYYY'W'wwe). */ private static final DateFormatter STRICT_BASIC_WEEK_DATE = new JavaDateFormatter( "strict_basic_week_date", @@ -584,7 +584,7 @@ public class DateFormatters { /* * Returns a basic formatter that combines a basic weekyear date and time - * without millis, separated by a 'T' (xxxx'W'wwe'T'HHmmssX). + * without millis, separated by a 'T' (YYYY'W'wwe'T'HHmmssX). */ private static final DateFormatter STRICT_BASIC_WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter( "strict_basic_week_date_time_no_millis", @@ -616,7 +616,7 @@ public class DateFormatters { /* * Returns a basic formatter that combines a basic weekyear date and time, - * separated by a 'T' (xxxx'W'wwe'T'HHmmss.SSSX). + * separated by a 'T' (YYYY'W'wwe'T'HHmmss.SSSX). */ private static final DateFormatter STRICT_BASIC_WEEK_DATE_TIME = new JavaDateFormatter( "strict_basic_week_date_time", @@ -1080,13 +1080,13 @@ public class DateFormatters { /* * Returns a formatter for a full date as four digit weekyear, two digit - * week of weekyear, and one digit day of week (xxxx-'W'ww-e). + * week of weekyear, and one digit day of week (YYYY-'W'ww-e). */ private static final DateFormatter STRICT_WEEK_DATE = new JavaDateFormatter("strict_week_date", ISO_WEEK_DATE); /* * Returns a formatter that combines a full weekyear date and time without millis, - * separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ssZZ). + * separated by a 'T' (YYYY-'W'ww-e'T'HH:mm:ssZZ). */ private static final DateFormatter STRICT_WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter( "strict_week_date_time_no_millis", @@ -1109,7 +1109,7 @@ public class DateFormatters { /* * Returns a formatter that combines a full weekyear date and time, - * separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ). + * separated by a 'T' (YYYY-'W'ww-e'T'HH:mm:ss.SSSZZ). */ private static final DateFormatter STRICT_WEEK_DATE_TIME = new JavaDateFormatter( "strict_week_date_time", @@ -1153,13 +1153,13 @@ public class DateFormatters { /* * Returns a formatter for a four digit weekyear and two digit week of - * weekyear. (xxxx-'W'ww) + * weekyear. (YYYY-'W'ww) */ private static final DateFormatter STRICT_WEEKYEAR_WEEK = new JavaDateFormatter("strict_weekyear_week", STRICT_WEEKYEAR_WEEK_FORMATTER); /* * Returns a formatter for a four digit weekyear, two digit week of - * weekyear, and one digit day of week. (xxxx-'W'ww-e) + * weekyear, and one digit day of week. (YYYY-'W'ww-e) */ private static final DateFormatter STRICT_WEEKYEAR_WEEK_DAY = new JavaDateFormatter( "strict_weekyear_week_day", @@ -1693,7 +1693,7 @@ public class DateFormatters { /* * Returns a formatter that combines a full weekyear date and time, - * separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ). + * separated by a 'T' (YYYY-'W'ww-e'T'HH:mm:ss.SSSZZ). */ private static final DateFormatter WEEK_DATE_TIME = new JavaDateFormatter( "week_date_time", @@ -1718,7 +1718,7 @@ public class DateFormatters { /* * Returns a formatter that combines a full weekyear date and time, - * separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ssZZ). + * separated by a 'T' (YYYY-'W'ww-e'T'HH:mm:ssZZ). */ private static final DateFormatter WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter( "week_date_time_no_millis", @@ -1741,7 +1741,7 @@ public class DateFormatters { /* * Returns a basic formatter that combines a basic weekyear date and time, - * separated by a 'T' (xxxx'W'wwe'T'HHmmss.SSSX). + * separated by a 'T' (YYYY'W'wwe'T'HHmmss.SSSX). */ private static final DateFormatter BASIC_WEEK_DATE_TIME = new JavaDateFormatter( "basic_week_date_time", @@ -1763,7 +1763,7 @@ public class DateFormatters { /* * Returns a basic formatter that combines a basic weekyear date and time, - * separated by a 'T' (xxxx'W'wwe'T'HHmmssX). + * separated by a 'T' (YYYY'W'wwe'T'HHmmssX). */ private static final DateFormatter BASIC_WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter( "basic_week_date_time_no_millis", @@ -1906,13 +1906,13 @@ public class DateFormatters { /* * Returns a formatter for a full date as four digit weekyear, two digit - * week of weekyear, and one digit day of week (xxxx-'W'ww-e). + * week of weekyear, and one digit day of week (YYYY-'W'ww-e). */ private static final DateFormatter WEEK_DATE = new JavaDateFormatter("week_date", ISO_WEEK_DATE, WEEK_DATE_FORMATTER); /* * Returns a formatter for a four digit weekyear and two digit week of - * weekyear. (xxxx-'W'ww) + * weekyear. (YYYY-'W'ww) */ private static final DateFormatter WEEKYEAR_WEEK = new JavaDateFormatter( "weekyear_week", @@ -1926,7 +1926,7 @@ public class DateFormatters { /* * Returns a formatter for a four digit weekyear, two digit week of - * weekyear, and one digit day of week. (xxxx-'W'ww-e) + * weekyear, and one digit day of week. (YYYY-'W'ww-e) */ private static final DateFormatter WEEKYEAR_WEEK_DAY = new JavaDateFormatter( "weekyear_week_day", 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 2bb3cde3ab9ef..d9df109bda0cd 100644 --- a/server/src/main/java/org/elasticsearch/common/time/EpochTime.java +++ b/server/src/main/java/org/elasticsearch/common/time/EpochTime.java @@ -252,7 +252,7 @@ public long getFrom(TemporalAccessor temporal) { static final DateFormatter SECONDS_FORMATTER = new JavaDateFormatter( "epoch_second", SECONDS_FORMATTER1, - builder -> builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L), + (builder, parser) -> builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L), SECONDS_FORMATTER1, SECONDS_FORMATTER2 ); @@ -260,7 +260,7 @@ public long getFrom(TemporalAccessor temporal) { static final DateFormatter MILLIS_FORMATTER = new JavaDateFormatter( "epoch_millis", MILLISECONDS_FORMATTER1, - builder -> builder.parseDefaulting(EpochTime.NANOS_OF_MILLI, 999_999L), + (builder, parser) -> builder.parseDefaulting(EpochTime.NANOS_OF_MILLI, 999_999L), MILLISECONDS_FORMATTER1, MILLISECONDS_FORMATTER2 ); 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 6a84a7f215f16..f83b6a66da400 100644 --- a/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java @@ -22,10 +22,44 @@ import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.function.Consumer; +import java.util.function.BiConsumer; import java.util.function.UnaryOperator; class JavaDateFormatter implements DateFormatter { + /** + * A default consumer that allows to round up fields (used for range searches, optional fields missing) + * it relies on toString implementation of DateTimeFormatter and ChronoField. + * For instance for pattern + * the parser would have a toString() + * + * Value(MonthOfYear,2)'/'Value(DayOfMonth,2)'/'Value(YearOfEra,4,19,EXCEEDS_PAD)' + * 'Value(ClockHourOfAmPm,2)':'Value(MinuteOfHour,2)' 'Text(AmPmOfDay,SHORT) + * + * and ChronoField.CLOCK_HOUR_OF_AMPM would have toString() ClockHourOfAmPm + * this allows the rounding logic to default CLOCK_HOUR_OF_AMPM field instead of HOUR_OF_DAY + * without this logic, the rounding would result in a conflict as HOUR_OF_DAY would be missing, but CLOCK_HOUR_OF_AMPM would be provided + */ + private static final BiConsumer DEFAULT_ROUND_UP = (builder, parser) -> { + String parserAsString = parser.toString(); + if (parserAsString.contains(ChronoField.MONTH_OF_YEAR.toString())) { + 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); + } else if (parserAsString.contains(ChronoField.HOUR_OF_AMPM.toString())) { + builder.parseDefaulting(ChronoField.HOUR_OF_AMPM, 11L); + builder.parseDefaulting(ChronoField.AMPM_OF_DAY, 1L); + } else { + builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 23L); + } + builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 59L); + builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 59L); + builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L); + }; private final String format; private final DateTimeFormatter printer; @@ -50,12 +84,7 @@ JavaDateFormatter getRoundupParser() { format, printer, // set up base fields which should be used for default parsing, when we round up for date math - builder -> builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1L) - .parseDefaulting(ChronoField.DAY_OF_MONTH, 1L) - .parseDefaulting(ChronoField.HOUR_OF_DAY, 23L) - .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 59L) - .parseDefaulting(ChronoField.SECOND_OF_MINUTE, 59L) - .parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L), + DEFAULT_ROUND_UP, parsers ); } @@ -64,7 +93,7 @@ JavaDateFormatter getRoundupParser() { JavaDateFormatter( String format, DateTimeFormatter printer, - Consumer roundupParserConsumer, + BiConsumer roundupParserConsumer, DateTimeFormatter... parsers ) { if (printer == null) { @@ -105,7 +134,7 @@ private static DateTimeFormatter[] parsersArray(DateTimeFormatter printer, DateT */ private static RoundUpFormatter createRoundUpParser( String format, - Consumer roundupParserConsumer, + BiConsumer roundupParserConsumer, Locale locale, DateTimeFormatter[] parsers ) { @@ -113,7 +142,7 @@ private static RoundUpFormatter createRoundUpParser( return new RoundUpFormatter(format, mapParsers(parser -> { DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); builder.append(parser); - roundupParserConsumer.accept(builder); + roundupParserConsumer.accept(builder, parser); return builder.toFormatter(locale); }, parsers)); } diff --git a/server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java b/server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java index 5f273ed01ea4a..52305acc1248a 100644 --- a/server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java @@ -85,6 +85,38 @@ public void testOverridingLocaleOrZoneAndCompositeRoundUpParser() { assertDateEquals(gotMillis, "297276785531", "297276785531"); } + public void testWeekBasedDate() { + DateFormatter formatter = DateFormatter.forPattern("strict_basic_week_date");// YYYY'W'wwe + // first week of 2022 is starting on Monday 3rd Jan + assertDateMathEquals(formatter.toDateMathParser(), "2022W0101", "2022-01-03T23:59:59.999Z", 0, true, ZoneOffset.UTC); + + // defaulting missing day of week + formatter = DateFormatter.forPattern("YYYY'W'ww[e]");// YYYY'W'wwe + // second week of 2022 is starting on Monday 10th Jan + assertDateMathEquals(formatter.toDateMathParser(), "2022W02", "2022-01-10T23:59:59.999Z", 0, true, ZoneOffset.UTC); + } + + public void testDayOfYear() { + DateFormatter formatter = DateFormatter.forPattern("yyyy-DDD'T'HH:mm:ss.SSS"); + assertDateMathEquals(formatter.toDateMathParser(), "2022-104T14:08:30.293", "2022-04-14T14:08:30.293", 0, true, ZoneOffset.UTC); + } + + public void testAMPM() { + DateFormatter formatter = DateFormatter.forPattern("MM/dd/yyyy hh:mm a"); // h clock-hour-of-am-pm (1-12) + assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020 12:48 AM", "2020-04-30T00:48:59.999Z", 0, true, ZoneOffset.UTC); + + formatter = DateFormatter.forPattern("MM/dd/yyyy KK:mm a"); // K hour-of-am-pm (0-11) + assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020 00:48 AM", "2020-04-30T00:48:59.999Z", 0, true, ZoneOffset.UTC); + } + + public void testAMPMWithTimeMissing() { + DateFormatter formatter = DateFormatter.forPattern("MM/dd/yyyy[ hh:mm a]"); // h clock-hour-of-am-pm (1-12) + assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020", "2020-04-30T23:59:59.999Z", 0, true, ZoneOffset.UTC); + + formatter = DateFormatter.forPattern("MM/dd/yyyy[ KK:mm a]"); // K hour-of-am-pm (0-11) + assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020", "2020-04-30T23:59:59.999Z", 0, true, ZoneOffset.UTC); + } + public void testWeekDates() { DateFormatter formatter = DateFormatter.forPattern("YYYY-ww"); assertDateMathEquals(formatter.toDateMathParser(), "2016-01", "2016-01-04T23:59:59.999Z", 0, true, ZoneOffset.UTC);