Skip to content

Commit aaa6209

Browse files
authored
[7.x] [Java.time] Calculate week of a year with ISO rules BACKPORT(#48209) (#48349)
Reverting the change introducing IsoLocal.ROOT and introducing IsoCalendarDataProvider that defaults start of the week to Monday and requires minimum 4 days in first week of a year. This extension is using java SPI mechanism and defaults for Locale.ROOT only. It require jvm property java.locale.providers to be set with SPI,COMPAT closes #41670 backport #48209
1 parent 9c75f14 commit aaa6209

File tree

15 files changed

+155
-75
lines changed

15 files changed

+155
-75
lines changed

buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy

+7
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,12 @@ class BuildPlugin implements Plugin<Project> {
842842
if ((ext.get('runtimeJavaVersion') as JavaVersion) >= JavaVersion.VERSION_1_9) {
843843
test.jvmArgs '--illegal-access=warn'
844844
}
845+
//TODO remove once jvm.options are added to test system properties
846+
if ((ext.get('runtimeJavaVersion') as JavaVersion) == JavaVersion.VERSION_1_8) {
847+
test.systemProperty ('java.locale.providers','SPI,JRE')
848+
} else if ((ext.get('runtimeJavaVersion') as JavaVersion) >= JavaVersion.VERSION_1_9) {
849+
test.systemProperty ('java.locale.providers','SPI,COMPAT')
850+
}
845851
}
846852

847853
test.jvmArgumentProviders.add(nonInputProperties)
@@ -876,6 +882,7 @@ class BuildPlugin implements Plugin<Project> {
876882
'tests.security.manager': 'true',
877883
'jna.nosys': 'true'
878884

885+
879886
// ignore changing test seed when build is passed -Dignore.tests.seed for cacheability experimentation
880887
if (System.getProperty('ignore.tests.seed') != null) {
881888
nonInputProperties.systemProperty('tests.seed', project.property('testSeed'))

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

+34-29
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import java.time.temporal.ChronoField;
3838
import java.time.temporal.IsoFields;
3939
import java.time.temporal.TemporalAccessor;
40-
import java.time.temporal.TemporalAdjusters;
4140
import java.time.temporal.TemporalQueries;
4241
import java.time.temporal.TemporalQuery;
4342
import java.time.temporal.WeekFields;
@@ -53,6 +52,7 @@
5352
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
5453

5554
public class DateFormatters {
55+
public static final WeekFields WEEK_FIELDS = WeekFields.of(DayOfWeek.MONDAY,4);
5656

5757
private static final DateTimeFormatter TIME_ZONE_FORMATTER_NO_COLON = new DateTimeFormatterBuilder()
5858
.appendOffset("+HHmm", "Z")
@@ -946,14 +946,14 @@ public class DateFormatters {
946946
* Returns a formatter for a four digit weekyear
947947
*/
948948
private static final DateFormatter STRICT_WEEKYEAR = new JavaDateFormatter("strict_weekyear", new DateTimeFormatterBuilder()
949-
.appendValue(WeekFields.ISO.weekBasedYear(), 4, 10, SignStyle.EXCEEDS_PAD)
949+
.appendValue(WEEK_FIELDS.weekBasedYear(), 4, 10, SignStyle.EXCEEDS_PAD)
950950
.toFormatter(Locale.ROOT)
951951
.withResolverStyle(ResolverStyle.STRICT));
952952

953953
private static final DateTimeFormatter STRICT_WEEKYEAR_WEEK_FORMATTER = new DateTimeFormatterBuilder()
954-
.appendValue(WeekFields.ISO.weekBasedYear(), 4, 10, SignStyle.EXCEEDS_PAD)
954+
.appendValue(WEEK_FIELDS.weekBasedYear(), 4, 10, SignStyle.EXCEEDS_PAD)
955955
.appendLiteral("-W")
956-
.appendValue(WeekFields.ISO.weekOfWeekBasedYear(), 2, 2, SignStyle.NOT_NEGATIVE)
956+
.appendValue(WEEK_FIELDS.weekOfWeekBasedYear(), 2, 2, SignStyle.NOT_NEGATIVE)
957957
.toFormatter(Locale.ROOT)
958958
.withResolverStyle(ResolverStyle.STRICT);
959959

@@ -972,7 +972,7 @@ public class DateFormatters {
972972
new DateTimeFormatterBuilder()
973973
.append(STRICT_WEEKYEAR_WEEK_FORMATTER)
974974
.appendLiteral("-")
975-
.appendValue(WeekFields.ISO.dayOfWeek())
975+
.appendValue(WEEK_FIELDS.dayOfWeek())
976976
.toFormatter(Locale.ROOT)
977977
.withResolverStyle(ResolverStyle.STRICT));
978978

@@ -1162,7 +1162,7 @@ public class DateFormatters {
11621162
* Returns a formatter for a four digit weekyear. (YYYY)
11631163
*/
11641164
private static final DateFormatter WEEK_YEAR = new JavaDateFormatter("week_year",
1165-
new DateTimeFormatterBuilder().appendValue(WeekFields.ISO.weekBasedYear()).toFormatter(Locale.ROOT)
1165+
new DateTimeFormatterBuilder().appendValue(WEEK_FIELDS.weekBasedYear()).toFormatter(Locale.ROOT)
11661166
.withResolverStyle(ResolverStyle.STRICT));
11671167

11681168
/*
@@ -1591,9 +1591,9 @@ public class DateFormatters {
15911591
*/
15921592
private static final DateFormatter WEEKYEAR_WEEK = new JavaDateFormatter("weekyear_week", STRICT_WEEKYEAR_WEEK_FORMATTER,
15931593
new DateTimeFormatterBuilder()
1594-
.appendValue(WeekFields.ISO.weekBasedYear())
1594+
.appendValue(WEEK_FIELDS.weekBasedYear())
15951595
.appendLiteral("-W")
1596-
.appendValue(WeekFields.ISO.weekOfWeekBasedYear())
1596+
.appendValue(WEEK_FIELDS.weekOfWeekBasedYear())
15971597
.toFormatter(Locale.ROOT)
15981598
.withResolverStyle(ResolverStyle.STRICT)
15991599
);
@@ -1606,15 +1606,15 @@ public class DateFormatters {
16061606
new DateTimeFormatterBuilder()
16071607
.append(STRICT_WEEKYEAR_WEEK_FORMATTER)
16081608
.appendLiteral("-")
1609-
.appendValue(WeekFields.ISO.dayOfWeek())
1609+
.appendValue(WEEK_FIELDS.dayOfWeek())
16101610
.toFormatter(Locale.ROOT)
16111611
.withResolverStyle(ResolverStyle.STRICT),
16121612
new DateTimeFormatterBuilder()
1613-
.appendValue(WeekFields.ISO.weekBasedYear())
1613+
.appendValue(WEEK_FIELDS.weekBasedYear())
16141614
.appendLiteral("-W")
1615-
.appendValue(WeekFields.ISO.weekOfWeekBasedYear())
1615+
.appendValue(WEEK_FIELDS.weekOfWeekBasedYear())
16161616
.appendLiteral("-")
1617-
.appendValue(WeekFields.ISO.dayOfWeek())
1617+
.appendValue(WEEK_FIELDS.dayOfWeek())
16181618
.toFormatter(Locale.ROOT)
16191619
.withResolverStyle(ResolverStyle.STRICT)
16201620
);
@@ -1858,7 +1858,7 @@ public static ZonedDateTime from(TemporalAccessor accessor) {
18581858
} else if (isLocalDateSet) {
18591859
return localDate.atStartOfDay(zoneId);
18601860
} else if (isLocalTimeSet) {
1861-
return of(getLocaldate(accessor), localTime, zoneId);
1861+
return of(getLocalDate(accessor), localTime, zoneId);
18621862
} else if (accessor.isSupported(ChronoField.YEAR) || accessor.isSupported(ChronoField.YEAR_OF_ERA) ) {
18631863
if (accessor.isSupported(MONTH_OF_YEAR)) {
18641864
return getFirstOfMonth(accessor).atStartOfDay(zoneId);
@@ -1868,26 +1868,29 @@ public static ZonedDateTime from(TemporalAccessor accessor) {
18681868
}
18691869
} else if (accessor.isSupported(MONTH_OF_YEAR)) {
18701870
// missing year, falling back to the epoch and then filling
1871-
return getLocaldate(accessor).atStartOfDay(zoneId);
1872-
} else if (accessor.isSupported(WeekFields.ISO.weekBasedYear())) {
1873-
if (accessor.isSupported(WeekFields.ISO.weekOfWeekBasedYear())) {
1874-
return Year.of(accessor.get(WeekFields.ISO.weekBasedYear()))
1875-
.atDay(1)
1876-
.with(WeekFields.ISO.weekOfWeekBasedYear(), accessor.getLong(WeekFields.ISO.weekOfWeekBasedYear()))
1877-
.atStartOfDay(zoneId);
1878-
} else {
1879-
return Year.of(accessor.get(WeekFields.ISO.weekBasedYear()))
1880-
.atDay(1)
1881-
.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY))
1882-
.atStartOfDay(zoneId);
1883-
}
1871+
return getLocalDate(accessor).atStartOfDay(zoneId);
1872+
} else if (accessor.isSupported(WEEK_FIELDS.weekBasedYear())) {
1873+
return localDateFromWeekBasedDate(accessor).atStartOfDay(zoneId);
18841874
}
18851875

18861876
// we should not reach this piece of code, everything being parsed we should be able to
18871877
// convert to a zoned date time! If not, we have to extend the above methods
18881878
throw new IllegalArgumentException("temporal accessor [" + accessor + "] cannot be converted to zoned date time");
18891879
}
18901880

1881+
private static LocalDate localDateFromWeekBasedDate(TemporalAccessor accessor) {
1882+
if (accessor.isSupported(WEEK_FIELDS.weekOfWeekBasedYear())) {
1883+
return LocalDate.ofEpochDay(0)
1884+
.with(WEEK_FIELDS.weekBasedYear(), accessor.get(WEEK_FIELDS.weekBasedYear()))
1885+
.with(WEEK_FIELDS.weekOfWeekBasedYear(), accessor.get(WEEK_FIELDS.weekOfWeekBasedYear()))
1886+
.with(ChronoField.DAY_OF_WEEK, WEEK_FIELDS.getFirstDayOfWeek().getValue());
1887+
} else {
1888+
return LocalDate.ofEpochDay(0)
1889+
.with(WEEK_FIELDS.weekBasedYear(), accessor.get(WEEK_FIELDS.weekBasedYear()))
1890+
.with(ChronoField.DAY_OF_WEEK, WEEK_FIELDS.getFirstDayOfWeek().getValue());
1891+
}
1892+
}
1893+
18911894
/**
18921895
* extending the java.time.temporal.TemporalQueries.LOCAL_DATE implementation to also create local dates
18931896
* when YearOfEra was used instead of Year.
@@ -1915,9 +1918,11 @@ public String toString() {
19151918
}
19161919
};
19171920

1918-
private static LocalDate getLocaldate(TemporalAccessor accessor) {
1919-
int year = getYear(accessor);
1920-
if (accessor.isSupported(MONTH_OF_YEAR)) {
1921+
private static LocalDate getLocalDate(TemporalAccessor accessor) {
1922+
if (accessor.isSupported(WEEK_FIELDS.weekBasedYear())) {
1923+
return localDateFromWeekBasedDate(accessor);
1924+
} else if (accessor.isSupported(MONTH_OF_YEAR)) {
1925+
int year = getYear(accessor);
19211926
if (accessor.isSupported(DAY_OF_MONTH)) {
19221927
return LocalDate.of(year, accessor.get(MONTH_OF_YEAR), accessor.get(DAY_OF_MONTH));
19231928
} else {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.common.time;
20+
21+
import java.util.Calendar;
22+
import java.util.Locale;
23+
import java.util.spi.CalendarDataProvider;
24+
25+
public class IsoCalendarDataProvider extends CalendarDataProvider {
26+
27+
@Override
28+
public int getFirstDayOfWeek(Locale locale) {
29+
return Calendar.MONDAY;
30+
}
31+
32+
@Override
33+
public int getMinimalDaysInFirstWeek(Locale locale) {
34+
return 4;
35+
}
36+
37+
@Override
38+
public Locale[] getAvailableLocales() {
39+
return new Locale[]{Locale.ROOT};
40+
}
41+
}

server/src/main/java/org/elasticsearch/script/JodaCompatibleZonedDateTime.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.elasticsearch.common.SuppressForbidden;
2424
import org.elasticsearch.common.logging.DeprecationLogger;
2525
import org.elasticsearch.common.time.DateFormatter;
26+
import org.elasticsearch.common.time.DateFormatters;
2627
import org.elasticsearch.common.time.DateUtils;
2728
import org.joda.time.DateTime;
2829

@@ -50,7 +51,6 @@
5051
import java.time.temporal.TemporalQuery;
5152
import java.time.temporal.TemporalUnit;
5253
import java.time.temporal.ValueRange;
53-
import java.time.temporal.WeekFields;
5454
import java.util.Locale;
5555
import java.util.Objects;
5656

@@ -474,14 +474,14 @@ public int getSecondOfMinute() {
474474

475475
@Deprecated
476476
public int getWeekOfWeekyear() {
477-
logDeprecatedMethod("getWeekOfWeekyear()", "get(WeekFields.ISO.weekOfWeekBasedYear())");
478-
return dt.get(WeekFields.ISO.weekOfWeekBasedYear());
477+
logDeprecatedMethod("getWeekOfWeekyear()", "get(DateFormatters.WEEK_FIELDS.weekOfWeekBasedYear())");
478+
return dt.get(DateFormatters.WEEK_FIELDS.weekOfWeekBasedYear());
479479
}
480480

481481
@Deprecated
482482
public int getWeekyear() {
483-
logDeprecatedMethod("getWeekyear()", "get(WeekFields.ISO.weekBasedYear())");
484-
return dt.get(WeekFields.ISO.weekBasedYear());
483+
logDeprecatedMethod("getWeekyear()", "get(DateFormatters.WEEK_FIELDS.weekBasedYear())");
484+
return dt.get(DateFormatters.WEEK_FIELDS.weekBasedYear());
485485
}
486486

487487
@Deprecated
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.elasticsearch.common.time.IsoCalendarDataProvider

server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@
3737
import java.time.temporal.TemporalAccessor;
3838
import java.util.Locale;
3939

40+
import static org.hamcrest.CoreMatchers.equalTo;
4041
import static org.hamcrest.Matchers.containsString;
4142
import static org.hamcrest.Matchers.is;
42-
import static org.hamcrest.core.IsEqual.equalTo;
4343

4444
public class JavaJodaTimeDuellingTests extends ESTestCase {
4545
@Override

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

+30
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.elasticsearch.common.time;
2121

22+
import org.elasticsearch.bootstrap.JavaVersion;
2223
import org.elasticsearch.test.ESTestCase;
2324

2425
import java.time.Clock;
@@ -40,6 +41,35 @@
4041

4142
public class DateFormattersTests extends ESTestCase {
4243

44+
public void testWeekBasedDates() {
45+
assumeFalse("won't work in jdk8 " +
46+
"because SPI mechanism is not looking at classpath - needs ISOCalendarDataProvider in jre's ext/libs",
47+
JavaVersion.current().equals(JavaVersion.parse("8")));
48+
49+
// as per WeekFields.ISO first week starts on Monday and has minimum 4 days
50+
DateFormatter dateFormatter = DateFormatters.forPattern("YYYY-ww");
51+
52+
// first week of 2016 starts on Monday 2016-01-04 as previous week in 2016 has only 3 days
53+
assertThat(DateFormatters.from(dateFormatter.parse("2016-01")) ,
54+
equalTo(ZonedDateTime.of(2016,01,04, 0,0,0,0,ZoneOffset.UTC)));
55+
56+
// first week of 2015 starts on Monday 2014-12-29 because 4days belong to 2019
57+
assertThat(DateFormatters.from(dateFormatter.parse("2015-01")) ,
58+
equalTo(ZonedDateTime.of(2014,12,29, 0,0,0,0,ZoneOffset.UTC)));
59+
60+
61+
// as per WeekFields.ISO first week starts on Monday and has minimum 4 days
62+
dateFormatter = DateFormatters.forPattern("YYYY");
63+
64+
// first week of 2016 starts on Monday 2016-01-04 as previous week in 2016 has only 3 days
65+
assertThat(DateFormatters.from(dateFormatter.parse("2016")) ,
66+
equalTo(ZonedDateTime.of(2016,01,04, 0,0,0,0,ZoneOffset.UTC)));
67+
68+
// first week of 2015 starts on Monday 2014-12-29 because 4days belong to 2019
69+
assertThat(DateFormatters.from(dateFormatter.parse("2015")) ,
70+
equalTo(ZonedDateTime.of(2014,12,29, 0,0,0,0,ZoneOffset.UTC)));
71+
}
72+
4373
// this is not in the duelling tests, because the epoch millis parser in joda time drops the milliseconds after the comma
4474
// but is able to parse the rest
4575
// as this feature is supported it also makes sense to make it exact

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

+19
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.common.time;
2121

2222
import org.elasticsearch.ElasticsearchParseException;
23+
import org.elasticsearch.bootstrap.JavaVersion;
2324
import org.elasticsearch.test.ESTestCase;
2425

2526
import java.time.Instant;
@@ -50,6 +51,24 @@ public void testOverridingLocaleOrZoneAndCompositeRoundUpParser() {
5051
assertDateEquals(gotMillis, "297276785531", "297276785531");
5152
}
5253

54+
public void testWeekDates() {
55+
assumeFalse("won't work in jdk8 " +
56+
"because SPI mechanism is not looking at classpath - needs ISOCalendarDataProvider in jre's ext/libs",
57+
JavaVersion.current().equals(JavaVersion.parse("8")));
58+
59+
DateFormatter formatter = DateFormatter.forPattern("YYYY-ww");
60+
assertDateMathEquals(formatter.toDateMathParser(), "2016-01", "2016-01-04T23:59:59.999Z", 0, true, ZoneOffset.UTC);
61+
62+
formatter = DateFormatter.forPattern("YYYY");
63+
assertDateMathEquals(formatter.toDateMathParser(), "2016", "2016-01-04T23:59:59.999Z", 0, true, ZoneOffset.UTC);
64+
65+
formatter = DateFormatter.forPattern("YYYY-ww");
66+
assertDateMathEquals(formatter.toDateMathParser(), "2015-01", "2014-12-29T23:59:59.999Z", 0, true, ZoneOffset.UTC);
67+
68+
formatter = DateFormatter.forPattern("YYYY");
69+
assertDateMathEquals(formatter.toDateMathParser(), "2015", "2014-12-29T23:59:59.999Z", 0, true, ZoneOffset.UTC);
70+
}
71+
5372
public void testBasicDates() {
5473
assertDateMathEquals("2014-05-30", "2014-05-30T00:00:00.000");
5574
assertDateMathEquals("2014-05-30T20", "2014-05-30T20:00:00.000");

server/src/test/java/org/elasticsearch/index/mapper/DateFieldMapperTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ public void testChangeLocale() throws IOException {
235235
mapper.parse(new SourceToParse("test", "type", "1", BytesReference
236236
.bytes(XContentFactory.jsonBuilder()
237237
.startObject()
238-
.field("field", "Mi., 06 Dez. 2000 02:55:00 -0800")
238+
.field("field", "Mi, 06 Dez 2000 02:55:00 -0800")
239239
.endObject()),
240240
XContentType.JSON));
241241
}

server/src/test/java/org/elasticsearch/script/JodaCompatibleZonedDateTimeTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,12 @@ public void testSecondOfMinute() {
213213

214214
public void testWeekOfWeekyear() {
215215
assertMethodDeprecation(() -> assertThat(javaTime.getWeekOfWeekyear(), equalTo(jodaTime.getWeekOfWeekyear())),
216-
"getWeekOfWeekyear()", "get(WeekFields.ISO.weekOfWeekBasedYear())");
216+
"getWeekOfWeekyear()", "get(DateFormatters.WEEK_FIELDS.weekOfWeekBasedYear())");
217217
}
218218

219219
public void testWeekyear() {
220220
assertMethodDeprecation(() -> assertThat(javaTime.getWeekyear(), equalTo(jodaTime.getWeekyear())),
221-
"getWeekyear()", "get(WeekFields.ISO.weekBasedYear())");
221+
"getWeekyear()", "get(DateFormatters.WEEK_FIELDS.weekBasedYear())");
222222
}
223223

224224
public void testYearOfCentury() {

server/src/test/java/org/elasticsearch/search/query/SearchQueryIT.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -1678,21 +1678,21 @@ public void testRangeQueryWithLocaleMapping() throws Exception {
16781678
.endObject().endObject().endObject()));
16791679

16801680
indexRandom(true,
1681-
client().prepareIndex("test", "type1", "1").setSource("date_field", "Mi., 06 Dez. 2000 02:55:00 -0800"),
1682-
client().prepareIndex("test", "type1", "2").setSource("date_field", "Do., 07 Dez. 2000 02:55:00 -0800")
1681+
client().prepareIndex("test", "type1", "1").setSource("date_field", "Mi, 06 Dez 2000 02:55:00 -0800"),
1682+
client().prepareIndex("test", "type1", "2").setSource("date_field", "Do, 07 Dez 2000 02:55:00 -0800")
16831683
);
16841684

16851685
SearchResponse searchResponse = client().prepareSearch("test")
16861686
.setQuery(QueryBuilders.rangeQuery("date_field")
1687-
.gte("Di., 05 Dez. 2000 02:55:00 -0800")
1688-
.lte("Do., 07 Dez. 2000 00:00:00 -0800"))
1687+
.gte("Di, 05 Dez 2000 02:55:00 -0800")
1688+
.lte("Do, 07 Dez 2000 00:00:00 -0800"))
16891689
.get();
16901690
assertHitCount(searchResponse, 1L);
16911691

16921692
searchResponse = client().prepareSearch("test")
16931693
.setQuery(QueryBuilders.rangeQuery("date_field")
1694-
.gte("Di., 05 Dez. 2000 02:55:00 -0800")
1695-
.lte("Fr., 08 Dez. 2000 00:00:00 -0800"))
1694+
.gte("Di, 05 Dez 2000 02:55:00 -0800")
1695+
.lte("Fr, 08 Dez 2000 00:00:00 -0800"))
16961696
.get();
16971697
assertHitCount(searchResponse, 2L);
16981698
}

0 commit comments

Comments
 (0)