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 c7f1a6d27 Take FastDatePrinter era names from the locale's calendar 
(#1800)
c7f1a6d27 is described below

commit c7f1a6d27707e8cd8cbed94f83e0c58d2c480614
Author: alhuda <[email protected]>
AuthorDate: Mon Oct 5 01:49:21 2026 +0530

    Take FastDatePrinter era names from the locale's calendar (#1800)
    
    * take FastDatePrinter era names from the locale calendar
    
    DateFormatSymbols.getEras() only knows the two Gregorian eras, but the 
printer formats with the locale's calendar, which is Buddhist for th_TH and 
Japanese Imperial for ja_JP_JP. Build the era table from that calendar's 
display names instead; Gregorian locales keep the symbols table.
    
    * resolve FastDatePrinter era name from the calendar being formatted
    
    format(Calendar) applies the rules to the caller's calendar, so an era
    table chosen from the printer's locale alone mislabels a calendar of
    another type: a Gregorian calendar formatted by a ja_JP_JP printer has
    ERA=1 and printed the Meiji era name instead of the Gregorian AD name,
    and a th_TH printer printed the Buddhist era name for it.
    
    Replace the TextField era rule with an EraField rule that picks the
    name by the type of the calendar being formatted. The locale's own
    calendar still uses the prebuilt table, a Gregorian calendar keeps
    DateFormatSymbols.getEras(), and any other calendar is asked for its
    own era display name, as SimpleDateFormat does. The lookups are bounds
    checked, so a Japanese Imperial calendar formatted by a Gregorian
    locale no longer throws ArrayIndexOutOfBoundsException either.
---
 .../apache/commons/lang3/time/FastDatePrinter.java | 89 +++++++++++++++++++++-
 .../commons/lang3/time/FastDateFormatTest.java     | 15 ++++
 .../commons/lang3/time/FastDatePrinterTest.java    | 43 +++++++++++
 3 files changed, 145 insertions(+), 2 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 2b4b12bc8..457b955bb 100644
--- a/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
+++ b/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
@@ -24,17 +24,21 @@
 import java.text.FieldPosition;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.TimeZone;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.CharUtils;
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.LocaleUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 
 /**
@@ -168,6 +172,88 @@ public int estimateLength() {
         }
     }
 
+    /**
+     * Inner class to output the era name of the calendar being formatted.
+     * <p>
+     * {@link DateFormatSymbols#getEras()} only knows the two Gregorian eras. 
A locale whose default calendar is not Gregorian, like the Thai Buddhist or
+     * Japanese Imperial calendar, prints that calendar's year, so the era 
names have to come from that calendar as well, as {@link SimpleDateFormat} and
+     * {@link FastDateParser} do. The Japanese Imperial calendar also has more 
than two eras, so indexing the Gregorian names by its era value would throw an
+     * {@link ArrayIndexOutOfBoundsException}.
+     * </p>
+     * <p>
+     * {@link FastDatePrinter#format(Calendar)} applies the rules to the 
caller's calendar, which need not be of the type the locale defaults to. An era 
value
+     * only has a meaning in the calendar it comes from, so the names are 
picked by the type of the calendar being formatted.
+     * </p>
+     */
+    private static final class EraField implements Rule {
+
+        /** The calendar type of a Gregorian calendar. */
+        private static final String GREGORY = "gregory";
+
+        private final String calendarType;
+        private final String[] values;
+        private final String[] gregorianValues;
+        private final int style;
+        private final Locale locale;
+
+        /**
+         * Constructs an instance of {@link EraField}.
+         *
+         * @param calendar A calendar of the type the printer's locale 
defaults to.
+         * @param gregorianValues The Gregorian era names.
+         * @param style {@link Calendar#SHORT} or {@link Calendar#LONG}.
+         * @param locale The locale.
+         */
+        EraField(final Calendar calendar, final String[] gregorianValues, 
final int style, final Locale locale) {
+            this.calendarType = calendar.getCalendarType();
+            this.gregorianValues = gregorianValues;
+            this.style = style;
+            this.locale = locale;
+            // The Buddhist calendar extends GregorianCalendar, so test the 
calendar type rather than the class.
+            final Map<String, Integer> displayNames = 
GREGORY.equals(calendarType) ? null : calendar.getDisplayNames(Calendar.ERA, 
style, locale);
+            if (displayNames == null) {
+                this.values = gregorianValues;
+            } else {
+                final String[] eras = new 
String[calendar.getMaximum(Calendar.ERA) + 1];
+                Arrays.fill(eras, StringUtils.EMPTY);
+                displayNames.forEach((name, era) -> eras[era] = name);
+                this.values = eras;
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void appendTo(final Appendable buffer, final Calendar calendar) 
throws IOException {
+            final String type = calendar.getCalendarType();
+            final int era = calendar.get(Calendar.ERA);
+            String value = null;
+            if (calendarType.equals(type)) {
+                value = ArrayUtils.get(values, era);
+            } else if (!GREGORY.equals(type)) {
+                // Not the calendar the names were built for, so ask the 
calendar itself.
+                value = calendar.getDisplayName(Calendar.ERA, style, locale);
+            }
+            buffer.append(value != null ? value : 
ArrayUtils.get(gregorianValues, era, StringUtils.EMPTY));
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int estimateLength() {
+            int max = 0;
+            for (int i = values.length; --i >= 0;) {
+                final int len = values[i].length();
+                if (len > max) {
+                    max = len;
+                }
+            }
+            return max;
+        }
+    }
+
     /**
      * Inner class to output a time zone as a number {@code +/-HHMM}
      * or {@code +/-HH:MM}.
@@ -1368,7 +1454,6 @@ protected List<Rule> parsePattern() {
         final DateFormatSymbols symbols = new DateFormatSymbols(locale);
         final List<Rule> rules = new ArrayList<>();
 
-        final String[] ERAs = symbols.getEras();
         final String[] months = symbols.getMonths();
         final String[] shortMonths = symbols.getShortMonths();
         final String[] weekdays = symbols.getWeekdays();
@@ -1393,7 +1478,7 @@ protected List<Rule> parsePattern() {
 
             switch (c) {
             case 'G': // era designator (text)
-                rule = new TextField(Calendar.ERA, ERAs);
+                rule = new EraField(newCalendar(), symbols.getEras(), tokenLen 
>= 4 ? Calendar.LONG : Calendar.SHORT, locale);
                 break;
             case 'y': // year (number)
             case 'Y': // week year
diff --git 
a/src/test/java/org/apache/commons/lang3/time/FastDateFormatTest.java 
b/src/test/java/org/apache/commons/lang3/time/FastDateFormatTest.java
index 209a47d6b..50f66b50d 100644
--- a/src/test/java/org/apache/commons/lang3/time/FastDateFormatTest.java
+++ b/src/test/java/org/apache/commons/lang3/time/FastDateFormatTest.java
@@ -290,6 +290,21 @@ void testDateDefaults() {
                 FastDateFormat.getDateInstance(FastDateFormat.LONG, 
TimeZone.getDefault(), Locale.getDefault()));
     }
 
+    /**
+     * Tests that a formatted era parses back when the locale's calendar is 
not Gregorian.
+     */
+    @Test
+    void testEraRoundTripNonGregorianCalendar() throws ParseException {
+        final Calendar cal = Calendar.getInstance(TimeZones.GMT, Locale.US);
+        cal.clear();
+        cal.set(2024, Calendar.MAY, 1);
+        final Date date = cal.getTime();
+        for (final Locale locale : new Locale[] { new Locale("th", "TH"), new 
Locale("ja", "JP", "JP") }) {
+            final FastDateFormat format = FastDateFormat.getInstance("G 
yyyy-MM-dd", TimeZones.GMT, locale);
+            assertEquals(date, format.parse(format.format(date)), 
locale::toString);
+        }
+    }
+
     /**
      * Pre-patch: UnsupportedOperationException when formatting a Japanese 
Imperial
      * <p>
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 8581774b8..e7cf4544c 100644
--- a/src/test/java/org/apache/commons/lang3/time/FastDatePrinterTest.java
+++ b/src/test/java/org/apache/commons/lang3/time/FastDatePrinterTest.java
@@ -167,6 +167,49 @@ void testEquals() {
         assertNotEquals(printer1, new Object());
     }
 
+    /**
+     * Tests that 'G' names the era of the calendar being formatted when that 
calendar is not of the type the locale defaults to, like SimpleDateFormat.
+     */
+    @Test
+    void testEraMixedCalendarTypes() {
+        final Calendar cal = Calendar.getInstance(TimeZones.GMT, Locale.US);
+        cal.clear();
+        cal.set(2024, Calendar.MAY, 1);
+        final Date date = cal.getTime();
+        // Gregorian, Thai Buddhist and Japanese Imperial calendars.
+        final Locale[] locales = { Locale.US, new Locale("th", "TH"), new 
Locale("ja", "JP", "JP") };
+        for (final Locale locale : locales) {
+            for (final Locale calendarLocale : locales) {
+                final Calendar calendar = Calendar.getInstance(TimeZones.GMT, 
calendarLocale);
+                calendar.setTime(date);
+                for (final String pattern : new String[] { "G", "GGGG" }) {
+                    final SimpleDateFormat sdf = new SimpleDateFormat(pattern, 
locale);
+                    sdf.setCalendar((Calendar) calendar.clone());
+                    assertEquals(sdf.format(date), getInstance(pattern, 
TimeZones.GMT, locale).format(calendar),
+                            () -> locale + " " + calendar.getCalendarType() + 
" " + pattern);
+                }
+            }
+        }
+    }
+
+    /**
+     * Tests that 'G' uses the era names of the locale's calendar when that 
calendar is not Gregorian, like SimpleDateFormat.
+     */
+    @Test
+    void testEraNonGregorianCalendar() {
+        final Calendar cal = Calendar.getInstance(TimeZones.GMT, Locale.US);
+        cal.clear();
+        cal.set(2024, Calendar.MAY, 1);
+        final Date date = cal.getTime();
+        for (final Locale locale : new Locale[] { new Locale("th", "TH"), new 
Locale("ja", "JP", "JP") }) {
+            for (final String pattern : new String[] { "G", "GGGG" }) {
+                final SimpleDateFormat sdf = new SimpleDateFormat(pattern, 
locale);
+                sdf.setTimeZone(TimeZones.GMT);
+                assertEquals(sdf.format(date), getInstance(pattern, 
TimeZones.GMT, locale).format(date), () -> locale + " " + pattern);
+            }
+        }
+    }
+
     @DefaultLocale(language = "en", country = "US")
     @DefaultTimeZone("America/New_York")
     @Test

Reply via email to