Support parsing long millisecond timestamps in `InstantFormatter`

This commit adds support of parsing a simple long from a String and
turning it to an `Instant` by considering it represents a timestamp in
milliseconds (see `Instant.ofEpochMilli`). Failing to parse a long from
the String, the previous algorithm is used: first check for an RFC-1123
representation then an ISO_INSTANT representation.

See gh-30312
Closes gh-30546
This commit is contained in:
Remus Richard Dumitrache 2023-05-26 12:08:26 +02:00 committed by GitHub
parent 7150c23e93
commit 4d8f6c1b41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 49 additions and 6 deletions

View File

@ -41,13 +41,18 @@ public class InstantFormatter implements Formatter<Instant> {
@Override
public Instant parse(String text, Locale locale) throws ParseException {
if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) {
// assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT"
return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text));
try {
return Instant.ofEpochMilli(Long.parseLong(text));
}
else {
// assuming UTC instant a la "2007-12-03T10:15:30.00Z"
return Instant.parse(text);
catch (NumberFormatException ex) {
if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) {
// assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT"
return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text));
}
else {
// assuming UTC instant a la "2007-12-03T10:15:30.00Z"
return Instant.parse(text);
}
}
}

View File

@ -621,6 +621,19 @@ class DateTimeFormattingTests {
.hasMessageStartingWith("Text '210302'")
.hasNoCause();
}
@Test
void testBindInstantAsLongEpochMillis() {
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("instant", 1234L);
binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isZero();
assertThat(binder.getBindingResult().getRawFieldValue("instant"))
.isInstanceOf(Instant.class)
.isEqualTo(Instant.ofEpochMilli(1234L));
assertThat(binder.getBindingResult().getFieldValue("instant"))
.hasToString("1970-01-01T00:00:01.234Z");
}
}

View File

@ -19,6 +19,7 @@ package org.springframework.format.datetime.standard;
import java.text.ParseException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Random;
import java.util.stream.Stream;
@ -79,6 +80,16 @@ class InstantFormatterTests {
assertThat(actual).isEqualTo(expected);
}
@ParameterizedTest
@ArgumentsSource(RandomEpochMillisProvider.class)
void should_parse_into_an_Instant_from_epoch_mili(Instant input) throws ParseException {
Instant expected = input;
Instant actual = instantFormatter.parse(Long.toString(input.toEpochMilli()), null);
assertThat(actual).isEqualTo(expected);
}
private static class RandomInstantProvider implements ArgumentsProvider {
private static final long DATA_SET_SIZE = 10;
@ -121,5 +132,19 @@ class InstantFormatterTests {
.map(DateTimeFormatter.RFC_1123_DATE_TIME.withZone(systemDefault())::format);
}
}
private static final class RandomEpochMillisProvider implements ArgumentsProvider {
private static final long DATA_SET_SIZE = 10;
private static final Random random = new Random();
@Override
public Stream<Arguments> provideArguments(ExtensionContext context) {
return random.longs(DATA_SET_SIZE, Long.MIN_VALUE, Long.MAX_VALUE)
.mapToObj(Instant::ofEpochMilli)
.map(instant -> instant.truncatedTo(ChronoUnit.MILLIS))
.map(Arguments::of);
}
}
}