Skip to content

Commit c9a6f42

Browse files
monosoulsbrannen
authored andcommitted
Inverse condition to fix ISO-formatted Instant parsing
Prior to this commit, InstantFormatter was able to properly serialize an Instant that is far in the future (or in the past), but it could not properly deserialize it, because in such scenarios an ISO-formatted Instant starts with a +/- sign. This commit fixes this issue, while maintaining the previous contract, and also introduces tests for InstantFormatter. Closes gh-23895
1 parent 6ed6c08 commit c9a6f42

File tree

2 files changed

+124
-5
lines changed

2 files changed

+124
-5
lines changed

spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ public class InstantFormatter implements Formatter<Instant> {
4040

4141
@Override
4242
public Instant parse(String text, Locale locale) throws ParseException {
43-
if (text.length() > 0 && Character.isDigit(text.charAt(0))) {
44-
// assuming UTC instant a la "2007-12-03T10:15:30.00Z"
45-
return Instant.parse(text);
46-
}
47-
else {
43+
if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) {
4844
// assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT"
4945
return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text));
5046
}
47+
else {
48+
// assuming UTC instant a la "2007-12-03T10:15:30.00Z"
49+
return Instant.parse(text);
50+
}
5151
}
5252

5353
@Override
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.format.datetime.standard;
18+
19+
import org.junit.jupiter.api.extension.ExtensionContext;
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.Arguments;
22+
import org.junit.jupiter.params.provider.ArgumentsProvider;
23+
import org.junit.jupiter.params.provider.ArgumentsSource;
24+
25+
import java.text.ParseException;
26+
import java.time.Instant;
27+
import java.time.format.DateTimeFormatter;
28+
import java.util.Random;
29+
import java.util.stream.Stream;
30+
31+
import static java.time.Instant.MAX;
32+
import static java.time.Instant.MIN;
33+
import static java.time.ZoneId.systemDefault;
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
36+
/**
37+
* @author Andrei Nevedomskii
38+
*/
39+
@SuppressWarnings("ConstantConditions")
40+
class InstantFormatterTests {
41+
42+
private final InstantFormatter instantFormatter = new InstantFormatter();
43+
44+
@ParameterizedTest
45+
@ArgumentsSource(ISOSerializedInstantProvider.class)
46+
void should_parse_an_ISO_formatted_string_representation_of_an_instant(String input) throws ParseException {
47+
Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from);
48+
49+
Instant actual = instantFormatter.parse(input, null);
50+
51+
assertThat(actual).isEqualTo(expected);
52+
}
53+
54+
@ParameterizedTest
55+
@ArgumentsSource(RFC1123SerializedInstantProvider.class)
56+
void should_parse_an_RFC1123_formatted_string_representation_of_an_instant(String input) throws ParseException {
57+
Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from);
58+
59+
Instant actual = instantFormatter.parse(input, null);
60+
61+
assertThat(actual).isEqualTo(expected);
62+
}
63+
64+
@ParameterizedTest
65+
@ArgumentsSource(RandomInstantProvider.class)
66+
void should_serialize_an_instant_using_ISO_format_and_ignoring_locale(Instant input) {
67+
String expected = DateTimeFormatter.ISO_INSTANT.format(input);
68+
69+
String actual = instantFormatter.print(input, null);
70+
71+
assertThat(actual).isEqualTo(expected);
72+
}
73+
74+
private static class ISOSerializedInstantProvider extends RandomInstantProvider {
75+
76+
@Override
77+
Stream<?> provideArguments() {
78+
return randomInstantStream(MIN, MAX).map(DateTimeFormatter.ISO_INSTANT::format);
79+
}
80+
}
81+
82+
private static class RFC1123SerializedInstantProvider extends RandomInstantProvider {
83+
84+
// RFC-1123 supports only 4-digit years
85+
private final Instant min = Instant.parse("0000-01-01T00:00:00.00Z");
86+
87+
private final Instant max = Instant.parse("9999-12-31T23:59:59.99Z");
88+
89+
@Override
90+
Stream<?> provideArguments() {
91+
return randomInstantStream(min, max)
92+
.map(DateTimeFormatter.RFC_1123_DATE_TIME.withZone(systemDefault())::format);
93+
}
94+
}
95+
96+
private static class RandomInstantProvider implements ArgumentsProvider {
97+
98+
private static final long DATA_SET_SIZE = 10;
99+
100+
static final Random RANDOM = new Random();
101+
102+
Stream<?> provideArguments() {
103+
return randomInstantStream(MIN, MAX);
104+
}
105+
106+
@Override
107+
public final Stream<? extends Arguments> provideArguments(ExtensionContext context) {
108+
return provideArguments().map(Arguments::of).limit(DATA_SET_SIZE);
109+
}
110+
111+
Stream<Instant> randomInstantStream(Instant min, Instant max) {
112+
return Stream.concat(
113+
Stream.of(Instant.now()), // make sure that the data set includes current instant
114+
RANDOM.longs(min.getEpochSecond(), max.getEpochSecond())
115+
.mapToObj(Instant::ofEpochSecond)
116+
);
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)