Skip to content

Commit 4083e70

Browse files
committed
Extend the date rounding logic to be conditional
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 elastic#89096 closes elastic#58986
1 parent e64eb8c commit 4083e70

File tree

3 files changed

+66
-12
lines changed

3 files changed

+66
-12
lines changed

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: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,43 @@
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+
if (parser.toString().contains(ChronoField.DAY_OF_YEAR.toString())) {
44+
builder.parseDefaulting(ChronoField.DAY_OF_YEAR, 1L);
45+
} else {
46+
builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1L);
47+
builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1L);
48+
}
49+
if (parser.toString().contains(ChronoField.CLOCK_HOUR_OF_AMPM.toString())) {
50+
builder.parseDefaulting(ChronoField.CLOCK_HOUR_OF_AMPM, 11L);
51+
builder.parseDefaulting(ChronoField.AMPM_OF_DAY, 1L);
52+
} else if (parser.toString().contains(ChronoField.HOUR_OF_AMPM.toString())) {
53+
builder.parseDefaulting(ChronoField.HOUR_OF_AMPM, 11L);
54+
builder.parseDefaulting(ChronoField.AMPM_OF_DAY, 1L);
55+
} else {
56+
builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 23L);
57+
}
58+
builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 59L);
59+
builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 59L);
60+
builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L);
61+
};
2962

3063
private final String format;
3164
private final DateTimeFormatter printer;
@@ -50,12 +83,7 @@ JavaDateFormatter getRoundupParser() {
5083
format,
5184
printer,
5285
// 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),
86+
DEFAULT_ROUND_UP,
5987
parsers
6088
);
6189
}
@@ -64,7 +92,7 @@ JavaDateFormatter getRoundupParser() {
6492
JavaDateFormatter(
6593
String format,
6694
DateTimeFormatter printer,
67-
Consumer<DateTimeFormatterBuilder> roundupParserConsumer,
95+
BiConsumer<DateTimeFormatterBuilder, DateTimeFormatter> roundupParserConsumer,
6896
DateTimeFormatter... parsers
6997
) {
7098
if (printer == null) {
@@ -105,15 +133,15 @@ private static DateTimeFormatter[] parsersArray(DateTimeFormatter printer, DateT
105133
*/
106134
private static RoundUpFormatter createRoundUpParser(
107135
String format,
108-
Consumer<DateTimeFormatterBuilder> roundupParserConsumer,
136+
BiConsumer<DateTimeFormatterBuilder, DateTimeFormatter> roundupParserConsumer,
109137
Locale locale,
110138
DateTimeFormatter[] parsers
111139
) {
112140
if (format.contains("||") == false) {
113141
return new RoundUpFormatter(format, mapParsers(parser -> {
114142
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
115143
builder.append(parser);
116-
roundupParserConsumer.accept(builder);
144+
roundupParserConsumer.accept(builder, parser);
117145
return builder.toFormatter(locale);
118146
}, parsers));
119147
}

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

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

88+
public void testDayOfYear() {
89+
DateFormatter formatter = DateFormatter.forPattern("yyyy-DDD'T'HH:mm:ss.SSS");
90+
assertDateMathEquals(formatter.toDateMathParser(), "2022-104T14:08:30.293", "2022-04-14T14:08:30.293", 0, true, ZoneOffset.UTC);
91+
}
92+
93+
public void testDayOfYearWithMissingFields() {
94+
DateFormatter formatter = DateFormatter.forPattern("yyyy[-DDD'T'HH:mm:ss.SSS]");
95+
assertDateMathEquals(formatter.toDateMathParser(), "2022", "2022-01-01T23:59:59.999Z", 0, true, ZoneOffset.UTC);
96+
}
97+
98+
public void testAMPM() {
99+
DateFormatter formatter = DateFormatter.forPattern("MM/dd/yyyy hh:mm a"); // h clock-hour-of-am-pm (1-12)
100+
assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020 12:48 AM", "2020-04-30T00:48:59.999Z", 0, true, ZoneOffset.UTC);
101+
102+
formatter = DateFormatter.forPattern("MM/dd/yyyy KK:mm a"); // K hour-of-am-pm (0-11)
103+
assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020 00:48 AM", "2020-04-30T00:48:59.999Z", 0, true, ZoneOffset.UTC);
104+
}
105+
106+
public void testAMPMWithTimeMissing() {
107+
DateFormatter formatter = DateFormatter.forPattern("MM/dd/yyyy[ hh:mm a]"); // h clock-hour-of-am-pm (1-12)
108+
assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020", "2020-04-30T23:59:59.999Z", 0, true, ZoneOffset.UTC);
109+
110+
formatter = DateFormatter.forPattern("MM/dd/yyyy[ KK:mm a]"); // K hour-of-am-pm (0-11)
111+
assertDateMathEquals(formatter.toDateMathParser(), "04/30/2020", "2020-04-30T23:59:59.999Z", 0, true, ZoneOffset.UTC);
112+
}
113+
88114
public void testWeekDates() {
89115
DateFormatter formatter = DateFormatter.forPattern("YYYY-ww");
90116
assertDateMathEquals(formatter.toDateMathParser(), "2016-01", "2016-01-04T23:59:59.999Z", 0, true, ZoneOffset.UTC);

0 commit comments

Comments
 (0)