This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git

commit 66f93099008bfe6485164166e4c0d70e5dade96a
Author: Gary Gregory <[email protected]>
AuthorDate: Thu Oct 15 14:51:41 2026 -0400

    Use standalone month names for month-only patterns in FastDatePrinter
    (#1815)
    
    Create CalendarUtils only when a rule needs standalone month names.
    
    Clarify the month-only pattern Javadoc and expand regression coverage
    for Java 8, numeric months, literals, and mixed month fields. Use a
    Cartesian test for locale, pattern, and month combinations.
    Validated with the default Maven build and focused tests on Java 8
    and Java 21.
---
 .../apache/commons/lang3/time/FastDatePrinter.java |  70 +++++++-------
 .../commons/lang3/time/FastDatePrinterTest.java    | 104 +++++++++++++++------
 2 files changed, 111 insertions(+), 63 deletions(-)

diff --git a/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java 
b/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
index dbe482f69..ab42fab78 100644
--- a/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
+++ b/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
@@ -1440,6 +1440,39 @@ private void init() {
         maxLengthEstimate = len;
     }
 
+    /**
+     * Tests whether the pattern contains exactly one {@code M} field 
occurrence and no other fields.
+     * <p>
+     * Since Java 9, {@link SimpleDateFormat} formats the text month of such a 
pattern with standalone month names. In some languages, like the Slavic and
+     * Baltic ones, these have a different grammatical form from the names 
used next to a day ("{@code MMMM}" alone prints Russian February as nominative
+     * "{@code февраль}" where "{@code d MMMM}" prints genitive "{@code 
февраля}"). Quoted and unquoted literals are ignored, but repeated fields such 
as
+     * "{@code MMM MMM}" prevent standalone formatting.
+     * </p>
+     *
+     * @return Whether the pattern contains exactly one {@code M} field 
occurrence and no other fields.
+     */
+    private boolean isMonthOnlyPattern() {
+        final int[] indexRef = new int[1];
+        boolean monthField = false;
+        for (int i = 0; i < pattern.length(); i++) {
+            indexRef[0] = i;
+            final String token = parseToken(pattern, indexRef);
+            i = indexRef[0];
+            if (token.isEmpty()) {
+                break;
+            }
+            final char c = token.charAt(0);
+            if (c == '\'') {
+                continue;
+            }
+            if (c != 'M' || monthField) {
+                return false;
+            }
+            monthField = true;
+        }
+        return monthField;
+    }
+
     /**
      * Creates a new Calendar instance.
      *
@@ -1468,7 +1501,7 @@ protected List<Rule> parsePattern() {
         final int length = pattern.length();
         final int[] indexRef = new int[1];
         final boolean monthOnly = STANDALONE_MONTH_PATTERN && 
isMonthOnlyPattern();
-        final CalendarUtils calendarUtils = CalendarUtils.getInstance(locale);
+        CalendarUtils calendarUtils = null;
 
         for (int i = 0; i < length; i++) {
             indexRef[0] = i;
@@ -1482,6 +1515,9 @@ protected List<Rule> parsePattern() {
 
             Rule rule;
             final char c = token.charAt(0);
+            if (calendarUtils == null && tokenLen >= 3 && (c == 'L' || c == 
'M' && monthOnly)) {
+                calendarUtils = CalendarUtils.getInstance(locale);
+            }
 
             switch (c) {
             case 'G': // era designator (text)
@@ -1598,38 +1634,6 @@ protected List<Rule> parsePattern() {
         return rules;
     }
 
-    /**
-     * Tests whether the month is the only field in the pattern.
-     * <p>
-     * {@link SimpleDateFormat} formats the text month of such a pattern with 
the standalone month names, which in some languages, like the Slavic and Baltic
-     * ones, are a different grammatical form from the names used next to a 
day ("{@code MMMM}" alone prints Russian February as nominative
-     * "{@code февраль}" where "{@code d MMMM}" prints genitive "{@code 
февраля}").
-     * </p>
-     *
-     * @return Whether the pattern contains exactly one field, the month.
-     */
-    private boolean isMonthOnlyPattern() {
-        final int[] indexRef = new int[1];
-        boolean monthField = false;
-        for (int i = 0; i < pattern.length(); i++) {
-            indexRef[0] = i;
-            final String token = parseToken(pattern, indexRef);
-            i = indexRef[0];
-            if (token.isEmpty()) {
-                break;
-            }
-            final char c = token.charAt(0);
-            if (c == '\'') {
-                continue;
-            }
-            if (c != 'M' || monthField) {
-                return false;
-            }
-            monthField = true;
-        }
-        return monthField;
-    }
-
     /**
      * Performs the parsing of tokens.
      *
diff --git 
a/src/test/java/org/apache/commons/lang3/time/FastDatePrinterTest.java 
b/src/test/java/org/apache/commons/lang3/time/FastDatePrinterTest.java
index 3bdc7bc7b..0616e46a9 100644
--- a/src/test/java/org/apache/commons/lang3/time/FastDatePrinterTest.java
+++ b/src/test/java/org/apache/commons/lang3/time/FastDatePrinterTest.java
@@ -20,9 +20,11 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 import java.io.Serializable;
+import java.text.DateFormatSymbols;
 import java.text.FieldPosition;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
@@ -39,6 +41,8 @@
 import org.junit.jupiter.api.Test;
 import org.junitpioneer.jupiter.DefaultLocale;
 import org.junitpioneer.jupiter.DefaultTimeZone;
+import org.junitpioneer.jupiter.cartesian.ArgumentSets;
+import org.junitpioneer.jupiter.cartesian.CartesianTest;
 
 /**
  * Tests {@link FastDatePrinter}.
@@ -80,6 +84,15 @@ private static Calendar initializeCalendar(final TimeZone 
tz) {
         return cal;
     }
 
+    static ArgumentSets testStandaloneMonthForMonthOnlyPatternFactory() {
+        // Month-only patterns, with or without literals, use the standalone 
form; patterns with another field keep the format form.
+        return ArgumentSets.argumentsForFirstParameter(Locale.US, 
Locale.GERMANY, new Locale("ru"), new Locale("cs"), new Locale("pl"), new 
Locale("fi"))
+                .argumentsForNextParameter("M", "MM", "MMM", "MMMM", "MMMMM", 
"'m:'MMMM", "MMMM''", "MMMM,", "MMMM-", "MMMM ", "d MMMM", "MMMM yyyy",
+                        "MMM MMM", "MMM LLL", "MMMM LLLL", "LLL", "LLLL")
+                .argumentsForNextParameter(Calendar.JANUARY, 
Calendar.FEBRUARY, Calendar.MARCH, Calendar.APRIL, Calendar.MAY, Calendar.JUNE, 
Calendar.JULY,
+                        Calendar.AUGUST, Calendar.SEPTEMBER, Calendar.OCTOBER, 
Calendar.NOVEMBER, Calendar.DECEMBER);
+    }
+
     private DatePrinter getDateInstance(final int dateStyle, final Locale 
locale) {
         return 
getInstance(AbstractFormatCache.getPatternForStyle(Integer.valueOf(dateStyle), 
null, locale), TimeZone.getDefault(), Locale.getDefault());
     }
@@ -214,36 +227,6 @@ void testEraNonGregorianCalendar() {
         }
     }
 
-    /**
-     * Tests that a month-only pattern uses the standalone month names, like 
SimpleDateFormat. In some languages, like the Slavic and Baltic ones, they are a
-     * different grammatical form from the names used next to a day: "MMMM" 
alone prints Russian February as nominative "февраль" where "d MMMM" prints
-     * genitive "февраля".
-     */
-    @Test
-    void testStandaloneMonthForMonthOnlyPattern() throws ParseException {
-        // SimpleDateFormat forces the standalone form for a month-only 
pattern only since Java 9 (JDK-8114833); Java 8 has no such rule.
-        assumeTrue(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9));
-        final Locale[] locales = { Locale.US, Locale.GERMANY, new 
Locale("ru"), new Locale("cs"), new Locale("pl"), new Locale("fi") };
-        // Month-only patterns, with or without literals, use the standalone 
form; patterns with another field keep the format form.
-        final String[] patterns = { "MMM", "MMMM", "'m:'MMMM", "MMMM''", 
"MMMM,", "d MMMM", "MMMM yyyy", "MMM MMM" };
-        for (final Locale locale : locales) {
-            for (final String pattern : patterns) {
-                for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; 
month++) {
-                    final Calendar cal = Calendar.getInstance(TimeZones.GMT, 
locale);
-                    cal.clear();
-                    cal.set(2024, month, 10);
-                    final Date date = cal.getTime();
-                    final SimpleDateFormat sdf = new SimpleDateFormat(pattern, 
locale);
-                    sdf.setTimeZone(TimeZones.GMT);
-                    final FastDateFormat fdf = 
FastDateFormat.getInstance(pattern, TimeZones.GMT, locale);
-                    assertEquals(sdf.format(date), fdf.format(date), () -> 
locale + " " + pattern);
-                    // FastDateParser accepts all name styles, so the 
printer's output parses back.
-                    assertEquals(month, 
DateUtils.toCalendar(fdf.parse(fdf.format(date)), 
TimeZones.GMT).get(Calendar.MONTH), () -> locale + " " + pattern);
-                }
-            }
-        }
-    }
-
     @DefaultLocale(language = "en", country = "US")
     @DefaultTimeZone("America/New_York")
     @Test
