@@ -339,6 +339,70 @@ void isoDate(String propertyValue) {
339339 assertThat (bindingResult .getFieldValue (propertyName )).isEqualTo ("2021-03-02" );
340340 }
341341
342+ /**
343+ * {@link SimpleDateBean#styleDateTimeWithFallbackPatternsForPreAndPostJdk20}
344+ * configures "SS" as the date/time style to use. Thus, we have to be aware
345+ * of the following if we do not configure fallback patterns for parsing.
346+ *
347+ * <ul>
348+ * <li>JDK ≤ 19 requires a standard space before the "PM".
349+ * <li>JDK ≥ 20 requires a narrow non-breaking space (NNBSP) before the "PM".
350+ * </ul>
351+ *
352+ * <p>To avoid compatibility issues between JDK versions, we have configured
353+ * two fallback patterns which emulate the "SS" style: <code>"MM/dd/yy h:mm a"</code>
354+ * matches against a standard space before the "PM", and <code>"MM/dd/yy h:mm\u202Fa"</code>
355+ * matches against a narrow non-breaking space (NNBSP) before the "PM".
356+ *
357+ * <p>Thus, the following should theoretically be supported on any JDK (or at least
358+ * JDK 17 - 23, where we have tested it).
359+ *
360+ * @see #patternDateTime(String)
361+ */
362+ @ ParameterizedTest (name = "input date: {0}" ) // gh-33151
363+ @ ValueSource (strings = {"10/31/09, 12:00 PM" , "10/31/09, 12:00\u202F PM" })
364+ void styleDateTime_PreAndPostJdk20 (String propertyValue ) {
365+ String propertyName = "styleDateTimeWithFallbackPatternsForPreAndPostJdk20" ;
366+ MutablePropertyValues propertyValues = new MutablePropertyValues ();
367+ propertyValues .add (propertyName , propertyValue );
368+ binder .bind (propertyValues );
369+ BindingResult bindingResult = binder .getBindingResult ();
370+ assertThat (bindingResult .getErrorCount ()).isEqualTo (0 );
371+ String value = binder .getBindingResult ().getFieldValue (propertyName ).toString ();
372+ // Since the "SS" style is always used for printing and the underlying format
373+ // changes depending on the JDK version, we cannot be certain that a normal
374+ // space is used before the "PM". Consequently we have to use a regular
375+ // expression to match against any Unicode space character (\p{Zs}).
376+ assertThat (value ).startsWith ("10/31/09" ).matches (".+?12:00\\ p{Zs}PM" );
377+ }
378+
379+ /**
380+ * To avoid the use of Locale-based styles (such as "MM") for
381+ * {@link SimpleDateBean#patternDateTimeWithFallbackPatternForPreAndPostJdk20}, we have configured a
382+ * primary pattern (<code>"MM/dd/yy h:mm a"</code>) that matches against a standard space
383+ * before the "PM" and a fallback pattern (<code>"MM/dd/yy h:mm\u202Fa"</code> that matches
384+ * against a narrow non-breaking space (NNBSP) before the "PM".
385+ *
386+ * <p>Thus, the following should theoretically be supported on any JDK (or at least
387+ * JDK 17 - 23, where we have tested it).
388+ *
389+ * @see #styleDateTime(String)
390+ */
391+ @ ParameterizedTest (name = "input date: {0}" ) // gh-33151
392+ @ ValueSource (strings = {"10/31/09 3:45 PM" , "10/31/09 3:45\u202F PM" })
393+ void patternDateTime_PreAndPostJdk20 (String propertyValue ) {
394+ String propertyName = "patternDateTimeWithFallbackPatternForPreAndPostJdk20" ;
395+ MutablePropertyValues propertyValues = new MutablePropertyValues ();
396+ propertyValues .add (propertyName , propertyValue );
397+ binder .bind (propertyValues );
398+ BindingResult bindingResult = binder .getBindingResult ();
399+ assertThat (bindingResult .getErrorCount ()).isEqualTo (0 );
400+ String value = binder .getBindingResult ().getFieldValue (propertyName ).toString ();
401+ // Since the "MM/dd/yy h:mm a" primary pattern is always used for printing, we
402+ // can be certain that a normal space is used before the "PM".
403+ assertThat (value ).matches ("10/31/09 3:45 PM" );
404+ }
405+
342406 @ Test
343407 void patternDateWithUnsupportedPattern () {
344408 String propertyValue = "210302" ;
@@ -389,12 +453,23 @@ private static class SimpleDateBean {
389453 @ DateTimeFormat (style = "S-" , fallbackPatterns = { "yyyy-MM-dd" , "yyyyMMdd" , "yyyy.MM.dd" })
390454 private Date styleDateWithFallbackPatterns ;
391455
456+ // "SS" style matches either a standard space or a narrow non-breaking space (NNBSP) before AM/PM,
457+ // depending on the version of the JDK.
458+ // Fallback patterns match a standard space OR a narrow non-breaking space (NNBSP) before AM/PM.
459+ @ DateTimeFormat (style = "SS" , fallbackPatterns = { "M/d/yy, h:mm a" , "M/d/yy, h:mm\u202F a" })
460+ private Date styleDateTimeWithFallbackPatternsForPreAndPostJdk20 ;
461+
392462 @ DateTimeFormat (pattern = "M/d/yy h:mm" )
393463 private Date patternDate ;
394464
395465 @ DateTimeFormat (pattern = "yyyy-MM-dd" , fallbackPatterns = { "M/d/yy" , "yyyyMMdd" , "yyyy.MM.dd" })
396466 private Date patternDateWithFallbackPatterns ;
397467
468+ // Primary pattern matches a standard space before AM/PM.
469+ // Fallback pattern matches a narrow non-breaking space (NNBSP) before AM/PM.
470+ @ DateTimeFormat (pattern = "MM/dd/yy h:mm a" , fallbackPatterns = "MM/dd/yy h:mm\u202F a" )
471+ private Date patternDateTimeWithFallbackPatternForPreAndPostJdk20 ;
472+
398473 @ DateTimeFormat (iso = ISO .DATE )
399474 private Date isoDate ;
400475
@@ -459,6 +534,14 @@ public void setStyleDateWithFallbackPatterns(Date styleDateWithFallbackPatterns)
459534 this .styleDateWithFallbackPatterns = styleDateWithFallbackPatterns ;
460535 }
461536
537+ public Date getStyleDateTimeWithFallbackPatternsForPreAndPostJdk20 () {
538+ return this .styleDateTimeWithFallbackPatternsForPreAndPostJdk20 ;
539+ }
540+
541+ public void setStyleDateTimeWithFallbackPatternsForPreAndPostJdk20 (Date styleDateTimeWithFallbackPatternsForPreAndPostJdk20 ) {
542+ this .styleDateTimeWithFallbackPatternsForPreAndPostJdk20 = styleDateTimeWithFallbackPatternsForPreAndPostJdk20 ;
543+ }
544+
462545 public Date getPatternDate () {
463546 return this .patternDate ;
464547 }
@@ -475,6 +558,14 @@ public void setPatternDateWithFallbackPatterns(Date patternDateWithFallbackPatte
475558 this .patternDateWithFallbackPatterns = patternDateWithFallbackPatterns ;
476559 }
477560
561+ public Date getPatternDateTimeWithFallbackPatternForPreAndPostJdk20 () {
562+ return this .patternDateTimeWithFallbackPatternForPreAndPostJdk20 ;
563+ }
564+
565+ public void setPatternDateTimeWithFallbackPatternForPreAndPostJdk20 (Date patternDateTimeWithFallbackPatternForPreAndPostJdk20 ) {
566+ this .patternDateTimeWithFallbackPatternForPreAndPostJdk20 = patternDateTimeWithFallbackPatternForPreAndPostJdk20 ;
567+ }
568+
478569 public Date getIsoDate () {
479570 return this .isoDate ;
480571 }
0 commit comments