Skip to content

Commit

Permalink
Extend the date rounding logic to be conditional (#89693) (#89794)
Browse files Browse the repository at this point in the history
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 DayOfMonth, MonthOfYear, ClockHourOfAMPM and HourOfAmPM

closes #89096
closes #58986
backports #89693
  • Loading branch information
pgomulka committed Sep 5, 2022
1 parent 935d139 commit 330c97e
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 27 deletions.
7 changes: 7 additions & 0 deletions docs/changelog/89693.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pr: 89693
summary: Extend the date rounding logic to be conditional
area: Infra/Core
type: bug
issues:
- 89096
- 58986
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,15 @@ 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
);

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
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
* <code>
* Value(MonthOfYear,2)'/'Value(DayOfMonth,2)'/'Value(YearOfEra,4,19,EXCEEDS_PAD)'
* 'Value(ClockHourOfAmPm,2)':'Value(MinuteOfHour,2)' 'Text(AmPmOfDay,SHORT)
* </code>
* 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<DateTimeFormatterBuilder, DateTimeFormatter> 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;
Expand All @@ -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
);
}
Expand All @@ -64,7 +93,7 @@ JavaDateFormatter getRoundupParser() {
JavaDateFormatter(
String format,
DateTimeFormatter printer,
Consumer<DateTimeFormatterBuilder> roundupParserConsumer,
BiConsumer<DateTimeFormatterBuilder, DateTimeFormatter> roundupParserConsumer,
DateTimeFormatter... parsers
) {
if (printer == null) {
Expand Down Expand Up @@ -105,15 +134,15 @@ private static DateTimeFormatter[] parsersArray(DateTimeFormatter printer, DateT
*/
private static RoundUpFormatter createRoundUpParser(
String format,
Consumer<DateTimeFormatterBuilder> roundupParserConsumer,
BiConsumer<DateTimeFormatterBuilder, DateTimeFormatter> roundupParserConsumer,
Locale locale,
DateTimeFormatter[] parsers
) {
if (format.contains("||") == false) {
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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 330c97e

Please sign in to comment.