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);