Skip to content

Commit 8e517da

Browse files
authored
Extend the date rounding logic to be conditional (#89693)
Date rounding logic should take into account the fields that will be parsed be a parser. If a parser has a DayOfYear field, the rounding logic should not try to default DayOfMonth as it will conflict with DayOfYear However the DateTimeFormatter does not have a public method to return information of fields that will be parsed. The hacky workaround is to rely on toString() implementation that will return a field info when it was defined with textual pattern. This commits introduced conditional logic for DayOfYear, ClockHourOfAMPM and HourOfAmPM closes #89096 closes #58986
1 parent 9b459a2 commit 8e517da

File tree

5 files changed

+95
-27
lines changed

5 files changed

+95
-27
lines changed

docs/changelog/89693.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pr: 89693
2+
summary: Extend the date rounding logic to be conditional
3+
area: Infra/Core
4+
type: bug
5+
issues:
6+
- 89096
7+
- 58986

server/src/main/java/org/elasticsearch/common/time/DateFormatters.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ public class DateFormatters {
574574

575575
/*
576576
* Returns a basic formatter for a full date as four digit weekyear, two
577-
* digit week of weekyear, and one digit day of week (xxxx'W'wwe).
577+
* digit week of weekyear, and one digit day of week (YYYY'W'wwe).
578578
*/
579579
private static final DateFormatter STRICT_BASIC_WEEK_DATE = new JavaDateFormatter(
580580
"strict_basic_week_date",
@@ -584,7 +584,7 @@ public class DateFormatters {
584584

585585
/*
586586
* Returns a basic formatter that combines a basic weekyear date and time
587-
* without millis, separated by a 'T' (xxxx'W'wwe'T'HHmmssX).
587+
* without millis, separated by a 'T' (YYYY'W'wwe'T'HHmmssX).
588588
*/
589589
private static final DateFormatter STRICT_BASIC_WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter(
590590
"strict_basic_week_date_time_no_millis",
@@ -616,7 +616,7 @@ public class DateFormatters {
616616

617617
/*
618618
* Returns a basic formatter that combines a basic weekyear date and time,
619-
* separated by a 'T' (xxxx'W'wwe'T'HHmmss.SSSX).
619+
* separated by a 'T' (YYYY'W'wwe'T'HHmmss.SSSX).
620620
*/
621621
private static final DateFormatter STRICT_BASIC_WEEK_DATE_TIME = new JavaDateFormatter(
622622
"strict_basic_week_date_time",
@@ -1080,13 +1080,13 @@ public class DateFormatters {
10801080

10811081
/*
10821082
* Returns a formatter for a full date as four digit weekyear, two digit
1083-
* week of weekyear, and one digit day of week (xxxx-'W'ww-e).
1083+
* week of weekyear, and one digit day of week (YYYY-'W'ww-e).
10841084
*/
10851085
private static final DateFormatter STRICT_WEEK_DATE = new JavaDateFormatter("strict_week_date", ISO_WEEK_DATE);
10861086

10871087
/*
10881088
* Returns a formatter that combines a full weekyear date and time without millis,
1089-
* separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ssZZ).
1089+
* separated by a 'T' (YYYY-'W'ww-e'T'HH:mm:ssZZ).
10901090
*/
10911091
private static final DateFormatter STRICT_WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter(
10921092
"strict_week_date_time_no_millis",
@@ -1109,7 +1109,7 @@ public class DateFormatters {
11091109

11101110
/*
11111111
* Returns a formatter that combines a full weekyear date and time,
1112-
* separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ).
1112+
* separated by a 'T' (YYYY-'W'ww-e'T'HH:mm:ss.SSSZZ).
11131113
*/
11141114
private static final DateFormatter STRICT_WEEK_DATE_TIME = new JavaDateFormatter(
11151115
"strict_week_date_time",
@@ -1153,13 +1153,13 @@ public class DateFormatters {
11531153

11541154
/*
11551155
* Returns a formatter for a four digit weekyear and two digit week of
1156-
* weekyear. (xxxx-'W'ww)
1156+
* weekyear. (YYYY-'W'ww)
11571157
*/
11581158
private static final DateFormatter STRICT_WEEKYEAR_WEEK = new JavaDateFormatter("strict_weekyear_week", STRICT_WEEKYEAR_WEEK_FORMATTER);
11591159

11601160
/*
11611161
* Returns a formatter for a four digit weekyear, two digit week of
1162-
* weekyear, and one digit day of week. (xxxx-'W'ww-e)
1162+
* weekyear, and one digit day of week. (YYYY-'W'ww-e)
11631163
*/
11641164
private static final DateFormatter STRICT_WEEKYEAR_WEEK_DAY = new JavaDateFormatter(
11651165
"strict_weekyear_week_day",
@@ -1693,7 +1693,7 @@ public class DateFormatters {
16931693

16941694
/*
16951695
* Returns a formatter that combines a full weekyear date and time,
1696-
* separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ).
1696+
* separated by a 'T' (YYYY-'W'ww-e'T'HH:mm:ss.SSSZZ).
16971697
*/
16981698
private static final DateFormatter WEEK_DATE_TIME = new JavaDateFormatter(
16991699
"week_date_time",
@@ -1718,7 +1718,7 @@ public class DateFormatters {
17181718

17191719
/*
17201720
* Returns a formatter that combines a full weekyear date and time,
1721-
* separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ssZZ).
1721+
* separated by a 'T' (YYYY-'W'ww-e'T'HH:mm:ssZZ).
17221722
*/
17231723
private static final DateFormatter WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter(
17241724
"week_date_time_no_millis",
@@ -1741,7 +1741,7 @@ public class DateFormatters {
17411741

17421742
/*
17431743
* Returns a basic formatter that combines a basic weekyear date and time,
1744-
* separated by a 'T' (xxxx'W'wwe'T'HHmmss.SSSX).
1744+
* separated by a 'T' (YYYY'W'wwe'T'HHmmss.SSSX).
17451745
*/
17461746
private static final DateFormatter BASIC_WEEK_DATE_TIME = new JavaDateFormatter(
17471747
"basic_week_date_time",
@@ -1763,7 +1763,7 @@ public class DateFormatters {
17631763

17641764
/*
17651765
* Returns a basic formatter that combines a basic weekyear date and time,
1766-
* separated by a 'T' (xxxx'W'wwe'T'HHmmssX).
1766+
* separated by a 'T' (YYYY'W'wwe'T'HHmmssX).
17671767
*/
17681768
private static final DateFormatter BASIC_WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter(
17691769
"basic_week_date_time_no_millis",
@@ -1906,13 +1906,13 @@ public class DateFormatters {
19061906

19071907
/*
19081908
* Returns a formatter for a full date as four digit weekyear, two digit
1909-
* week of weekyear, and one digit day of week (xxxx-'W'ww-e).
1909+
* week of weekyear, and one digit day of week (YYYY-'W'ww-e).
19101910
*/
19111911
private static final DateFormatter WEEK_DATE = new JavaDateFormatter("week_date", ISO_WEEK_DATE, WEEK_DATE_FORMATTER);
19121912

19131913
/*
19141914
* Returns a formatter for a four digit weekyear and two digit week of
1915-
* weekyear. (xxxx-'W'ww)
1915+
* weekyear. (YYYY-'W'ww)
19161916
*/
19171917
private static final DateFormatter WEEKYEAR_WEEK = new JavaDateFormatter(
19181918
"weekyear_week",
@@ -1926,7 +1926,7 @@ public class DateFormatters {
19261926

19271927
/*
19281928
* Returns a formatter for a four digit weekyear, two digit week of
1929-
* weekyear, and one digit day of week. (xxxx-'W'ww-e)
1929+
* weekyear, and one digit day of week. (YYYY-'W'ww-e)
19301930
*/
19311931
private static final DateFormatter WEEKYEAR_WEEK_DAY = new JavaDateFormatter(
19321932
"weekyear_week_day",

server/src/main/java/org/elasticsearch/common/time/EpochTime.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,15 +252,15 @@ public long getFrom(TemporalAccessor temporal) {
252252
static final DateFormatter SECONDS_FORMATTER = new JavaDateFormatter(
253253
"epoch_second",
254254
SECONDS_FORMATTER1,
255-
builder -> builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L),
255+
(builder, parser) -> builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L),
256256
SECONDS_FORMATTER1,
257257
SECONDS_FORMATTER2
258258
);
259259

260260
static final DateFormatter MILLIS_FORMATTER = new JavaDateFormatter(
261261
"epoch_millis",
262262
MILLISECONDS_FORMATTER1,
263-
builder -> builder.parseDefaulting(EpochTime.NANOS_OF_MILLI, 999_999L),
263+
(builder, parser) -> builder.parseDefaulting(EpochTime.NANOS_OF_MILLI, 999_999L),
264264
MILLISECONDS_FORMATTER1,
265265
MILLISECONDS_FORMATTER2
266266
);

server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,44 @@
2222
import java.util.List;
2323
import java.util.Locale;
2424
import java.util.Objects;
25-
import java.util.function.Consumer;
25+
import java.util.function.BiConsumer;
2626
import java.util.function.UnaryOperator;
2727

2828
class JavaDateFormatter implements DateFormatter {
29+
/**
30+
* A default consumer that allows to round up fields (used for range searches, optional fields missing)
31+
* it relies on toString implementation of DateTimeFormatter and ChronoField.
32+
* For instance for pattern
33+
* the parser would have a toString()
34+
* <code>
35+
* Value(MonthOfYear,2)'/'Value(DayOfMonth,2)'/'Value(YearOfEra,4,19,EXCEEDS_PAD)'
36+
* 'Value(ClockHourOfAmPm,2)':'Value(MinuteOfHour,2)' 'Text(AmPmOfDay,SHORT)
37+
* </code>
38+
* and ChronoField.CLOCK_HOUR_OF_AMPM would have toString() ClockHourOfAmPm
39+
* this allows the rounding logic to default CLOCK_HOUR_OF_AMPM field instead of HOUR_OF_DAY
40+
* 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
41+
*/
42+
private static final BiConsumer<DateTimeFormatterBuilder, DateTimeFormatter> DEFAULT_ROUND_UP = (builder, parser) -> {
43+
String parserAsString = parser.toString();
44+
if (parserAsString.contains(ChronoField.MONTH_OF_YEAR.toString())) {
45+
builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1L);
46+
}
47+
if (parserAsString.contains(ChronoField.DAY_OF_MONTH.toString())) {
48+
builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1L);
49+
}
50+
if (parserAsString.contains(ChronoField.CLOCK_HOUR_OF_AMPM.toString())) {
51+
builder.parseDefaulting(ChronoField.CLOCK_HOUR_OF_AMPM, 11L);
52+
builder.parseDefaulting(ChronoField.AMPM_OF_DAY, 1L);
53+
} else if (parserAsString.contains(ChronoField.HOUR_OF_AMPM.toString())) {
54+
builder.parseDefaulting(ChronoField.HOUR_OF_AMPM, 11L);
55+
builder.parseDefaulting(ChronoField.AMPM_OF_DAY, 1L);
56+
} else {
57+
builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 23L);
58+
}
59+
builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 59L);
60+
builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 59L);
61+
builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L);
62+
};
2963

3064
private final String format;
3165
private final DateTimeFormatter printer;
@@ -50,12 +84,7 @@ JavaDateFormatter getRoundupParser() {
5084
format,
5185
printer,
5286
// set up base fields which should be used for default parsing, when we round up for date math
53-
builder -> builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1L)
54-
.parseDefaulting(ChronoField.DAY_OF_MONTH, 1L)
55-
.parseDefaulting(ChronoField.HOUR_OF_DAY, 23L)
56-
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 59L)
57-
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 59L)
58-
.parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L),
87+
DEFAULT_ROUND_UP,
5988
parsers
6089
);
6190
}
@@ -64,7 +93,7 @@ JavaDateFormatter getRoundupParser() {
6493
JavaDateFormatter(
6594
String format,
6695
DateTimeFormatter printer,
67-
Consumer<DateTimeFormatterBuilder> roundupParserConsumer,
96+
BiConsumer<DateTimeFormatterBuilder, DateTimeFormatter> roundupParserConsumer,
6897
DateTimeFormatter... parsers
6998
) {
7099
if (printer == null) {
@@ -105,15 +134,15 @@ private static DateTimeFormatter[] parsersArray(DateTimeFormatter printer, DateT
105134
*/
106135
private static RoundUpFormatter createRoundUpParser(
107136
String format,
108-
Consumer<DateTimeFormatterBuilder> roundupParserConsumer,
137+
BiConsumer<DateTimeFormatterBuilder, DateTimeFormatter> roundupParserConsumer,
109138
Locale locale,
110139
DateTimeFormatter[] parsers
111140
) {
112141
if (format.contains("||") == false) {
113142
return new RoundUpFormatter(format, mapParsers(parser -> {
114143
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
115144
builder.append(parser);
116-
roundupParserConsumer.accept(builder);
145+
roundupParserConsumer.accept(builder, parser);
117146
return builder.toFormatter(locale);
118147
}, parsers));
119148
}

server/src/test/java/org/elasticsearch/common/time/JavaDateMathParserTests.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,38 @@ public void testOverridingLocaleOrZoneAndCompositeRoundUpParser() {
8585
assertDateEquals(gotMillis, "297276785531", "297276785531");
8686
}
8787

88+
public void testWeekBasedDate() {
89+
DateFormatter formatter = DateFormatter.forPattern("strict_basic_week_date");// YYYY'W'wwe
90+
// first week of 2022 is starting on Monday 3rd Jan
91+
assertDateMathEquals(formatter.toDateMathParser(), "2022W0101", "2022-01-03T23:59:59.999Z", 0, true, ZoneOffset.UTC);
92+
93+
// defaulting missing day of week
94+
formatter = DateFormatter.forPattern("YYYY'W'ww[e]");// YYYY'W'wwe
95+
// second week of 2022 is starting on Monday 10th Jan
96+
assertDateMathEquals(formatter.toDateMathParser(), "2022W02", "2022-01-10T23:59:59.999Z", 0, true, ZoneOffset.UTC);
97+
}
98+
99+
public void testDayOfYear() {
100+
DateFormatter formatter = DateFormatter.forPattern("yyyy-DDD'T'HH:mm:ss.SSS");
101+
assertDateMathEquals(formatter.toDateMathParser(), "2022-104T14:08:30.293", "2022-04-14T14:08:30.293", 0, true, ZoneOffset.UTC);
102+
}
103+
104+
public void testAMPM() {
105+
DateFormatter formatter = DateFormatter.forPattern("MM/dd/yyyy hh:mm a"); // h clock-hour-of-am-pm (1-12)
106+
assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020 12:48 AM", "2020-04-30T00:48:59.999Z", 0, true, ZoneOffset.UTC);
107+
108+
formatter = DateFormatter.forPattern("MM/dd/yyyy KK:mm a"); // K hour-of-am-pm (0-11)
109+
assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020 00:48 AM", "2020-04-30T00:48:59.999Z", 0, true, ZoneOffset.UTC);
110+
}
111+
112+
public void testAMPMWithTimeMissing() {
113+
DateFormatter formatter = DateFormatter.forPattern("MM/dd/yyyy[ hh:mm a]"); // h clock-hour-of-am-pm (1-12)
114+
assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020", "2020-04-30T23:59:59.999Z", 0, true, ZoneOffset.UTC);
115+
116+
formatter = DateFormatter.forPattern("MM/dd/yyyy[ KK:mm a]"); // K hour-of-am-pm (0-11)
117+
assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020", "2020-04-30T23:59:59.999Z", 0, true, ZoneOffset.UTC);
118+
}
119+
88120
public void testWeekDates() {
89121
DateFormatter formatter = DateFormatter.forPattern("YYYY-ww");
90122
assertDateMathEquals(formatter.toDateMathParser(), "2016-01", "2016-01-04T23:59:59.999Z", 0, true, ZoneOffset.UTC);

0 commit comments

Comments
 (0)