Skip to content

Commit 5cbcf9c

Browse files
Fix SimpleDateFormat parsing of quoted literals and add X, Y, u support. (#4342)
Fixed #3489 * Fixes parsing logic to correctly handle escaped single quotes (e.g. 'o''clock') and prevents exceptions when quoted strings are followed by non-pattern characters. * Adds support for pattern letters 'X' (ISO 8601 Timezone), 'Y' (Week Year), and 'u' (Day number of week) to prevent IllegalArgumentException. * Adds regression tests in SimpleDateFormatTest.java. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 7af2bce commit 5cbcf9c

File tree

2 files changed

+116
-8
lines changed

2 files changed

+116
-8
lines changed

CodenameOne/src/com/codename1/l10n/SimpleDateFormat.java

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ public class SimpleDateFormat extends DateFormat {
118118
* Pattern character for RFC 822-style timezone.
119119
*/
120120
private static final char TIMEZONE822_LETTER = 'Z';
121+
/**
122+
* Pattern character for ISO 8601 timezone.
123+
*/
124+
private static final char ISO_TIMEZONE_LETTER = 'X';
125+
/**
126+
* Pattern character for week year.
127+
*/
128+
private static final char WEEK_YEAR_LETTER = 'Y';
129+
/**
130+
* Pattern character for day number of week.
131+
*/
132+
private static final char DAY_NUMBER_OF_WEEK_LETTER = 'u';
121133
/**
122134
* Internally used character for literal text.
123135
*/
@@ -141,7 +153,7 @@ public class SimpleDateFormat extends DateFormat {
141153
/**
142154
* Pattern characters recognized by this implementation (same as JDK 1.6).
143155
*/
144-
private static final String PATTERN_LETTERS = "adDEFGHhKkMmsSwWyzZ";
156+
private static final String PATTERN_LETTERS = "adDEFGHhKkMmsSwWyzZXYu";
145157
/**
146158
* TimeZone ID for Greenwich Mean Time
147159
*/
@@ -371,7 +383,24 @@ String format(Date source, StringBuilder toAppendTo) {
371383
toAppendTo.append(leftPad(v / 60, 2));
372384
toAppendTo.append(leftPad(v % 60, 2));
373385
break;
386+
case ISO_TIMEZONE_LETTER:
387+
v = getOffsetInMinutes(calendar, calendar.getTimeZone());
388+
if (v < 0) {
389+
toAppendTo.append(SIGN_NEGATIVE);
390+
v = -v;
391+
} else {
392+
toAppendTo.append(SIGN_POSITIVE);
393+
}
394+
toAppendTo.append(leftPad(v / 60, 2));
395+
if (len >= 2) {
396+
if (len == 3) {
397+
toAppendTo.append(':');
398+
}
399+
toAppendTo.append(leftPad(v % 60, 2));
400+
}
401+
break;
374402
case YEAR_LETTER:
403+
case WEEK_YEAR_LETTER:
375404
v = calendar.get(Calendar.YEAR);
376405
if (len == 2) {
377406
v %= 100;
@@ -441,6 +470,10 @@ String format(Date source, StringBuilder toAppendTo) {
441470
v = calendar.get(DAY_OF_WEEK_IN_MONTH);
442471
toAppendTo.append(leftPad(v, len));
443472
break;
473+
case DAY_NUMBER_OF_WEEK_LETTER:
474+
v = ((calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7) + 1;
475+
toAppendTo.append(leftPad(v, len));
476+
break;
444477
}
445478
}
446479
return toAppendTo.toString();
@@ -515,6 +548,7 @@ public Date parse(String source) throws ParseException {
515548
break;
516549
case TIMEZONE_LETTER:
517550
case TIMEZONE822_LETTER:
551+
case ISO_TIMEZONE_LETTER:
518552
s = readTimeZone(source, startIndex);
519553
TimeZoneResult res = new TimeZoneResult();
520554
if (s == null || (v = parseTimeZone(s, startIndex, res)) == -1) {
@@ -525,6 +559,7 @@ public Date parse(String source) throws ParseException {
525559
tzMinutes = ((tzMinutes == -1) ? 0 : tzMinutes) + v;
526560
break;
527561
case YEAR_LETTER:
562+
case WEEK_YEAR_LETTER:
528563
s = readNumber(source, startIndex, token, adjacent);
529564
calendar.set(Calendar.YEAR, parseYear(s, token, startIndex));
530565
break;
@@ -572,6 +607,14 @@ public Date parse(String source) throws ParseException {
572607
calendar.set(DAY_OF_WEEK_IN_MONTH,
573608
parseNumber(s, startIndex, "day of week in month", -5, 5));
574609
break;
610+
case DAY_NUMBER_OF_WEEK_LETTER:
611+
s = readNumber(source, startIndex, token, adjacent);
612+
v = parseNumber(s, startIndex, "day number of week", 1, 7);
613+
// Map 1(Mon)-7(Sun) to Calendar 2(Mon)..1(Sun)
614+
// Mon=1 -> 2. Tue=2 -> 3. ... Sat=6 -> 7. Sun=7 -> 1.
615+
// v % 7 + 1
616+
calendar.set(Calendar.DAY_OF_WEEK, (v % 7) + 1);
617+
break;
575618
}
576619
if (s != null) {
577620
startIndex += s.length();
@@ -1186,14 +1229,27 @@ List<String> parseDatePattern(String pattern) {
11861229
char ch = pattern.charAt(i);
11871230
// Handle literal text enclosed in quotes
11881231
if (ch == EXPLICIT_LITERAL) {
1189-
int n = pattern.indexOf(EXPLICIT_LITERAL, i + 1);
1190-
if (n != -1) {
1191-
if (tmp != null) {
1192-
tokens.add(tmp.charAt(0) + tmp);
1193-
tmp = null;
1232+
if (tmp != null) {
1233+
tokens.add(tmp.charAt(0) + tmp);
1234+
tmp = null;
1235+
}
1236+
StringBuilder sb = new StringBuilder();
1237+
int n = i + 1;
1238+
while (n < plen) {
1239+
char c = pattern.charAt(n);
1240+
if (c == EXPLICIT_LITERAL) {
1241+
if (n + 1 < plen && pattern.charAt(n + 1) == EXPLICIT_LITERAL) {
1242+
sb.append(EXPLICIT_LITERAL);
1243+
n += 2;
1244+
continue;
1245+
} else {
1246+
break;
1247+
}
11941248
}
1195-
tokens.add(LITERAL_LETTER + pattern.substring(i + 1, n));
1249+
sb.append(c);
1250+
n++;
11961251
}
1252+
tokens.add(LITERAL_LETTER + sb.toString());
11971253
i = n;
11981254
continue;
11991255
}
@@ -1208,7 +1264,7 @@ List<String> parseDatePattern(String pattern) {
12081264
int n;
12091265
for (n = i; n < plen; n++) {
12101266
ch = pattern.charAt(n);
1211-
if (PATTERN_LETTERS.indexOf(ch) != -1) {
1267+
if (PATTERN_LETTERS.indexOf(ch) != -1 || ch == EXPLICIT_LITERAL) {
12121268
break;
12131269
}
12141270
if (isAlpha(ch)) {

maven/core-unittests/src/test/java/com/codename1/l10n/SimpleDateFormatTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,58 @@ void cloneCopiesPatternAndSymbols() {
133133
assertSame(symbols, clone.getDateFormatSymbols());
134134
}
135135

136+
@Test
137+
void testQuotedStringLiteral() {
138+
// "formats like yyyy.MM.dd G 'at' HH:mm:ss z yield illegal argument exception on the t char."
139+
assertDoesNotThrow(() -> {
140+
new SimpleDateFormat("yyyy.MM.dd G 'at' HH:mm:ss z");
141+
}, "Should not throw exception for quoted literal 'at'");
142+
}
143+
144+
@Test
145+
void testEscapedSingleQuoteFormat() {
146+
// "Also formats like: hh 'o''clock' yield exception for the c char."
147+
SimpleDateFormat sdf = new SimpleDateFormat("hh 'o''clock'");
148+
Calendar cal = Calendar.getInstance();
149+
cal.set(Calendar.HOUR, 5);
150+
String result = sdf.format(cal.getTime());
151+
// Expected: 05 o'clock
152+
assertTrue(result.contains("o'clock"), "Should contain single quote: " + result);
153+
}
154+
155+
@Test
156+
void testXXXFormat() {
157+
// "yyyy-MM-dd'T'HH:mm:ss.SSSXXX (exception for X char, recent API level prerequisite)"
158+
assertDoesNotThrow(() -> {
159+
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
160+
}, "Should not throw exception for XXX format");
161+
162+
SimpleDateFormat sdf = new SimpleDateFormat("XXX");
163+
// We cannot reliably test exact timezone string without controlling default timezone,
164+
// but we can check it doesn't crash and produces something reasonable.
165+
String result = sdf.format(new Date());
166+
assertNotNull(result);
167+
assertFalse(result.isEmpty());
168+
}
169+
170+
@Test
171+
void testYYYYAnduFormat() {
172+
// "YYYY-'W'ww-u (exception for Y char)"
173+
assertDoesNotThrow(() -> {
174+
new SimpleDateFormat("YYYY-'W'ww-u");
175+
}, "Should not throw exception for YYYY and u format");
176+
177+
SimpleDateFormat sdf = new SimpleDateFormat("u");
178+
Calendar cal = Calendar.getInstance();
179+
cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
180+
String result = sdf.format(cal.getTime());
181+
assertEquals("1", result, "Monday should be 1");
182+
183+
cal.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
184+
result = sdf.format(cal.getTime());
185+
assertEquals("7", result, "Sunday should be 7");
186+
}
187+
136188
private static class StubL10NManager extends L10NManager {
137189
StubL10NManager() {
138190
super("en", "US");

0 commit comments

Comments
 (0)