@@ -421,12 +404,46 @@ void testMilleniumBug() {
         assertEquals("01.01.1000", format.format(cal));
     }
 
+    @Test
+    void testMonthOnlyPatternOnJava8() {
+        assumeFalse(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9));
+        final Locale[] locales = { Locale.US, Locale.GERMANY, new 
Locale("ru"), new Locale("cs"), new Locale("pl"), new Locale("fi") };
+        for (final Locale locale : locales) {
+            final DateFormatSymbols symbols = new DateFormatSymbols(locale);
+            for (int month = Calendar.JANUARY; month <= Calendar.DECEMBER; 
month++) {
+                final Calendar cal = Calendar.getInstance(TimeZones.GMT, 
locale);
+                cal.clear();
+                cal.set(2024, month, 10);
+                final Date date = cal.getTime();
+                // Preserve the DateFormatSymbols names used before the Java 9 
standalone-month rule.
+                assertEquals(symbols.getShortMonths()[month], 
getInstance("MMM", TimeZones.GMT, locale).format(date), () -> locale + " MMM");
+                assertEquals(symbols.getMonths()[month], getInstance("MMMM", 
TimeZones.GMT, locale).format(date), () -> locale + " MMMM");
+                assertEquals(symbols.getMonths()[month], getInstance("MMMMM", 
TimeZones.GMT, locale).format(date), () -> locale + " MMMMM");
+                assertEquals(symbols.getMonths()[month] + ",", 
getInstance("MMMM,", TimeZones.GMT, locale).format(date), () -> locale + " 
MMMM,");
+            }
+        }
+    }
+
     @Test
     void testPatternMatches() {
         final DatePrinter printer = getInstance(YYYY_MM_DD);
         assertEquals(YYYY_MM_DD, printer.getPattern());
     }
 
