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.
This commit is contained in:
Alexander Reelsen 2018-10-02 14:13:30 +02:00 committed by GitHub
parent 1d049cadbe
commit b1b0f3276b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 198 additions and 45 deletions

View File

@ -24,6 +24,7 @@ import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
@ -46,6 +47,14 @@ public interface DateFormatter {
*/
DateFormatter withZone(ZoneId zoneId);
/**
* Create a copy of this formatter that is configured to parse dates in the specified locale
*
* @param locale The local to use for the new formatter
* @return A copy of the date formatter this has been called on
*/
DateFormatter withLocale(Locale locale);
/**
* Print the supplied java time accessor in a string based representation according to this formatter
*
@ -62,6 +71,20 @@ public interface DateFormatter {
*/
String pattern();
/**
* Returns the configured locale of the date formatter
*
* @return The locale of this formatter
*/
Locale getLocale();
/**
* Returns the configured time zone of the date formatter
*
* @return The time zone of this formatter
*/
ZoneId getZone();
/**
* Configure a formatter using default fields for a TemporalAccessor that should be used in case
* the supplied date is not having all of those fields
@ -115,6 +138,11 @@ public interface DateFormatter {
return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.withZone(zoneId)).toArray(DateFormatter[]::new));
}
@Override
public DateFormatter withLocale(Locale locale) {
return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.withLocale(locale)).toArray(DateFormatter[]::new));
}
@Override
public String format(TemporalAccessor accessor) {
return formatters[0].format(accessor);
@ -125,6 +153,16 @@ public interface DateFormatter {
return format;
}
@Override
public Locale getLocale() {
return formatters[0].getLocale();
}
@Override
public ZoneId getZone() {
return formatters[0].getZone();
}
@Override
public DateFormatter parseDefaulting(Map<TemporalField, Long> fields) {
return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.parseDefaulting(fields)).toArray(DateFormatter[]::new));

View File

@ -1269,7 +1269,7 @@ public class DateFormatters {
return forPattern(input, Locale.ROOT);
}
public static DateFormatter forPattern(String input, Locale locale) {
private static DateFormatter forPattern(String input, Locale locale) {
if (Strings.hasLength(input)) {
input = input.trim();
}

View File

@ -25,6 +25,7 @@ import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.util.Locale;
import java.util.Map;
/**
@ -40,7 +41,8 @@ class EpochMillisDateFormatter implements DateFormatter {
public static DateFormatter INSTANCE = new EpochMillisDateFormatter();
private EpochMillisDateFormatter() {}
private EpochMillisDateFormatter() {
}
@Override
public TemporalAccessor parse(String input) {
@ -53,6 +55,17 @@ class EpochMillisDateFormatter implements DateFormatter {
@Override
public DateFormatter withZone(ZoneId zoneId) {
if (ZoneOffset.UTC.equals(zoneId) == false) {
throw new IllegalArgumentException(pattern() + " date formatter can only be in zone offset UTC");
}
return INSTANCE;
}
@Override
public DateFormatter withLocale(Locale locale) {
if (Locale.ROOT.equals(locale) == false) {
throw new IllegalArgumentException(pattern() + " date formatter can only be in locale ROOT");
}
return this;
}
@ -70,4 +83,14 @@ class EpochMillisDateFormatter implements DateFormatter {
public DateFormatter parseDefaulting(Map<TemporalField, Long> fields) {
return this;
}
@Override
public Locale getLocale() {
return Locale.ROOT;
}
@Override
public ZoneId getZone() {
return ZoneOffset.UTC;
}
}

View File

@ -26,6 +26,7 @@ import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
@ -59,11 +60,6 @@ public class EpochSecondsDateFormatter implements DateFormatter {
}
}
@Override
public DateFormatter withZone(ZoneId zoneId) {
return this;
}
@Override
public String format(TemporalAccessor accessor) {
Instant instant = Instant.from(accessor);
@ -75,7 +71,33 @@ public class EpochSecondsDateFormatter implements DateFormatter {
@Override
public String pattern() {
return "epoch_seconds";
return "epoch_second";
}
@Override
public Locale getLocale() {
return Locale.ROOT;
}
@Override
public ZoneId getZone() {
return ZoneOffset.UTC;
}
@Override
public DateFormatter withZone(ZoneId zoneId) {
if (zoneId.equals(ZoneOffset.UTC) == false) {
throw new IllegalArgumentException(pattern() + " date formatter can only be in zone offset UTC");
}
return this;
}
@Override
public DateFormatter withLocale(Locale locale) {
if (Locale.ROOT.equals(locale) == false) {
throw new IllegalArgumentException(pattern() + " date formatter can only be in locale ROOT");
}
return this;
}
@Override

View File

@ -28,6 +28,7 @@ import java.time.temporal.TemporalField;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
class JavaDateFormatter implements DateFormatter {
@ -36,10 +37,17 @@ class JavaDateFormatter implements DateFormatter {
private final DateTimeFormatter[] parsers;
JavaDateFormatter(String format, DateTimeFormatter printer, DateTimeFormatter... parsers) {
if (printer == null) {
throw new IllegalArgumentException("printer may not be null");
}
long distinctZones = Arrays.stream(parsers).map(DateTimeFormatter::getZone).distinct().count();
if (distinctZones > 1) {
throw new IllegalArgumentException("formatters must have the same time zone");
}
long distinctLocales = Arrays.stream(parsers).map(DateTimeFormatter::getLocale).distinct().count();
if (distinctLocales > 1) {
throw new IllegalArgumentException("formatters must have the same locale");
}
if (parsers.length == 0) {
this.parsers = new DateTimeFormatter[]{printer};
} else {
@ -83,6 +91,21 @@ class JavaDateFormatter implements DateFormatter {
return new JavaDateFormatter(format, printer.withZone(zoneId), parsersWithZone);
}
@Override
public DateFormatter withLocale(Locale locale) {
// shortcurt to not create new objects unnecessarily
if (locale.equals(parsers[0].getLocale())) {
return this;
}
final DateTimeFormatter[] parsersWithZone = new DateTimeFormatter[parsers.length];
for (int i = 0; i < parsers.length; i++) {
parsersWithZone[i] = parsers[i].withLocale(locale);
}
return new JavaDateFormatter(format, printer.withLocale(locale), parsersWithZone);
}
@Override
public String format(TemporalAccessor accessor) {
return printer.format(accessor);
@ -109,4 +132,36 @@ class JavaDateFormatter implements DateFormatter {
return new JavaDateFormatter(format, parseDefaultingBuilder.toFormatter(Locale.ROOT), parsersWithDefaulting);
}
}
@Override
public Locale getLocale() {
return this.printer.getLocale();
}
@Override
public ZoneId getZone() {
return this.printer.getZone();
}
@Override
public int hashCode() {
return Objects.hash(getLocale(), printer.getZone(), format);
}
@Override
public boolean equals(Object obj) {
if (obj.getClass().equals(this.getClass()) == false) {
return false;
}
JavaDateFormatter other = (JavaDateFormatter) obj;
return Objects.equals(format, other.format) &&
Objects.equals(getLocale(), other.getLocale()) &&
Objects.equals(this.printer.getZone(), other.printer.getZone());
}
@Override
public String toString() {
return String.format(Locale.ROOT, "format[%s] locale[%s]", format, getLocale());
}
}

View File

@ -23,38 +23,23 @@ import org.elasticsearch.test.ESTestCase;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.util.Locale;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
public class DateFormattersTests extends ESTestCase {
public void testEpochMilliParser() {
DateFormatter formatter = DateFormatters.forPattern("epoch_millis");
DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("invalid"));
assertThat(e.getMessage(), containsString("invalid number"));
// different zone, should still yield the same output, as epoch is time zone independent
ZoneId zoneId = randomZone();
DateFormatter zonedFormatter = formatter.withZone(zoneId);
// test with negative and non negative values
assertThatSameDateTime(formatter, zonedFormatter, randomNonNegativeLong() * -1);
assertThatSameDateTime(formatter, zonedFormatter, randomNonNegativeLong());
assertThatSameDateTime(formatter, zonedFormatter, 0);
assertThatSameDateTime(formatter, zonedFormatter, -1);
assertThatSameDateTime(formatter, zonedFormatter, 1);
// format() output should be equal as well
assertSameFormat(formatter, randomNonNegativeLong() * -1);
assertSameFormat(formatter, randomNonNegativeLong());
assertSameFormat(formatter, 0);
assertSameFormat(formatter, -1);
assertSameFormat(formatter, 1);
}
// 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 class DateFormattersTests extends ESTestCase {
assertThat(e.getMessage(), is("invalid number [abc]"));
e = expectThrows(DateTimeParseException.class, () -> formatter.parse("1234.abc"));
assertThat(e.getMessage(), is("invalid number [1234.abc]"));
// different zone, should still yield the same output, as epoch is time zone independent
ZoneId zoneId = randomZone();
DateFormatter zonedFormatter = formatter.withZone(zoneId);
assertThatSameDateTime(formatter, zonedFormatter, randomLongBetween(-100_000_000, 100_000_000));
assertSameFormat(formatter, randomLongBetween(-100_000_000, 100_000_000));
assertThat(formatter.format(Instant.ofEpochSecond(1234, 567_000_000)), is("1234.567"));
}
public void testEpochMilliParsersWithDifferentFormatters() {
@ -100,16 +77,54 @@ public class DateFormattersTests extends ESTestCase {
assertThat(formatter.pattern(), is("strict_date_optional_time||epoch_millis"));
}
private void assertThatSameDateTime(DateFormatter formatter, DateFormatter zonedFormatter, long millis) {
String millisAsString = String.valueOf(millis);
ZonedDateTime formatterZonedDateTime = DateFormatters.toZonedDateTime(formatter.parse(millisAsString));
ZonedDateTime zonedFormatterZonedDateTime = DateFormatters.toZonedDateTime(zonedFormatter.parse(millisAsString));
assertThat(formatterZonedDateTime.toInstant().toEpochMilli(), is(zonedFormatterZonedDateTime.toInstant().toEpochMilli()));
public void testLocales() {
assertThat(DateFormatters.forPattern("strict_date_optional_time").getLocale(), is(Locale.ROOT));
Locale locale = randomLocale(random());
assertThat(DateFormatters.forPattern("strict_date_optional_time").withLocale(locale).getLocale(), is(locale));
IllegalArgumentException e =
expectThrows(IllegalArgumentException.class, () -> DateFormatters.forPattern("epoch_millis").withLocale(locale));
assertThat(e.getMessage(), is("epoch_millis date formatter can only be in locale ROOT"));
e = expectThrows(IllegalArgumentException.class, () -> DateFormatters.forPattern("epoch_second").withLocale(locale));
assertThat(e.getMessage(), is("epoch_second date formatter can only be in locale ROOT"));
}
private void assertSameFormat(DateFormatter formatter, long millis) {
String millisAsString = String.valueOf(millis);
TemporalAccessor accessor = formatter.parse(millisAsString);
assertThat(millisAsString, is(formatter.format(accessor)));
public void testTimeZones() {
// zone is null by default due to different behaviours between java8 and above
assertThat(DateFormatters.forPattern("strict_date_optional_time").getZone(), is(nullValue()));
ZoneId zoneId = randomZone();
assertThat(DateFormatters.forPattern("strict_date_optional_time").withZone(zoneId).getZone(), is(zoneId));
IllegalArgumentException e =
expectThrows(IllegalArgumentException.class, () -> DateFormatters.forPattern("epoch_millis").withZone(zoneId));
assertThat(e.getMessage(), is("epoch_millis date formatter can only be in zone offset UTC"));
e = expectThrows(IllegalArgumentException.class, () -> DateFormatters.forPattern("epoch_second").withZone(zoneId));
assertThat(e.getMessage(), is("epoch_second date formatter can only be in zone offset UTC"));
}
public void testEqualsAndHashcode() {
assertThat(DateFormatters.forPattern("strict_date_optional_time"),
sameInstance(DateFormatters.forPattern("strict_date_optional_time")));
assertThat(DateFormatters.forPattern("YYYY"), equalTo(DateFormatters.forPattern("YYYY")));
assertThat(DateFormatters.forPattern("YYYY").hashCode(),
is(DateFormatters.forPattern("YYYY").hashCode()));
// different timezone, thus not equals
assertThat(DateFormatters.forPattern("YYYY").withZone(ZoneId.of("CET")), not(equalTo(DateFormatters.forPattern("YYYY"))));
// different locale, thus not equals
assertThat(DateFormatters.forPattern("YYYY").withLocale(randomLocale(random())),
not(equalTo(DateFormatters.forPattern("YYYY"))));
// different pattern, thus not equals
assertThat(DateFormatters.forPattern("YYYY"), not(equalTo(DateFormatters.forPattern("YY"))));
DateFormatter epochSecondFormatter = DateFormatters.forPattern("epoch_second");
assertThat(epochSecondFormatter, sameInstance(DateFormatters.forPattern("epoch_second")));
assertThat(epochSecondFormatter, equalTo(DateFormatters.forPattern("epoch_second")));
assertThat(epochSecondFormatter.hashCode(), is(DateFormatters.forPattern("epoch_second").hashCode()));
DateFormatter epochMillisFormatter = DateFormatters.forPattern("epoch_millis");
assertThat(epochMillisFormatter.hashCode(), is(DateFormatters.forPattern("epoch_millis").hashCode()));
assertThat(epochMillisFormatter, sameInstance(DateFormatters.forPattern("epoch_millis")));
assertThat(epochMillisFormatter, equalTo(DateFormatters.forPattern("epoch_millis")));
}
}