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


The following commit(s) were added to refs/heads/master by this push:
     new 6b98966dd Use standalone month names for month-only patterns in 
FastDatePrinter (#1815)
6b98966dd is described below

commit 6b98966dd7d440b8b8690e5790ed8c2a1414b9c0
Author: alhuda <[email protected]>
AuthorDate: Fri Oct 16 00:07:52 2026 +0530

    Use standalone month names for month-only patterns in FastDatePrinter 
(#1815)
    
    * use standalone month names for month-only patterns in FastDatePrinter
    
    * hoist CalendarUtils lookup in parsePattern and read parsed month in GMT
    
    * Update month patterns in FastDatePrinterTest
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    ---------
    
    Co-authored-by: Gary Gregory <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../apache/commons/lang3/time/FastDatePrinter.java | 47 ++++++++++++++++++++--
 .../commons/lang3/time/FastDatePrinterTest.java    | 34 ++++++++++++++++
 2 files changed, 77 insertions(+), 4 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 457b955bb..dbe482f69 100644
--- a/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
+++ b/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
@@ -37,8 +37,10 @@
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.CharUtils;
 import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.JavaVersion;
 import org.apache.commons.lang3.LocaleUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.SystemUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 
 /**
@@ -1015,6 +1017,9 @@ public int estimateLength() {
 
     private static final int MAX_DIGITS = 10; // log10(Integer.MAX_VALUE) ~= 
9.3
 
+    /** Whether SimpleDateFormat formats a month-only pattern with the 
standalone month names; since Java 9 (JDK-8114833). */
+    private static final boolean STANDALONE_MONTH_PATTERN = 
SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9);
+
     private static final ConcurrentMap<TimeZoneDisplayKey, String> 
timeZoneDisplayCache = new ConcurrentHashMap<>(7);
 
     /**
@@ -1462,6 +1467,8 @@ 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);
 
         for (int i = 0; i < length; i++) {
             indexRef[0] = i;
@@ -1493,9 +1500,9 @@ protected List<Rule> parsePattern() {
                 break;
             case 'M': // month in year (text and number)
                 if (tokenLen >= 4) {
-                    rule = new TextField(Calendar.MONTH, months);
+                    rule = new TextField(Calendar.MONTH, monthOnly ? 
calendarUtils.getStandaloneLongMonthNames() : months);
                 } else if (tokenLen == 3) {
-                    rule = new TextField(Calendar.MONTH, shortMonths);
+                    rule = new TextField(Calendar.MONTH, monthOnly ? 
calendarUtils.getStandaloneShortMonthNames() : shortMonths);
                 } else if (tokenLen == 2) {
                     rule = TwoDigitMonthField.INSTANCE;
                 } else {
@@ -1504,9 +1511,9 @@ protected List<Rule> parsePattern() {
                 break;
             case 'L': // month in year (text and number)
                 if (tokenLen >= 4) {
-                    rule = new TextField(Calendar.MONTH, 
CalendarUtils.getInstance(locale).getStandaloneLongMonthNames());
+                    rule = new TextField(Calendar.MONTH, 
calendarUtils.getStandaloneLongMonthNames());
                 } else if (tokenLen == 3) {
-                    rule = new TextField(Calendar.MONTH, 
CalendarUtils.getInstance(locale).getStandaloneShortMonthNames());
+                    rule = new TextField(Calendar.MONTH, 
calendarUtils.getStandaloneShortMonthNames());
                 } else if (tokenLen == 2) {
                     rule = TwoDigitMonthField.INSTANCE;
                 } else {
@@ -1591,6 +1598,38 @@ 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 e7cf4544c..3bdc7bc7b 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.assumeTrue;
 
 import java.io.Serializable;
 import java.text.FieldPosition;
+import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.Date;
@@ -31,7 +33,9 @@
 import java.util.TimeZone;
 
 import org.apache.commons.lang3.AbstractLangTest;
+import org.apache.commons.lang3.JavaVersion;
 import org.apache.commons.lang3.SerializationUtils;
+import org.apache.commons.lang3.SystemUtils;
 import org.junit.jupiter.api.Test;
 import org.junitpioneer.jupiter.DefaultLocale;
 import org.junitpioneer.jupiter.DefaultTimeZone;
@@ -210,6 +214,36 @@ 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

Reply via email to