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:
parent
1d049cadbe
commit
b1b0f3276b
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue