Skip to content

Commit 7dc9009

Browse files
spinscalekcm
authored andcommitted
Core: Add methods to get locale/timezone in DateFormatter (#34113)
This adds some method into the `DateFormatter` interface, namely * `withLocale()` to change the locale of a date formatter * `getLocale()` * `getZone()` * `hashCode()` * `equals()` These methods will be needed for aggregations and mapping changes, where zones and locales can be specified in the mapping or in search/aggs parts of a search request.
1 parent 83de344 commit 7dc9009

File tree

6 files changed

+198
-45
lines changed

6 files changed

+198
-45
lines changed

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.time.temporal.TemporalAccessor;
2525
import java.time.temporal.TemporalField;
2626
import java.util.Arrays;
27+
import java.util.Locale;
2728
import java.util.Map;
2829
import java.util.stream.Collectors;
2930

@@ -46,6 +47,14 @@ public interface DateFormatter {
4647
*/
4748
DateFormatter withZone(ZoneId zoneId);
4849

50+
/**
51+
* Create a copy of this formatter that is configured to parse dates in the specified locale
52+
*
53+
* @param locale The local to use for the new formatter
54+
* @return A copy of the date formatter this has been called on
55+
*/
56+
DateFormatter withLocale(Locale locale);
57+
4958
/**
5059
* Print the supplied java time accessor in a string based representation according to this formatter
5160
*
@@ -62,6 +71,20 @@ public interface DateFormatter {
6271
*/
6372
String pattern();
6473

74+
/**
75+
* Returns the configured locale of the date formatter
76+
*
77+
* @return The locale of this formatter
78+
*/
79+
Locale getLocale();
80+
81+
/**
82+
* Returns the configured time zone of the date formatter
83+
*
84+
* @return The time zone of this formatter
85+
*/
86+
ZoneId getZone();
87+
6588
/**
6689
* Configure a formatter using default fields for a TemporalAccessor that should be used in case
6790
* the supplied date is not having all of those fields
@@ -115,6 +138,11 @@ public DateFormatter withZone(ZoneId zoneId) {
115138
return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.withZone(zoneId)).toArray(DateFormatter[]::new));
116139
}
117140

141+
@Override
142+
public DateFormatter withLocale(Locale locale) {
143+
return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.withLocale(locale)).toArray(DateFormatter[]::new));
144+
}
145+
118146
@Override
119147
public String format(TemporalAccessor accessor) {
120148
return formatters[0].format(accessor);
@@ -125,6 +153,16 @@ public String pattern() {
125153
return format;
126154
}
127155

156+
@Override
157+
public Locale getLocale() {
158+
return formatters[0].getLocale();
159+
}
160+
161+
@Override
162+
public ZoneId getZone() {
163+
return formatters[0].getZone();
164+
}
165+
128166
@Override
129167
public DateFormatter parseDefaulting(Map<TemporalField, Long> fields) {
130168
return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.parseDefaulting(fields)).toArray(DateFormatter[]::new));

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1269,7 +1269,7 @@ public static DateFormatter forPattern(String input) {
12691269
return forPattern(input, Locale.ROOT);
12701270
}
12711271

1272-
public static DateFormatter forPattern(String input, Locale locale) {
1272+
private static DateFormatter forPattern(String input, Locale locale) {
12731273
if (Strings.hasLength(input)) {
12741274
input = input.trim();
12751275
}

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.time.format.DateTimeParseException;
2626
import java.time.temporal.TemporalAccessor;
2727
import java.time.temporal.TemporalField;
28+
import java.util.Locale;
2829
import java.util.Map;
2930

3031
/**
@@ -40,7 +41,8 @@ class EpochMillisDateFormatter implements DateFormatter {
4041

4142
public static DateFormatter INSTANCE = new EpochMillisDateFormatter();
4243

43-
private EpochMillisDateFormatter() {}
44+
private EpochMillisDateFormatter() {
45+
}
4446

4547
@Override
4648
public TemporalAccessor parse(String input) {
@@ -53,6 +55,17 @@ public TemporalAccessor parse(String input) {
5355

5456
@Override
5557
public DateFormatter withZone(ZoneId zoneId) {
58+
if (ZoneOffset.UTC.equals(zoneId) == false) {
59+
throw new IllegalArgumentException(pattern() + " date formatter can only be in zone offset UTC");
60+
}
61+
return INSTANCE;
62+
}
63+
64+
@Override
65+
public DateFormatter withLocale(Locale locale) {
66+
if (Locale.ROOT.equals(locale) == false) {
67+
throw new IllegalArgumentException(pattern() + " date formatter can only be in locale ROOT");
68+
}
5669
return this;
5770
}
5871

@@ -70,4 +83,14 @@ public String pattern() {
7083
public DateFormatter parseDefaulting(Map<TemporalField, Long> fields) {
7184
return this;
7285
}
86+
87+
@Override
88+
public Locale getLocale() {
89+
return Locale.ROOT;
90+
}
91+
92+
@Override
93+
public ZoneId getZone() {
94+
return ZoneOffset.UTC;
95+
}
7396
}

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

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.time.format.DateTimeParseException;
2727
import java.time.temporal.TemporalAccessor;
2828
import java.time.temporal.TemporalField;
29+
import java.util.Locale;
2930
import java.util.Map;
3031
import java.util.regex.Pattern;
3132

@@ -59,11 +60,6 @@ public TemporalAccessor parse(String input) {
5960
}
6061
}
6162

62-
@Override
63-
public DateFormatter withZone(ZoneId zoneId) {
64-
return this;
65-
}
66-
6763
@Override
6864
public String format(TemporalAccessor accessor) {
6965
Instant instant = Instant.from(accessor);
@@ -75,7 +71,33 @@ public String format(TemporalAccessor accessor) {
7571

7672
@Override
7773
public String pattern() {
78-
return "epoch_seconds";
74+
return "epoch_second";
75+
}
76+
77+
@Override
78+
public Locale getLocale() {
79+
return Locale.ROOT;
80+
}
81+
82+
@Override
83+
public ZoneId getZone() {
84+
return ZoneOffset.UTC;
85+
}
86+
87+
@Override
88+
public DateFormatter withZone(ZoneId zoneId) {
89+
if (zoneId.equals(ZoneOffset.UTC) == false) {
90+
throw new IllegalArgumentException(pattern() + " date formatter can only be in zone offset UTC");
91+
}
92+
return this;
93+
}
94+
95+
@Override
96+
public DateFormatter withLocale(Locale locale) {
97+
if (Locale.ROOT.equals(locale) == false) {
98+
throw new IllegalArgumentException(pattern() + " date formatter can only be in locale ROOT");
99+
}
100+
return this;
79101
}
80102

81103
@Override

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.Arrays;
2929
import java.util.Locale;
3030
import java.util.Map;
31+
import java.util.Objects;
3132

3233
class JavaDateFormatter implements DateFormatter {
3334

@@ -36,10 +37,17 @@ class JavaDateFormatter implements DateFormatter {
3637
private final DateTimeFormatter[] parsers;
3738

3839
JavaDateFormatter(String format, DateTimeFormatter printer, DateTimeFormatter... parsers) {
40+
if (printer == null) {
41+
throw new IllegalArgumentException("printer may not be null");
42+
}
3943
long distinctZones = Arrays.stream(parsers).map(DateTimeFormatter::getZone).distinct().count();
4044
if (distinctZones > 1) {
4145
throw new IllegalArgumentException("formatters must have the same time zone");
4246
}
47+
long distinctLocales = Arrays.stream(parsers).map(DateTimeFormatter::getLocale).distinct().count();
48+
if (distinctLocales > 1) {
49+
throw new IllegalArgumentException("formatters must have the same locale");
50+
}
4351
if (parsers.length == 0) {
4452
this.parsers = new DateTimeFormatter[]{printer};
4553
} else {
@@ -83,6 +91,21 @@ public DateFormatter withZone(ZoneId zoneId) {
8391
return new JavaDateFormatter(format, printer.withZone(zoneId), parsersWithZone);
8492
}
8593

94+
@Override
95+
public DateFormatter withLocale(Locale locale) {
96+
// shortcurt to not create new objects unnecessarily
97+
if (locale.equals(parsers[0].getLocale())) {
98+
return this;
99+
}
100+
101+
final DateTimeFormatter[] parsersWithZone = new DateTimeFormatter[parsers.length];
102+
for (int i = 0; i < parsers.length; i++) {
103+
parsersWithZone[i] = parsers[i].withLocale(locale);
104+
}
105+
106+
return new JavaDateFormatter(format, printer.withLocale(locale), parsersWithZone);
107+
}
108+
86109
@Override
87110
public String format(TemporalAccessor accessor) {
88111
return printer.format(accessor);
@@ -109,4 +132,36 @@ public DateFormatter parseDefaulting(Map<TemporalField, Long> fields) {
109132
return new JavaDateFormatter(format, parseDefaultingBuilder.toFormatter(Locale.ROOT), parsersWithDefaulting);
110133
}
111134
}
135+
136+
@Override
137+
public Locale getLocale() {
138+
return this.printer.getLocale();
139+
}
140+
141+
@Override
142+
public ZoneId getZone() {
143+
return this.printer.getZone();
144+
}
145+
146+
@Override
147+
public int hashCode() {
148+
return Objects.hash(getLocale(), printer.getZone(), format);
149+
}
150+
151+
@Override
152+
public boolean equals(Object obj) {
153+
if (obj.getClass().equals(this.getClass()) == false) {
154+
return false;
155+
}
156+
JavaDateFormatter other = (JavaDateFormatter) obj;
157+
158+
return Objects.equals(format, other.format) &&
159+
Objects.equals(getLocale(), other.getLocale()) &&
160+
Objects.equals(this.printer.getZone(), other.printer.getZone());
161+
}
162+
163+
@Override
164+
public String toString() {
165+
return String.format(Locale.ROOT, "format[%s] locale[%s]", format, getLocale());
166+
}
112167
}

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

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,23 @@
2323

2424
import java.time.Instant;
2525
import java.time.ZoneId;
26-
import java.time.ZonedDateTime;
2726
import java.time.format.DateTimeParseException;
2827
import java.time.temporal.TemporalAccessor;
28+
import java.util.Locale;
2929

3030
import static org.hamcrest.Matchers.containsString;
31+
import static org.hamcrest.Matchers.equalTo;
3132
import static org.hamcrest.Matchers.is;
33+
import static org.hamcrest.Matchers.not;
34+
import static org.hamcrest.Matchers.nullValue;
35+
import static org.hamcrest.Matchers.sameInstance;
3236

3337
public class DateFormattersTests extends ESTestCase {
3438

3539
public void testEpochMilliParser() {
3640
DateFormatter formatter = DateFormatters.forPattern("epoch_millis");
37-
3841
DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("invalid"));
3942
assertThat(e.getMessage(), containsString("invalid number"));
40-
41-
// different zone, should still yield the same output, as epoch is time zone independent
42-
ZoneId zoneId = randomZone();
43-
DateFormatter zonedFormatter = formatter.withZone(zoneId);
44-
45-
// test with negative and non negative values
46-
assertThatSameDateTime(formatter, zonedFormatter, randomNonNegativeLong() * -1);
47-
assertThatSameDateTime(formatter, zonedFormatter, randomNonNegativeLong());
48-
assertThatSameDateTime(formatter, zonedFormatter, 0);
49-
assertThatSameDateTime(formatter, zonedFormatter, -1);
50-
assertThatSameDateTime(formatter, zonedFormatter, 1);
51-
52-
// format() output should be equal as well
53-
assertSameFormat(formatter, randomNonNegativeLong() * -1);
54-
assertSameFormat(formatter, randomNonNegativeLong());
55-
assertSameFormat(formatter, 0);
56-
assertSameFormat(formatter, -1);
57-
assertSameFormat(formatter, 1);
5843
}
5944

6045
// this is not in the duelling tests, because the epoch second parser in joda time drops the milliseconds after the comma
@@ -83,14 +68,6 @@ public void testEpochSecondParser() {
8368
assertThat(e.getMessage(), is("invalid number [abc]"));
8469
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.abc"));
8570
assertThat(e.getMessage(), is("invalid number [1234.abc]"));
86-
87-
// different zone, should still yield the same output, as epoch is time zone independent
88-
ZoneId zoneId = randomZone();
89-
DateFormatter zonedFormatter = formatter.withZone(zoneId);
90-
91-
assertThatSameDateTime(formatter, zonedFormatter, randomLongBetween(-100_000_000, 100_000_000));
92-
assertSameFormat(formatter, randomLongBetween(-100_000_000, 100_000_000));
93-
assertThat(formatter.format(Instant.ofEpochSecond(1234, 567_000_000)), is("1234.567"));
9471
}
9572

9673
public void testEpochMilliParsersWithDifferentFormatters() {
@@ -100,16 +77,54 @@ public void testEpochMilliParsersWithDifferentFormatters() {
10077
assertThat(formatter.pattern(), is("strict_date_optional_time||epoch_millis"));
10178
}
10279

103-
private void assertThatSameDateTime(DateFormatter formatter, DateFormatter zonedFormatter, long millis) {
104-
String millisAsString = String.valueOf(millis);
105-
ZonedDateTime formatterZonedDateTime = DateFormatters.toZonedDateTime(formatter.parse(millisAsString));
106-
ZonedDateTime zonedFormatterZonedDateTime = DateFormatters.toZonedDateTime(zonedFormatter.parse(millisAsString));
107-
assertThat(formatterZonedDateTime.toInstant().toEpochMilli(), is(zonedFormatterZonedDateTime.toInstant().toEpochMilli()));
80+
public void testLocales() {
81+
assertThat(DateFormatters.forPattern("strict_date_optional_time").getLocale(), is(Locale.ROOT));
82+
Locale locale = randomLocale(random());
83+
assertThat(DateFormatters.forPattern("strict_date_optional_time").withLocale(locale).getLocale(), is(locale));
84+
IllegalArgumentException e =
85+
expectThrows(IllegalArgumentException.class, () -> DateFormatters.forPattern("epoch_millis").withLocale(locale));
86+
assertThat(e.getMessage(), is("epoch_millis date formatter can only be in locale ROOT"));
87+
e = expectThrows(IllegalArgumentException.class, () -> DateFormatters.forPattern("epoch_second").withLocale(locale));
88+
assertThat(e.getMessage(), is("epoch_second date formatter can only be in locale ROOT"));
89+
}
90+
91+
public void testTimeZones() {
92+
// zone is null by default due to different behaviours between java8 and above
93+
assertThat(DateFormatters.forPattern("strict_date_optional_time").getZone(), is(nullValue()));
94+
ZoneId zoneId = randomZone();
95+
assertThat(DateFormatters.forPattern("strict_date_optional_time").withZone(zoneId).getZone(), is(zoneId));
96+
IllegalArgumentException e =
97+
expectThrows(IllegalArgumentException.class, () -> DateFormatters.forPattern("epoch_millis").withZone(zoneId));
98+
assertThat(e.getMessage(), is("epoch_millis date formatter can only be in zone offset UTC"));
99+
e = expectThrows(IllegalArgumentException.class, () -> DateFormatters.forPattern("epoch_second").withZone(zoneId));
100+
assertThat(e.getMessage(), is("epoch_second date formatter can only be in zone offset UTC"));
108101
}
109102

110-
private void assertSameFormat(DateFormatter formatter, long millis) {
111-
String millisAsString = String.valueOf(millis);
112-
TemporalAccessor accessor = formatter.parse(millisAsString);
113-
assertThat(millisAsString, is(formatter.format(accessor)));
103+
public void testEqualsAndHashcode() {
104+
assertThat(DateFormatters.forPattern("strict_date_optional_time"),
105+
sameInstance(DateFormatters.forPattern("strict_date_optional_time")));
106+
assertThat(DateFormatters.forPattern("YYYY"), equalTo(DateFormatters.forPattern("YYYY")));
107+
assertThat(DateFormatters.forPattern("YYYY").hashCode(),
108+
is(DateFormatters.forPattern("YYYY").hashCode()));
109+
110+
// different timezone, thus not equals
111+
assertThat(DateFormatters.forPattern("YYYY").withZone(ZoneId.of("CET")), not(equalTo(DateFormatters.forPattern("YYYY"))));
112+
113+
// different locale, thus not equals
114+
assertThat(DateFormatters.forPattern("YYYY").withLocale(randomLocale(random())),
115+
not(equalTo(DateFormatters.forPattern("YYYY"))));
116+
117+
// different pattern, thus not equals
118+
assertThat(DateFormatters.forPattern("YYYY"), not(equalTo(DateFormatters.forPattern("YY"))));
119+
120+
DateFormatter epochSecondFormatter = DateFormatters.forPattern("epoch_second");
121+
assertThat(epochSecondFormatter, sameInstance(DateFormatters.forPattern("epoch_second")));
122+
assertThat(epochSecondFormatter, equalTo(DateFormatters.forPattern("epoch_second")));
123+
assertThat(epochSecondFormatter.hashCode(), is(DateFormatters.forPattern("epoch_second").hashCode()));
124+
125+
DateFormatter epochMillisFormatter = DateFormatters.forPattern("epoch_millis");
126+
assertThat(epochMillisFormatter.hashCode(), is(DateFormatters.forPattern("epoch_millis").hashCode()));
127+
assertThat(epochMillisFormatter, sameInstance(DateFormatters.forPattern("epoch_millis")));
128+
assertThat(epochMillisFormatter, equalTo(DateFormatters.forPattern("epoch_millis")));
114129
}
115130
}

0 commit comments

Comments
 (0)