Skip to content

Commit fee17e1

Browse files
committed
Default fallback parsing for UTC without milliseconds
Closes gh-32856
1 parent 65e1337 commit fee17e1

File tree

2 files changed

+54
-12
lines changed

2 files changed

+54
-12
lines changed

Diff for: spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java

+28-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,8 +22,10 @@
2222
import java.util.Collections;
2323
import java.util.Date;
2424
import java.util.EnumMap;
25+
import java.util.LinkedHashSet;
2526
import java.util.Locale;
2627
import java.util.Map;
28+
import java.util.Set;
2729
import java.util.TimeZone;
2830

2931
import org.springframework.format.Formatter;
@@ -35,9 +37,14 @@
3537

3638
/**
3739
* A formatter for {@link java.util.Date} types.
40+
*
3841
* <p>Supports the configuration of an explicit date time pattern, timezone,
3942
* locale, and fallback date time patterns for lenient parsing.
4043
*
44+
* <p>Common ISO patterns for UTC instants are applied at millisecond precision.
45+
* Note that {@link org.springframework.format.datetime.standard.InstantFormatter}
46+
* is recommended for flexible UTC parsing into a {@link java.time.Instant} instead.
47+
*
4148
* @author Keith Donald
4249
* @author Juergen Hoeller
4350
* @author Phillip Webb
@@ -49,15 +56,23 @@ public class DateFormatter implements Formatter<Date> {
4956

5057
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
5158

52-
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
5359
private static final Map<ISO, String> ISO_PATTERNS;
5460

61+
private static final Map<ISO, String> ISO_FALLBACK_PATTERNS;
62+
5563
static {
64+
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
5665
Map<ISO, String> formats = new EnumMap<>(ISO.class);
5766
formats.put(ISO.DATE, "yyyy-MM-dd");
5867
formats.put(ISO.TIME, "HH:mm:ss.SSSXXX");
5968
formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
6069
ISO_PATTERNS = Collections.unmodifiableMap(formats);
70+
71+
// Fallback format for the time part without milliseconds.
72+
Map<ISO, String> fallbackFormats = new EnumMap<>(ISO.class);
73+
fallbackFormats.put(ISO.TIME, "HH:mm:ssXXX");
74+
fallbackFormats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ssXXX");
75+
ISO_FALLBACK_PATTERNS = Collections.unmodifiableMap(fallbackFormats);
6176
}
6277

6378

@@ -202,8 +217,16 @@ public Date parse(String text, Locale locale) throws ParseException {
202217
return getDateFormat(locale).parse(text);
203218
}
204219
catch (ParseException ex) {
220+
Set<String> fallbackPatterns = new LinkedHashSet<>();
221+
String isoPattern = ISO_FALLBACK_PATTERNS.get(this.iso);
222+
if (isoPattern != null) {
223+
fallbackPatterns.add(isoPattern);
224+
}
205225
if (!ObjectUtils.isEmpty(this.fallbackPatterns)) {
206-
for (String pattern : this.fallbackPatterns) {
226+
Collections.addAll(fallbackPatterns, this.fallbackPatterns);
227+
}
228+
if (!fallbackPatterns.isEmpty()) {
229+
for (String pattern : fallbackPatterns) {
207230
try {
208231
DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale));
209232
// Align timezone for parsing format with printing format if ISO is set.
@@ -221,8 +244,8 @@ public Date parse(String text, Locale locale) throws ParseException {
221244
}
222245
if (this.source != null) {
223246
ParseException parseException = new ParseException(
224-
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
225-
ex.getErrorOffset());
247+
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
248+
ex.getErrorOffset());
226249
parseException.initCause(ex);
227250
throw parseException;
228251
}

Diff for: spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java

+26-7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
*
3636
* @author Keith Donald
3737
* @author Phillip Webb
38+
* @author Juergen Hoeller
3839
*/
3940
class DateFormatterTests {
4041

@@ -45,6 +46,7 @@ class DateFormatterTests {
4546
void shouldPrintAndParseDefault() throws Exception {
4647
DateFormatter formatter = new DateFormatter();
4748
formatter.setTimeZone(UTC);
49+
4850
Date date = getDate(2009, Calendar.JUNE, 1);
4951
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
5052
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
@@ -54,6 +56,7 @@ void shouldPrintAndParseDefault() throws Exception {
5456
void shouldPrintAndParseFromPattern() throws ParseException {
5557
DateFormatter formatter = new DateFormatter("yyyy-MM-dd");
5658
formatter.setTimeZone(UTC);
59+
5760
Date date = getDate(2009, Calendar.JUNE, 1);
5861
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
5962
assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date);
@@ -64,6 +67,7 @@ void shouldPrintAndParseShort() throws Exception {
6467
DateFormatter formatter = new DateFormatter();
6568
formatter.setTimeZone(UTC);
6669
formatter.setStyle(DateFormat.SHORT);
70+
6771
Date date = getDate(2009, Calendar.JUNE, 1);
6872
assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09");
6973
assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date);
@@ -74,6 +78,7 @@ void shouldPrintAndParseMedium() throws Exception {
7478
DateFormatter formatter = new DateFormatter();
7579
formatter.setTimeZone(UTC);
7680
formatter.setStyle(DateFormat.MEDIUM);
81+
7782
Date date = getDate(2009, Calendar.JUNE, 1);
7883
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
7984
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
@@ -84,6 +89,7 @@ void shouldPrintAndParseLong() throws Exception {
8489
DateFormatter formatter = new DateFormatter();
8590
formatter.setTimeZone(UTC);
8691
formatter.setStyle(DateFormat.LONG);
92+
8793
Date date = getDate(2009, Calendar.JUNE, 1);
8894
assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009");
8995
assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date);
@@ -94,50 +100,63 @@ void shouldPrintAndParseFull() throws Exception {
94100
DateFormatter formatter = new DateFormatter();
95101
formatter.setTimeZone(UTC);
96102
formatter.setStyle(DateFormat.FULL);
103+
97104
Date date = getDate(2009, Calendar.JUNE, 1);
98105
assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009");
99106
assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date);
100107
}
101108

102109
@Test
103-
void shouldPrintAndParseISODate() throws Exception {
110+
void shouldPrintAndParseIsoDate() throws Exception {
104111
DateFormatter formatter = new DateFormatter();
105112
formatter.setTimeZone(UTC);
106113
formatter.setIso(ISO.DATE);
114+
107115
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
108116
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
109117
assertThat(formatter.parse("2009-6-01", Locale.US))
110118
.isEqualTo(getDate(2009, Calendar.JUNE, 1));
111119
}
112120

113121
@Test
114-
void shouldPrintAndParseISOTime() throws Exception {
122+
void shouldPrintAndParseIsoTime() throws Exception {
115123
DateFormatter formatter = new DateFormatter();
116124
formatter.setTimeZone(UTC);
117125
formatter.setIso(ISO.TIME);
126+
118127
Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3);
119128
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z");
120129
assertThat(formatter.parse("14:23:05.003Z", Locale.US))
121130
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 3));
131+
132+
date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 0);
133+
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.000Z");
134+
assertThat(formatter.parse("14:23:05Z", Locale.US))
135+
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 0).toInstant());
122136
}
123137