+    @Test
+    void testPatternsWithoutTextMonthFields() {
+        final Calendar cal = Calendar.getInstance(TimeZones.GMT, Locale.US);
+        cal.clear();
+        cal.set(2024, Calendar.FEBRUARY, 10, 13, 45);
+        final Date date = cal.getTime();
+        final String[] patterns = { "M", "MM", "L", "LL", "yyyy-MM-dd", 
"'MMMM'", "'abc'", "", "HH:mm", "yyyy-dd" };
+        for (final String pattern : patterns) {
+            final SimpleDateFormat sdf = new SimpleDateFormat(pattern, 
Locale.US);
+            sdf.setTimeZone(TimeZones.GMT);
+            assertEquals(sdf.format(date), getInstance(pattern, TimeZones.GMT, 
Locale.US).format(date), () -> pattern);
+        }
+    }
+
     /**
      * Test case for {@link FastDateParser#FastDateParser(String, TimeZone, 
Locale)}.
      */
@@ -460,6 +477,33 @@ void testSimpleDate() {
         assertEquals("0001/03/02", format.format(cal));
     }
 
+    /**
+     * Tests that a month-only pattern uses the standalone month names, like 
SimpleDateFormat. In some languages, like the Slavic and Baltic ones, they are a
+     * different grammatical form from the names used next to a day: "MMMM" 
alone prints Russian February as nominative "февраль" where "d MMMM" prints
+     * genitive "февраля".
+     *
+     * @param locale the locale to test.
+     * @param pattern the date format pattern to test.
+     * @param month the calendar month to test.
+     * @throws ParseException if the formatted date cannot be parsed.
+     */
+    @CartesianTest
+    
@CartesianTest.MethodFactory("testStandaloneMonthForMonthOnlyPatternFactory")
+    void testStandaloneMonthForMonthOnlyPattern(final Locale locale, final 
String pattern, final int month) throws ParseException {
+        // SimpleDateFormat forces the standalone form for a month-only 
pattern only since Java 9 (JDK-8114833); Java 8 has no such rule.
+        assumeTrue(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9));
+        final Calendar cal = Calendar.getInstance(TimeZones.GMT, locale);
+        cal.clear();
+        cal.set(2024, month, 10);
+        final Date date = cal.getTime();
+        final SimpleDateFormat sdf = new SimpleDateFormat(pattern, locale);
+        sdf.setTimeZone(TimeZones.GMT);
+        final FastDateFormat fdf = FastDateFormat.getInstance(pattern, 
TimeZones.GMT, locale);
+        assertEquals(sdf.format(date), fdf.format(date), () -> locale + " " + 
pattern);
+        // FastDateParser accepts all name styles, so the printer's output 
parses back.
+        assertEquals(month, DateUtils.toCalendar(fdf.parse(fdf.format(date)), 
TimeZones.GMT).get(Calendar.MONTH), () -> locale + " " + pattern);
+    }
+
     @SuppressWarnings("deprecation")
     @Test
     void testStringBufferOptions() {

Reply via email to