Skip to content

Commit 96eb039

Browse files
committed
8324665: Loose matching of space separators in the lenient date/time parsing mode
Reviewed-by: joehw, jlu
1 parent 2d252ee commit 96eb039

File tree

4 files changed

+177
-6
lines changed

4 files changed

+177
-6
lines changed

src/java.base/share/classes/java/text/DateFormat.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -746,6 +746,10 @@ public TimeZone getTimeZone()
746746
* <p>This leniency value is overwritten by a call to {@link
747747
* #setCalendar(java.util.Calendar) setCalendar()}.
748748
*
749+
* @implSpec A {@link Character#SPACE_SEPARATOR SPACE_SEPARATOR} in the input
750+
* text will match any other {@link Character#SPACE_SEPARATOR SPACE_SEPARATOR}s
751+
* in the pattern with lenient parsing; otherwise, it will not match.
752+
*
749753
* @param lenient when {@code true}, parsing is lenient
750754
* @see java.util.Calendar#setLenient(boolean)
751755
*/

src/java.base/share/classes/java/text/SimpleDateFormat.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,7 +1487,8 @@ public Date parse(String text, ParsePosition pos)
14871487

14881488
switch (tag) {
14891489
case TAG_QUOTE_ASCII_CHAR:
1490-
if (start >= textLength || text.charAt(start) != (char)count) {
1490+
if (start >= textLength ||
1491+
!charEquals(text.charAt(start), (char)count)) {
14911492
pos.index = oldStart;
14921493
pos.errorIndex = start;
14931494
return null;
@@ -1497,7 +1498,8 @@ public Date parse(String text, ParsePosition pos)
14971498

14981499
case TAG_QUOTE_CHARS:
14991500
while (count-- > 0) {
1500-
if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
1501+
if (start >= textLength ||
1502+
!charEquals(text.charAt(start), compiledPattern[i++])) {
15011503
pos.index = oldStart;
15021504
pos.errorIndex = start;
15031505
return null;
@@ -1580,6 +1582,13 @@ public Date parse(String text, ParsePosition pos)
15801582
return parsedDate;
15811583
}
15821584

1585+
private boolean charEquals(char ch1, char ch2) {
1586+
return ch1 == ch2 ||
1587+
isLenient() &&
1588+
Character.getType(ch1) == Character.SPACE_SEPARATOR &&
1589+
Character.getType(ch2) == Character.SPACE_SEPARATOR;
1590+
}
1591+
15831592
/* If the next tag/pattern is a <Numeric_Field> then the parser
15841593
* should consider the count of digits while parsing the contiguous digits
15851594
* for the current tag/pattern

src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -353,6 +353,10 @@ public DateTimeFormatterBuilder parseCaseInsensitive() {
353353
* The change will remain in force until the end of the formatter that is eventually
354354
* constructed or until {@code parseLenient} is called.
355355
*
356+
* @implSpec A {@link Character#SPACE_SEPARATOR SPACE_SEPARATOR} in the input
357+
* text will not match any other {@link Character#SPACE_SEPARATOR SPACE_SEPARATOR}s
358+
* in the pattern with the strict parse style.
359+
*
356360
* @return this, for chaining, not null
357361
*/
358362
public DateTimeFormatterBuilder parseStrict() {
@@ -372,6 +376,10 @@ public DateTimeFormatterBuilder parseStrict() {
372376
* The change will remain in force until the end of the formatter that is eventually
373377
* constructed or until {@code parseStrict} is called.
374378
*
379+
* @implSpec A {@link Character#SPACE_SEPARATOR SPACE_SEPARATOR} in the input
380+
* text will match any other {@link Character#SPACE_SEPARATOR SPACE_SEPARATOR}s
381+
* in the pattern with the lenient parse style.
382+
*
375383
* @return this, for chaining, not null
376384
*/
377385
public DateTimeFormatterBuilder parseLenient() {
@@ -2731,9 +2739,11 @@ public int parse(DateTimeParseContext context, CharSequence text, int position)
27312739
*/
27322740
static final class CharLiteralPrinterParser implements DateTimePrinterParser {
27332741
private final char literal;
2742+
private final boolean isSpaceSeparator;
27342743

27352744
private CharLiteralPrinterParser(char literal) {
27362745
this.literal = literal;
2746+
isSpaceSeparator = Character.getType(literal) == Character.SPACE_SEPARATOR;
27372747
}
27382748

27392749
@Override
@@ -2750,9 +2760,10 @@ public int parse(DateTimeParseContext context, CharSequence text, int position)
27502760
}
27512761
char ch = text.charAt(position);
27522762
if (ch != literal) {
2753-
if (context.isCaseSensitive() ||
2763+
if ((context.isCaseSensitive() ||
27542764
(Character.toUpperCase(ch) != Character.toUpperCase(literal) &&
2755-
Character.toLowerCase(ch) != Character.toLowerCase(literal))) {
2765+
Character.toLowerCase(ch) != Character.toLowerCase(literal))) &&
2766+
!spaceEquals(context, ch)) {
27562767
return ~position;
27572768
}
27582769
}
@@ -2766,6 +2777,12 @@ public String toString() {
27662777
}
27672778
return "'" + literal + "'";
27682779
}
2780+
2781+
private boolean spaceEquals(DateTimeParseContext context, char ch) {
2782+
return !context.isStrict() && isSpaceSeparator &&
2783+
Character.getType(ch) == Character.SPACE_SEPARATOR;
2784+
}
2785+
27692786
}
27702787

27712788
//-----------------------------------------------------------------------
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8324665
27+
* @summary Checks if SPACE_SEPARATOR are correctly parsed in lenient mode
28+
* @run junit LenientSpaceParsingTest
29+
*/
30+
import org.junit.jupiter.api.Test;
31+
import org.junit.jupiter.params.ParameterizedTest;
32+
import org.junit.jupiter.params.provider.Arguments;
33+
import org.junit.jupiter.params.provider.MethodSource;
34+
import static org.junit.jupiter.api.Assertions.assertThrows;
35+
36+
import java.text.ParseException;
37+
import java.text.SimpleDateFormat;
38+
import java.time.format.DateTimeFormatterBuilder;
39+
import java.time.format.DateTimeParseException;
40+
import java.util.stream.Stream;
41+
42+
public class LenientSpaceParsingTest {
43+
@MethodSource
44+
private static Stream<Arguments> strictSpaces() {
45+
// input, pattern
46+
return Stream.of(
47+
Arguments.of("00\u002000", "H\u0020m"),
48+
Arguments.of("00\u202f00", "H\u202fm"),
49+
Arguments.of("00\u00a000", "H\u00a0m"),
50+
Arguments.of("00\u0020\u202f\u0020\u00a000", "H\u0020\u202f\u0020\u00a0m")
51+
);
52+
}
53+
54+
@MethodSource
55+
private static Stream<Arguments> lenientSpaces() {
56+
// input, pattern
57+
return Stream.of(
58+
Arguments.of("00\u002000", "H\u202fm"),
59+
Arguments.of("00\u202f00", "H\u0020m"),
60+
Arguments.of("00\u00a000", "H\u0020m"),
61+
Arguments.of("00\u002000", "H\u00a0m"),
62+
Arguments.of("00\u0020\u202f\u0020\u00a000", "H\u0020\u0020\u0020\u0020m"),
63+
Arguments.of("00\u0020\u202f\u0020\u00a000", "H\u202f\u00a0\u202f\u00a0m")
64+
);
65+
}
66+
67+
@MethodSource
68+
private static Stream<Arguments> nonSpaces() {
69+
// input, pattern
70+
return Stream.of(
71+
Arguments.of("00a00", "H\u202fm"),
72+
Arguments.of("00a00", "H\u00a0m"),
73+
Arguments.of("00a00", "H\u0020m"),
74+
Arguments.of("00aa00", "H\u0020\u0020m"),
75+
Arguments.of("00aa00", "H\u00a0\u202fm")
76+
);
77+
}
78+
79+
@ParameterizedTest
80+
@MethodSource({"strictSpaces", "lenientSpaces"})
81+
public void checkDateTimeFormatter_Lenient(String input, String pattern) {
82+
new DateTimeFormatterBuilder().parseLenient().appendPattern(pattern).toFormatter().parse(input);
83+
}
84+
85+
@ParameterizedTest
86+
@MethodSource("nonSpaces")
87+
public void checkDateTimeFormatter_Lenient_Exception(String input, String pattern) {
88+
var dtf = new DateTimeFormatterBuilder().parseLenient().appendPattern(pattern).toFormatter();
89+
assertThrows(DateTimeParseException.class, () -> {
90+
dtf.parse(input);
91+
});
92+
}
93+
94+
@ParameterizedTest
95+
@MethodSource("strictSpaces")
96+
public void checkDateTimeFormatter_Strict(String input, String pattern) {
97+
new DateTimeFormatterBuilder().parseStrict().appendPattern(pattern).toFormatter().parse(input);
98+
}
99+
100+
@ParameterizedTest
101+
@MethodSource({"lenientSpaces", "nonSpaces"})
102+
public void checkDateTimeFormatter_Strict_Exception(String input, String pattern) {
103+
var dtf = new DateTimeFormatterBuilder().parseStrict().appendPattern(pattern).toFormatter();
104+
assertThrows(DateTimeParseException.class, () -> {
105+
dtf.parse(input);
106+
});
107+
}
108+
109+
@ParameterizedTest
110+
@MethodSource({"strictSpaces", "lenientSpaces"})
111+
public void checkSimpleDateFormat_Lenient(String input, String pattern) throws ParseException {
112+
new SimpleDateFormat(pattern).parse(input);
113+
}
114+
115+
@ParameterizedTest
116+
@MethodSource("nonSpaces")
117+
public void checkSimpleDateFormat_Lenient_Exception(String input, String pattern) {
118+
var sdf = new SimpleDateFormat(pattern);
119+
assertThrows(ParseException.class, () -> {
120+
sdf.parse(input);
121+
});
122+
}
123+
124+
@ParameterizedTest
125+
@MethodSource("strictSpaces")
126+
public void checkSimpleDateFormat_Strict(String input, String pattern) throws ParseException {
127+
var sdf = new SimpleDateFormat(pattern);
128+
sdf.setLenient(false);
129+
sdf.parse(input);
130+
}
131+
132+
@ParameterizedTest
133+
@MethodSource({"lenientSpaces", "nonSpaces"})
134+
public void checkSimpleDateFormat_Strict_Exception(String input, String pattern) {
135+
var sdf = new SimpleDateFormat(pattern);
136+
sdf.setLenient(false);
137+
assertThrows(ParseException.class, () -> {
138+
sdf.parse(input);
139+
});
140+
}
141+
}

0 commit comments

Comments
 (0)