124138
@Test
125-
void shouldPrintAndParseISODateTime() throws Exception {
139+
void shouldPrintAndParseIsoDateTime() throws Exception {
126140
DateFormatter formatter = new DateFormatter();
127141
formatter.setTimeZone(UTC);
128142
formatter.setIso(ISO.DATE_TIME);
143+
129144
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
130145
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.003Z");
131146
assertThat(formatter.parse("2009-06-01T14:23:05.003Z", Locale.US)).isEqualTo(date);
147+
148+
date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 0);
149+
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.000Z");
150+
assertThat(formatter.parse("2009-06-01T14:23:05Z", Locale.US)).isEqualTo(date.toInstant());
132151
}
133152

134153
@Test
135154
void shouldThrowOnUnsupportedStylePattern() {
136155
DateFormatter formatter = new DateFormatter();
137156
formatter.setStylePattern("OO");
138-
assertThatIllegalStateException().isThrownBy(() ->
139-
formatter.parse("2009", Locale.US))
140-
.withMessageContaining("Unsupported style pattern 'OO'");
157+
158+
assertThatIllegalStateException().isThrownBy(() -> formatter.parse("2009", Locale.US))
159+
.withMessageContaining("Unsupported style pattern 'OO'");
141160
}
142161

143162
@Test
@@ -148,8 +167,8 @@ void shouldUseCorrectOrder() {
148167
formatter.setStylePattern("L-");
149168
formatter.setIso(ISO.DATE_TIME);
150169
formatter.setPattern("yyyy");
151-
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
152170

171+
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
153172
assertThat(formatter.print(date, Locale.US)).as("uses pattern").isEqualTo("2009");
154173

155174
formatter.setPattern("");

0 commit comments

Comments
 (0)