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