From a09959851c0f97c819b419cd374beaf6f24c69fa Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 1 Oct 2024 14:10:15 +0100 Subject: [PATCH 1/3] Explicitly use ISO weekfields for built-in weekyear date formats (#113787) This is so it doesn't change when changing JDK version and locale database --- .../ingest/common/DateFormat.java | 1 + .../common/time/DateFormatters.java | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/DateFormat.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/DateFormat.java index cf0de9f4f16c8..84d4569e5841f 100644 --- a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/DateFormat.java +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/DateFormat.java @@ -97,6 +97,7 @@ Function getFunction(String format, ZoneId zoneId, Locale // fill the rest of the date up with the parsed date if (accessor.isSupported(ChronoField.YEAR) == false && accessor.isSupported(ChronoField.YEAR_OF_ERA) == false + && accessor.isSupported(WeekFields.ISO.weekBasedYear()) == false && accessor.isSupported(WeekFields.of(locale).weekBasedYear()) == false && accessor.isSupported(ChronoField.INSTANT_SECONDS) == false) { int year = LocalDate.now(ZoneOffset.UTC).getYear(); 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 1d84623ba06e4..e9aea75f9995b 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java @@ -82,7 +82,7 @@ private static DateFormatter newDateFormatter(String format, DateTimeFormatter p ); } - public static final WeekFields WEEK_FIELDS_ROOT = WeekFields.of(Locale.ROOT); + public static final WeekFields WEEK_FIELDS_ROOT = WeekFields.ISO; private static final DateTimeFormatter TIME_ZONE_FORMATTER_NO_COLON = new DateTimeFormatterBuilder().appendOffset("+HHmm", "Z") .toFormatter(Locale.ROOT) @@ -2389,6 +2389,7 @@ public static ZonedDateTime from(TemporalAccessor accessor, Locale locale, ZoneI boolean isLocalTimeSet = localTime != null; // the first two cases are the most common, so this allows us to exit early when parsing dates + WeekFields localeWeekFields; if (isLocalDateSet && isLocalTimeSet) { return of(localDate, localTime, zoneId); } else if (accessor.isSupported(ChronoField.INSTANT_SECONDS) && accessor.isSupported(NANO_OF_SECOND)) { @@ -2407,8 +2408,10 @@ public static ZonedDateTime from(TemporalAccessor accessor, Locale locale, ZoneI } else if (accessor.isSupported(MONTH_OF_YEAR)) { // missing year, falling back to the epoch and then filling return getLocalDate(accessor, locale).atStartOfDay(zoneId); - } else if (accessor.isSupported(WeekFields.of(locale).weekBasedYear())) { - return localDateFromWeekBasedDate(accessor, locale).atStartOfDay(zoneId); + } else if (accessor.isSupported(WeekFields.ISO.weekBasedYear())) { + return localDateFromWeekBasedDate(accessor, locale, WeekFields.ISO).atStartOfDay(zoneId); + } else if (accessor.isSupported((localeWeekFields = WeekFields.of(locale)).weekBasedYear())) { + return localDateFromWeekBasedDate(accessor, locale, localeWeekFields).atStartOfDay(zoneId); } // we should not reach this piece of code, everything being parsed we should be able to @@ -2416,8 +2419,7 @@ public static ZonedDateTime from(TemporalAccessor accessor, Locale locale, ZoneI throw new IllegalArgumentException("temporal accessor [" + accessor + "] cannot be converted to zoned date time"); } - private static LocalDate localDateFromWeekBasedDate(TemporalAccessor accessor, Locale locale) { - WeekFields weekFields = WeekFields.of(locale); + private static LocalDate localDateFromWeekBasedDate(TemporalAccessor accessor, Locale locale, WeekFields weekFields) { if (accessor.isSupported(weekFields.weekOfWeekBasedYear())) { return LocalDate.ofEpochDay(0) .with(weekFields.weekBasedYear(), accessor.get(weekFields.weekBasedYear())) @@ -2459,8 +2461,11 @@ public String toString() { }; private static LocalDate getLocalDate(TemporalAccessor accessor, Locale locale) { - if (accessor.isSupported(WeekFields.of(locale).weekBasedYear())) { - return localDateFromWeekBasedDate(accessor, locale); + WeekFields localeWeekFields; + if (accessor.isSupported(WeekFields.ISO.weekBasedYear())) { + return localDateFromWeekBasedDate(accessor, locale, WeekFields.ISO); + } else if (accessor.isSupported((localeWeekFields = WeekFields.of(locale)).weekBasedYear())) { + return localDateFromWeekBasedDate(accessor, locale, localeWeekFields); } else if (accessor.isSupported(MONTH_OF_YEAR)) { int year = getYear(accessor); if (accessor.isSupported(DAY_OF_MONTH)) { From 9292e9d132a1b2940967261e46830e92cd8d62cc Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 1 Oct 2024 15:26:18 +0100 Subject: [PATCH 2/3] Add notes on weekyear calculation differences --- docs/reference/mapping/params/format.asciidoc | 27 +++++++++++++------ docs/reference/mapping/types/date.asciidoc | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/reference/mapping/params/format.asciidoc b/docs/reference/mapping/params/format.asciidoc index 5babb4def2320..943e8fb879ff3 100644 --- a/docs/reference/mapping/params/format.asciidoc +++ b/docs/reference/mapping/params/format.asciidoc @@ -31,8 +31,13 @@ down to the nearest day. [[custom-date-formats]] ==== Custom date formats -Completely customizable date formats are supported. The syntax for these is explained -https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html[DateTimeFormatter docs]. +Completely customizable date formats are supported. The syntax for these is explained in +https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/time/format/DateTimeFormatter.html[DateTimeFormatter docs]. + +Note that whilst the built-in formats for week dates use the ISO definition of weekyears, +custom formatters using the `Y`, `W`, or `w` field specifiers use the JDK locale definition +of weekyears. This can result in different values between the built-in formats and custom formats +for week dates. [[built-in-date-formats]] ==== Built In Formats @@ -256,31 +261,37 @@ The following tables lists all the defaults ISO formats supported: `week_date` or `strict_week_date`:: 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`. + weekyear, and one digit day of week: `YYYY-'W'ww-e`. + This uses the ISO week-date definition. `week_date_time` or `strict_week_date_time`:: A formatter that combines a full weekyear date and time, separated by a - 'T': `xxxx-'W'ww-e'T'HH:mm:ss.SSSZ`. + 'T': `YYYY-'W'ww-e'T'HH:mm:ss.SSSZ`. + This uses the ISO week-date definition. `week_date_time_no_millis` or `strict_week_date_time_no_millis`:: A formatter that combines a full weekyear date and time without millis, - separated by a 'T': `xxxx-'W'ww-e'T'HH:mm:ssZ`. + separated by a 'T': `YYYY-'W'ww-e'T'HH:mm:ssZ`. + This uses the ISO week-date definition. `weekyear` or `strict_weekyear`:: - A formatter for a four digit weekyear: `xxxx`. + A formatter for a four digit weekyear: `YYYY`. + This uses the ISO week-date definition. `weekyear_week` or `strict_weekyear_week`:: A formatter for a four digit weekyear and two digit week of weekyear: - `xxxx-'W'ww`. + `YYYY-'W'ww`. + This uses the ISO week-date definition. `weekyear_week_day` or `strict_weekyear_week_day`:: A formatter for a four digit weekyear, two digit week of weekyear, and one - digit day of week: `xxxx-'W'ww-e`. + digit day of week: `YYYY-'W'ww-e`. + This uses the ISO week-date definition. `year` or `strict_year`:: diff --git a/docs/reference/mapping/types/date.asciidoc b/docs/reference/mapping/types/date.asciidoc index a29db79167d2e..44e1c2949775e 100644 --- a/docs/reference/mapping/types/date.asciidoc +++ b/docs/reference/mapping/types/date.asciidoc @@ -126,7 +126,7 @@ The following parameters are accepted by `date` fields: The locale to use when parsing dates since months do not have the same names and/or abbreviations in all languages. The default is the - https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html#ROOT[`ROOT` locale], + https://docs.oracle.com/javase/8/docs/api/java/util/Locale.html#ROOT[`ROOT` locale]. <>:: From eb074707f9ebf4fd0bd36669aad5d889b0af3552 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Tue, 1 Oct 2024 17:02:58 +0100 Subject: [PATCH 3/3] Add a test for built-in week formats --- .../common/time/DateFormattersTests.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java index dfe3cf10fd494..b197fc3d5dc25 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java @@ -112,6 +112,24 @@ public void testWeekBasedDates() { assertThat(DateFormatters.from(dateFormatter.parse("2016")), equalTo(ZonedDateTime.of(2015, 12, 27, 0, 0, 0, 0, ZoneOffset.UTC))); assertThat(DateFormatters.from(dateFormatter.parse("2015")), equalTo(ZonedDateTime.of(2014, 12, 28, 0, 0, 0, 0, ZoneOffset.UTC))); + + // the built-in formats use different week definitions (ISO instead of locale) + dateFormatter = DateFormatters.forPattern("weekyear_week"); + + assertThat( + DateFormatters.from(dateFormatter.parse("2016-W01")), + equalTo(ZonedDateTime.of(2016, 01, 04, 0, 0, 0, 0, ZoneOffset.UTC)) + ); + + assertThat( + DateFormatters.from(dateFormatter.parse("2015-W01")), + equalTo(ZonedDateTime.of(2014, 12, 29, 0, 0, 0, 0, ZoneOffset.UTC)) + ); + + dateFormatter = DateFormatters.forPattern("weekyear"); + + assertThat(DateFormatters.from(dateFormatter.parse("2016")), equalTo(ZonedDateTime.of(2016, 01, 04, 0, 0, 0, 0, ZoneOffset.UTC))); + assertThat(DateFormatters.from(dateFormatter.parse("2015")), equalTo(ZonedDateTime.of(2014, 12, 29, 0, 0, 0, 0, ZoneOffset.UTC))); } public void testEpochMillisParser() {