Skip to content

Commit 8d1ea86

Browse files
authored
Set start of the week to Monday for root locale (#43652)
Introducing a IsoLocal.ROOT constant which should be used instead of java.util.Locale.ROOT in ES when dealing with dates. IsoLocal.ROOT customises start of the week to be Monday instead of Sunday. closes #42588 an issue with investigation details relates #41670 bug raised (this won't fix it on its own. joda.parseInto has to be reimplemented closes #43275 an issue raised by community member
1 parent af70fb9 commit 8d1ea86

File tree

10 files changed

+343
-207
lines changed

10 files changed

+343
-207
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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.Locale;
22+
23+
/**
24+
* Locale constants to be used across elasticsearch code base.
25+
* java.util.Locale.ROOT should not be used as it defaults start of the week incorrectly to Sunday.
26+
*/
27+
public final class IsoLocale {
28+
private IsoLocale() {
29+
throw new UnsupportedOperationException();
30+
}
31+
32+
/**
33+
* We want to use Locale.ROOT but with a start of the week as defined in ISO8601 to be compatible with the behaviour in joda-time
34+
* https://github.com/elastic/elasticsearch/issues/42588
35+
* @see java.time.temporal.WeekFields#of(Locale)
36+
*/
37+
public static final Locale ROOT = new Locale.Builder()
38+
.setLocale(Locale.ROOT)
39+
.setUnicodeLocaleKeyword("fw", "mon").build();
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
setup:
3+
- skip:
4+
version: " - 7.99.99" #TODO change this after backport
5+
reason: "not backported yet"
6+
7+
- do:
8+
indices.create:
9+
index: test
10+
body:
11+
mappings:
12+
properties:
13+
date:
14+
type: date
15+
16+
- do:
17+
index:
18+
index: test
19+
id: 1
20+
body: { "date": "2009-11-15T14:12:12" }
21+
22+
- do:
23+
indices.refresh:
24+
index: [test]
25+
26+
---
27+
# The inserted document has a field date=2009-11-15T14:12:12 which is Sunday.
28+
# When aggregating per day of the week this should be considered as last day of the week (7)
29+
# and this value should be used in 'key_as_string'
30+
"Date aggregartion per day of week":
31+
- do:
32+
search:
33+
rest_total_hits_as_int: true
34+
index: test
35+
body:
36+
aggregations:
37+
test:
38+
"date_histogram": {
39+
"field": "date",
40+
"calendar_interval": "day",
41+
"format": "e",
42+
"offset": 0
43+
}
44+
45+
- match: {hits.total: 1}
46+
- length: { aggregations.test.buckets: 1 }
47+
- match: { aggregations.test.buckets.0.key_as_string: "7" }

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

+216-193
Large diffs are not rendered by default.

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -125,27 +125,27 @@ public long getFrom(TemporalAccessor temporal) {
125125
.optionalStart() // optional is used so isSupported will be called when printing
126126
.appendFraction(NANOS_OF_SECOND, 0, 9, true)
127127
.optionalEnd()
128-
.toFormatter(Locale.ROOT);
128+
.toFormatter(IsoLocale.ROOT);
129129

130130
// this supports seconds ending in dot
131131
private static final DateTimeFormatter SECONDS_FORMATTER2 = new DateTimeFormatterBuilder()
132132
.appendValue(SECONDS, 1, 19, SignStyle.NORMAL)
133133
.appendLiteral('.')
134-
.toFormatter(Locale.ROOT);
134+
.toFormatter(IsoLocale.ROOT);
135135

136136
// this supports milliseconds without any fraction
137137
private static final DateTimeFormatter MILLISECONDS_FORMATTER1 = new DateTimeFormatterBuilder()
138138
.appendValue(MILLIS, 1, 19, SignStyle.NORMAL)
139139
.optionalStart()
140140
.appendFraction(NANOS_OF_MILLI, 0, 6, true)
141141
.optionalEnd()
142-
.toFormatter(Locale.ROOT);
142+
.toFormatter(IsoLocale.ROOT);
143143

144144
// this supports milliseconds ending in dot
145145
private static final DateTimeFormatter MILLISECONDS_FORMATTER2 = new DateTimeFormatterBuilder()
146146
.append(MILLISECONDS_FORMATTER1)
147147
.appendLiteral('.')
148-
.toFormatter(Locale.ROOT);
148+
.toFormatter(IsoLocale.ROOT);
149149

150150
static final DateFormatter SECONDS_FORMATTER = new JavaDateFormatter("epoch_second", SECONDS_FORMATTER1,
151151
builder -> builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 999_999_999L),

server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.elasticsearch.common.time.DateFormatters;
4444
import org.elasticsearch.common.time.DateMathParser;
4545
import org.elasticsearch.common.time.DateUtils;
46+
import org.elasticsearch.common.time.IsoLocale;
4647
import org.elasticsearch.common.util.LocaleUtils;
4748
import org.elasticsearch.common.xcontent.XContentBuilder;
4849
import org.elasticsearch.common.xcontent.support.XContentMapValues;
@@ -135,7 +136,7 @@ public static class Builder extends FieldMapper.Builder<Builder, DateFieldMapper
135136
public Builder(String name) {
136137
super(name, new DateFieldType(), new DateFieldType());
137138
builder = this;
138-
locale = Locale.ROOT;
139+
locale = IsoLocale.ROOT;
139140
}
140141

141142
@Override

server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.elasticsearch.common.collect.CopyOnWriteHashMap;
3030
import org.elasticsearch.common.logging.DeprecationLogger;
3131
import org.elasticsearch.common.settings.Settings;
32+
import org.elasticsearch.common.time.IsoLocale;
3233
import org.elasticsearch.common.xcontent.ToXContent;
3334
import org.elasticsearch.common.xcontent.XContentBuilder;
3435
import org.elasticsearch.common.xcontent.support.XContentMapValues;
@@ -41,7 +42,6 @@
4142
import java.util.HashMap;
4243
import java.util.Iterator;
4344
import java.util.List;
44-
import java.util.Locale;
4545
import java.util.Map;
4646

4747
public class ObjectMapper extends Mapper implements Cloneable {
@@ -530,7 +530,7 @@ public void toXContent(XContentBuilder builder, Params params, ToXContent custom
530530
builder.field("type", CONTENT_TYPE);
531531
}
532532
if (dynamic != null) {
533-
builder.field("dynamic", dynamic.name().toLowerCase(Locale.ROOT));
533+
builder.field("dynamic", dynamic.name().toLowerCase(IsoLocale.ROOT));
534534
}
535535
if (enabled != Defaults.ENABLED) {
536536
builder.field("enabled", enabled);

server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.elasticsearch.common.settings.Settings;
5252
import org.elasticsearch.common.time.DateFormatter;
5353
import org.elasticsearch.common.time.DateMathParser;
54+
import org.elasticsearch.common.time.IsoLocale;
5455
import org.elasticsearch.common.util.LocaleUtils;
5556
import org.elasticsearch.common.xcontent.XContentBuilder;
5657
import org.elasticsearch.common.xcontent.XContentParser;
@@ -94,7 +95,7 @@ public static class Defaults {
9495

9596
public static class Builder extends FieldMapper.Builder<Builder, RangeFieldMapper> {
9697
private Boolean coerce;
97-
private Locale locale = Locale.ROOT;
98+
private Locale locale = IsoLocale.ROOT;
9899
private String pattern;
99100

100101
public Builder(String name, RangeType type) {
@@ -413,7 +414,7 @@ && fieldType().dateTimeFormatter().pattern().equals(DateFieldMapper.DEFAULT_DATE
413414
}
414415
if (fieldType().rangeType == RangeType.DATE
415416
&& (includeDefaults || (fieldType().dateTimeFormatter() != null
416-
&& fieldType().dateTimeFormatter().locale() != Locale.ROOT))) {
417+
&& fieldType().dateTimeFormatter().locale() != IsoLocale.ROOT))) {
417418
builder.field("locale", fieldType().dateTimeFormatter().locale());
418419
}
419420
if (includeDefaults || coerce.explicit()) {

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

+24
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.joda.time.DateTimeZone;
2828
import org.joda.time.format.ISODateTimeFormat;
2929

30+
import java.time.LocalDateTime;
3031
import java.time.ZoneOffset;
3132
import java.time.ZonedDateTime;
3233
import java.time.format.DateTimeFormatter;
@@ -35,8 +36,31 @@
3536

3637
import static org.hamcrest.Matchers.containsString;
3738
import static org.hamcrest.Matchers.is;
39+
import static org.hamcrest.core.IsEqual.equalTo;
3840

3941
public class JavaJodaTimeDuellingTests extends ESTestCase {
42+
@Override
43+
protected boolean enableWarningsCheck() {
44+
return false;
45+
}
46+
47+
public void testDayOfWeek() {
48+
//7 (ok joda) vs 1 (java by default) but 7 with customized org.elasticsearch.common.time.IsoLocale.ISO8601
49+
ZonedDateTime now = LocalDateTime.of(2009,11,15,1,32,8,328402)
50+
.atZone(ZoneOffset.UTC); //Sunday
51+
DateFormatter jodaFormatter = Joda.forPattern("e").withLocale(Locale.ROOT).withZone(ZoneOffset.UTC);
52+
DateFormatter javaFormatter = DateFormatter.forPattern("8e").withZone(ZoneOffset.UTC);
53+
assertThat(jodaFormatter.format(now), equalTo(javaFormatter.format(now)));
54+
}
55+
56+
public void testStartOfWeek() {
57+
//2019-21 (ok joda) vs 2019-22 (java by default) but 2019-21 with customized org.elasticsearch.common.time.IsoLocale.ISO8601
58+
ZonedDateTime now = LocalDateTime.of(2019,5,26,1,32,8,328402)
59+
.atZone(ZoneOffset.UTC);
60+
DateFormatter jodaFormatter = Joda.forPattern("xxxx-ww").withLocale(Locale.ROOT).withZone(ZoneOffset.UTC);
61+
DateFormatter javaFormatter = DateFormatter.forPattern("8YYYY-ww").withZone(ZoneOffset.UTC);
62+
assertThat(jodaFormatter.format(now), equalTo(javaFormatter.format(now)));
63+
}
4064

4165
//these parsers should allow both ',' and '.' as a decimal point
4266
public void testDecimalPointParsing(){

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public void testParsersWithMultipleInternalFormats() throws Exception {
116116
}
117117

118118
public void testLocales() {
119-
assertThat(DateFormatters.forPattern("strict_date_optional_time").locale(), is(Locale.ROOT));
119+
assertThat(DateFormatters.forPattern("strict_date_optional_time").locale(), is(IsoLocale.ROOT));
120120
Locale locale = randomLocale(random());
121121
assertThat(DateFormatters.forPattern("strict_date_optional_time").withLocale(locale).locale(), is(locale));
122122
}

x-pack/plugin/sql/sql-proto/src/main/java/org/elasticsearch/xpack/sql/proto/StringUtils.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66

77
package org.elasticsearch.xpack.sql.proto;
88

9+
import org.elasticsearch.common.time.IsoLocale;
10+
911
import java.sql.Timestamp;
1012
import java.time.Duration;
1113
import java.time.OffsetTime;
1214
import java.time.Period;
1315
import java.time.ZonedDateTime;
1416
import java.time.format.DateTimeFormatter;
1517
import java.time.format.DateTimeFormatterBuilder;
16-
import java.util.Locale;
1718
import java.util.Objects;
1819
import java.util.concurrent.TimeUnit;
1920

@@ -24,7 +25,6 @@
2425
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
2526

2627
public final class StringUtils {
27-
2828
public static final String EMPTY = "";
2929

3030
public static final DateTimeFormatter ISO_DATE_WITH_MILLIS = new DateTimeFormatterBuilder()
@@ -38,7 +38,7 @@ public final class StringUtils {
3838
.appendValue(SECOND_OF_MINUTE, 2)
3939
.appendFraction(MILLI_OF_SECOND, 3, 3, true)
4040
.appendOffsetId()
41-
.toFormatter(Locale.ROOT);
41+
.toFormatter(IsoLocale.ROOT);
4242

4343
public static final DateTimeFormatter ISO_TIME_WITH_MILLIS = new DateTimeFormatterBuilder()
4444
.parseCaseInsensitive()
@@ -49,7 +49,7 @@ public final class StringUtils {
4949
.appendValue(SECOND_OF_MINUTE, 2)
5050
.appendFraction(MILLI_OF_SECOND, 3, 3, true)
5151
.appendOffsetId()
52-
.toFormatter(Locale.ROOT);
52+
.toFormatter(IsoLocale.ROOT);
5353

5454
private static final int SECONDS_PER_MINUTE = 60;
5555
private static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60;

0 commit comments

Comments
 (0)