datetimeformatter: DateTimeFormatter cheat sheet module (pattern letters incl. the four hour letters, yyyy vs YYYY week-year bug and WeekFields, en_IN locales and the U+202F AM/PM trap with parseLenient fix, SMART/STRICT/LENIENT parsing, ISO constants, SimpleDateFormat thread safety), 18 contract tests

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b3fvyFpxThdRxcozLx
This commit is contained in:
Claude
2026-11-14 01:24:52 +05:30
parent c8588ae520
commit 5faa57bb2d
22 changed files with 944 additions and 0 deletions
+1
View File
@@ -31,6 +31,7 @@ article; each module's own README has that article's version table, quickstart,
| [`string-format`](string-format/) | String.format, formatted() and printf in Java: The Complete Cheat Sheet | | [`string-format`](string-format/) | String.format, formatted() and printf in Java: The Complete Cheat Sheet |
| [`equals-hashcode`](equals-hashcode/) | equals() and hashCode() in Java: The Contract, Real Bugs and How Records Change It | | [`equals-hashcode`](equals-hashcode/) | equals() and hashCode() in Java: The Contract, Real Bugs and How Records Change It |
| [`copying`](copying/) | Deep Copy vs Shallow Copy in Java: clone(), Copy Constructors, Serialization and Records | | [`copying`](copying/) | Deep Copy vs Shallow Copy in Java: clone(), Copy Constructors, Serialization and Records |
| [`datetimeformatter`](datetimeformatter/) | DateTimeFormatter Cheat Sheet: Patterns, the yyyy vs YYYY Bug, Locales and ISO Formats |
## License ## License
+44
View File
@@ -0,0 +1,44 @@
# datetimeformatter
Companion code for the ankurm.com post *"DateTimeFormatter Cheat Sheet: Patterns, the yyyy vs YYYY Bug, Locales and ISO Formats."*
Module in `java-core-examples`, the Java-core series. All explanation lives in the post; this module holds the code and the captured output.
## Versions this was built and tested against
| Component | Version | Notes |
|---|---|---|
| JDK | 25.0.5+8 (Temurin, LTS) | `java.time` arrived in Java 8. Locale data is CLDR; the narrow no-break space before AM/PM comes from the CLDR 42 update that shipped in JDK 20. |
| JUnit Jupiter | 5.11.0 | |
| Maven | 3.9.11 | |
## Quickstart
```bash
export JAVA_HOME=/path/to/jdk-25
mvn compile
scripts/run.sh WeekYearBugDemo # one demo
scripts/run-all.sh # regenerate every file in output/
```
## What's in here
| File | What it shows | Output |
|---|---|---|
| `PatternLettersDemo.java` | About fifty pattern strings applied to one fixed moment: how repeat count changes `M`, `E`, `S`, `z`, `X`, `O`, and what `A`, `n`, `e`, `B`, `w`, `W` give. | `output/01` |
| `WeekYearBugDemo.java` | `yyyy` vs `YYYY` vs `uuuu`: the same date in four locales, and how many days in 2020-2030 each locale gets wrong. | `output/02` |
| `LocaleDemo.java` | en_IN, hi_IN, mr_IN; `Sept`; lowercase `pm`; the U+202F character before AM/PM the parse failure it causes, and `parseLenient()` as the fix. | `output/03` |
| `ParsingDemo.java` | `STRICT` / `SMART` / `LENIENT` on 31-02-2026, `yyyy` vs `uuuu` under STRICT, digit counts, two-digit years, case sensitivity, and `SimpleDateFormat` for comparison. | `output/04` |
| `IsoFormatsDemo.java` | Every predefined ISO constant on one value. | `output/05` |
| `ErrorsDemo.java` | The eleven exceptions people actually meet, with the real messages. | `output/06` |
| `ThreadSafetyDemo.java` | One shared `SimpleDateFormat` vs one shared `DateTimeFormatter`, 8 threads. Counts for `SimpleDateFormat` drift between runs. | `output/07` |
| `DateTimeFormatterTest.java` | Pins every claim above as an assertion, 18/18 passing. | `output/08` |
## Notes worth knowing before reading the post
- **`YYYY` is the week-based year.** On 2026-12-29 it prints `2027` in `en_US` and `en_IN` and `2026` in `en_GB` and `fr_FR` (`output/02`).
- **The default resolver style is `SMART`**, which parses `31-02-2026` as 28 February 2026 instead of failing (`output/04`).
- **`ofLocalizedTime(SHORT)` puts U+202F, not a space, before `PM`**, so a typed `3:30 PM` does not parse back (`output/03`).
## License
MIT - see the [repo-wide LICENSE](../LICENSE).
@@ -0,0 +1,61 @@
moment = 2026-11-14T01:17:05.123456789+05:30[Asia/Kolkata] (a Saturday, day 318 of the year)
yyyy -> 2026
yy -> 26
uuuu -> 2026
M -> 11
MM -> 11
MMM -> Nov
MMMM -> November
MMMMM -> N
d -> 14
dd -> 14
D -> 318
DDD -> 318
E -> Sat
EEEE -> Saturday
EEEEE -> S
e -> 7
ee -> 07
H -> 1
HH -> 01
h -> 1
hh -> 01
k -> 1
K -> 1
a -> AM
B -> in the morning
BBBB -> in the morning
m -> 17
mm -> 17
s -> 5
ss -> 05
S -> 1
SSS -> 123
SSSSSS -> 123456
n -> 123456789
A -> 4625123
G -> AD
Q -> 4
QQQ -> Q4
w -> 46
W -> 2
z -> IST
zzzz -> India Standard Time
VV -> Asia/Kolkata
O -> GMT+5:30
OOOO -> GMT+05:30
X -> +0530
XXX -> +05:30
x -> +0530
xxx -> +05:30
Z -> +0530
ZZZZ -> GMT+05:30
ZZZZZ -> +05:30
== the four hour letters, at midnight, 01:00, noon and 13:00 ==
time H h k K
00:17 0 12 24 0
01:17 1 1 1 1
12:17 12 12 12 0
13:17 13 1 13 1
@@ -0,0 +1,35 @@
== one date, two letters ==
date = 2026-12-29 (TUESDAY)
dd-MM-yyyy = 29-12-2026
dd-MM-YYYY = 29-12-2027
== the same YYYY pattern, four locales, three dates ==
date en_US en_GB fr_FR en_IN
2026-12-28 2027 2026 2026 2027
2026-12-29 2027 2026 2026 2027
2027-01-01 2027 2026 2026 2027
== why: each locale defines what a week is (WeekFields) ==
en_US WeekFields[SUNDAY,1] week-based-year of 2026-12-29 = 2027, week number = 1
en_GB WeekFields[MONDAY,4] week-based-year of 2026-12-29 = 2026, week number = 53
fr_FR WeekFields[MONDAY,4] week-based-year of 2026-12-29 = 2026, week number = 53
en_IN WeekFields[SUNDAY,1] week-based-year of 2026-12-29 = 2027, week number = 1
== how often is YYYY wrong? every day of 2020-2030, compared with yyyy ==
en_US 36 wrong days out of 4018
en_GB 19 wrong days out of 4018
fr_FR 19 wrong days out of 4018
en_IN 36 wrong days out of 4018
== the days that are wrong in the US locale ==
2024/2025: 2024-12-29->2025, 2024-12-30->2025, 2024-12-31->2025
2025/2026: 2025-12-28->2026, 2025-12-29->2026, 2025-12-30->2026, 2025-12-31->2026
2026/2027: 2026-12-27->2027, 2026-12-28->2027, 2026-12-29->2027, 2026-12-30->2027, 2026-12-31->2027
2027/2028: 2027-12-26->2028, 2027-12-27->2028, 2027-12-28->2028, 2027-12-29->2028, 2027-12-30->2028, 2027-12-31->2028
2028/2029: 2028-12-31->2029
== yyyy vs uuuu: same for modern dates, different for the era ==
date = -0043-03-15
yyyy = 0044
uuuu = -0043
yyyy G = 0044 BC
+34
View File
@@ -0,0 +1,34 @@
== the same pattern in four locales ==
en_US Tuesday, 1 September 2026
en_IN Tuesday, 1 September 2026
hi_IN मंगलवार, 1 सितंबर 2026
mr_IN मंगळवार, 1 सप्टेंबर 2026
== abbreviated month: not what every Indian or British reader expects ==
en_US 1 Sep 2026
en_GB 1 Sept 2026
en_IN 1 Sept 2026
== localized styles for en_IN (the pattern is chosen by the locale, not by you) ==
MEDIUM 1 Sept 2026
SHORT 01/09/26
FULL Tuesday, 1 September 2026
== AM/PM text per locale, with the hex of every character ==
en_US [03:30 PM] 30 33 3A 33 30 20 50 4D
en_IN [03:30 pm] 30 33 3A 33 30 20 70 6D
== ofLocalizedTime(SHORT): the space before PM is not a space ==
en_US 33 3A 33 30 202F 50 4D
en_IN 33 3A 33 30 202F 70 6D
== parsing it back with a plain space ==
DateTimeParseException: Text '3:30 PM' could not be parsed at index 4
15:30
== the other direction: text that CONTAINS U+202F, read by a pattern with a plain space ==
DateTimeParseException: Text '3:30_PM' could not be parsed at index 4
== parseLenient() accepts either kind of space, in both directions ==
localized SHORT, input with plain space : 15:30
pattern h:mm a, input with U+202F : 15:30
+30
View File
@@ -0,0 +1,30 @@
== 31 February under three resolver styles (pattern dd-MM-uuuu) ==
STRICT DateTimeParseException: Text '31-02-2026' could not be parsed: Invalid date 'FEBRUARY 31'
SMART 2026-02-28
LENIENT 2026-03-03
== 30 February and 32 January ==
SMART 30-02-2026 2026-02-28
SMART 32-01-2026 DateTimeParseException: Text '32-01-2026' could not be parsed: Invalid value for DayOfMonth (valid values 1 - 28/31): 32
== STRICT with yyyy: year-of-era needs an era, so it cannot resolve ==
yyyy DateTimeParseException: Text '14-11-2026' could not be parsed: Unable to obtain LocalDate from TemporalAccessor: {MonthOfYear=11, DayOfMonth=14, YearOfEra=2026},ISO of type java.time.format.Parsed
uuuu 2026-11-14
== how many digits? MM wants two, M accepts one or two ==
yyyy-MM-dd 2026-1-5 DateTimeParseException: Text '2026-1-5' could not be parsed at index 5
yyyy-M-d 2026-1-5 2026-01-05
yyyy-M-d 2026-01-05 2026-01-05
== two-digit years are read as 2000-2099 ==
01-01-30 -> 2030-01-01
01-01-99 -> 2099-01-01
== month names are case-sensitive unless you say otherwise ==
14 Nov 2026 2026-11-14
14 NOV 2026 DateTimeParseException: Text '14 NOV 2026' could not be parsed at index 3
14 NOV 2026 (parseCaseInsensitive) 2026-11-14
== the old SimpleDateFormat, same 31-02-2026 ==
lenient (default): Tue Mar 03 00:00:00 IST 2026
setLenient(false): ParseException: Unparseable date: "31-02-2026"
@@ -0,0 +1,19 @@
moment = 2026-12-29T01:17:05.123+05:30[Asia/Kolkata]
ISO_LOCAL_DATE 2026-12-29
ISO_LOCAL_TIME 01:17:05.123
ISO_LOCAL_DATE_TIME 2026-12-29T01:17:05.123
ISO_OFFSET_DATE_TIME 2026-12-29T01:17:05.123+05:30
ISO_ZONED_DATE_TIME 2026-12-29T01:17:05.123+05:30[Asia/Kolkata]
ISO_INSTANT 2026-12-28T19:47:05.123Z
ISO_DATE_TIME 2026-12-29T01:17:05.123+05:30[Asia/Kolkata]
ISO_ORDINAL_DATE 2026-363+05:30
ISO_WEEK_DATE 2026-W53-2+05:30
BASIC_ISO_DATE 20261229+0530
RFC_1123_DATE_TIME Tue, 29 Dec 2026 01:17:05 +0530
== ISO_INSTANT on a ZonedDateTime converts to UTC ==
2026-12-28T19:47:05.123Z
== toString() is already ISO for the java.time types ==
2026-12-29 | 01:17:05.123 | 2026-12-29T01:17:05.123+05:30
+11
View File
@@ -0,0 +1,11 @@
hour pattern on a LocalDate UnsupportedTemporalTypeException: Unsupported field: HourOfDay
zone pattern on a LocalDateTime DateTimeException: Unable to extract ZoneId from temporal 2026-11-14T01:17
unknown pattern letter 'b' IllegalArgumentException: Unknown pattern letter: b
unterminated quote IllegalArgumentException: Pattern ends with an incomplete string literal: uuuu-MM-dd'T
the T in an ISO pattern, unquoted IllegalArgumentException: Unknown pattern letter: T
the T in an ISO pattern, quoted OK: 2026-11-14T01:17
text too short DateTimeParseException: Text '2026-11' could not be parsed at index 7
text too long DateTimeParseException: Text '2026-11-14 extra' could not be parsed, unparsed text found at index 10
date-only text into a LocalDateTime DateTimeParseException: Text '2026-11-14' could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor: {},ISO resolved to 2026-11-14 of type java.time.format.Parsed
month name in the wrong language DateTimeParseException: Text '14 novembre 2026' could not be parsed at index 3
same text, French locale OK: 2026-11-14
@@ -0,0 +1,3 @@
threads=8, parses per thread=20000
SimpleDateFormat (shared) : exceptions=461, wrong results=3824
DateTimeFormatter (shared): exceptions=0, wrong results=0
+3
View File
@@ -0,0 +1,3 @@
[INFO] Tests run: 18, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.918 s -- in com.ankurm.datetimeformatter.DateTimeFormatterTest
[INFO] Tests run: 18, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ankurm</groupId>
<artifactId>java-core-examples</artifactId>
<version>1.0</version>
</parent>
<artifactId>datetimeformatter</artifactId>
<name>datetimeformatter</name>
<description>DateTimeFormatter cheat sheet: pattern letters, the yyyy vs YYYY week-year bug, locales (including en_IN and the narrow no-break space trap), parsing strictness, ISO constants, and thread safety against SimpleDateFormat.</description>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>25</release>
<compilerArgs><arg>-Xlint:-deprecation</arg></compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<argLine>-Dstdout.encoding=UTF-8 -Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Regenerates every file in output/. 07 (threads) drifts between runs: the counts are indicative.
set -euo pipefail
cd "$(dirname "$0")/.."
mvn -q -B compile
mkdir -p output
run() { java -Dstdout.encoding=UTF-8 -cp target/classes "com.ankurm.datetimeformatter.$1" 2>&1 | grep -v '^Picked up JAVA_TOOL_OPTIONS' > "output/$2.txt"; }
run PatternLettersDemo 01-pattern-letters
run WeekYearBugDemo 02-week-year-bug
run LocaleDemo 03-locales
run ParsingDemo 04-parsing
run IsoFormatsDemo 05-iso-formats
run ErrorsDemo 06-errors
run ThreadSafetyDemo 07-thread-safety
mvn -B test 2>&1 | grep -E 'Tests run|BUILD|ERROR|FAIL' | grep -v 'Picked up' > output/08-tests.txt
echo "regenerated:"; ls output
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# Run one demo: scripts/run.sh WeekYearBugDemo
set -euo pipefail
cd "$(dirname "$0")/.."
mvn -q -B compile
java -Dstdout.encoding=UTF-8 -cp target/classes "com.ankurm.datetimeformatter.${1:?demo class name}"
@@ -0,0 +1,36 @@
package com.ankurm.datetimeformatter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.function.Supplier;
/** The exceptions people actually meet, each with the real message. */
public class ErrorsDemo {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 11, 14);
LocalDateTime dateTime = LocalDateTime.of(2026, 11, 14, 1, 17);
run("hour pattern on a LocalDate", () -> DateTimeFormatter.ofPattern("dd-MM-uuuu HH:mm").format(date));
run("zone pattern on a LocalDateTime", () -> DateTimeFormatter.ofPattern("HH:mm z").format(dateTime));
run("unknown pattern letter 'b'", () -> DateTimeFormatter.ofPattern("uuuu-MM-dd b"));
run("unterminated quote", () -> DateTimeFormatter.ofPattern("uuuu-MM-dd'T"));
run("the T in an ISO pattern, unquoted", () -> DateTimeFormatter.ofPattern("uuuu-MM-ddTHH:mm"));
run("the T in an ISO pattern, quoted", () -> DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm").format(dateTime));
run("text too short", () -> LocalDate.parse("2026-11", DateTimeFormatter.ofPattern("uuuu-MM-dd")));
run("text too long", () -> LocalDate.parse("2026-11-14 extra", DateTimeFormatter.ofPattern("uuuu-MM-dd")));
run("date-only text into a LocalDateTime", () -> LocalDateTime.parse("2026-11-14", DateTimeFormatter.ofPattern("uuuu-MM-dd")));
run("month name in the wrong language", () -> LocalDate.parse("14 novembre 2026", DateTimeFormatter.ofPattern("d MMMM uuuu", Locale.US)));
run("same text, French locale", () -> LocalDate.parse("14 novembre 2026", DateTimeFormatter.ofPattern("d MMMM uuuu", Locale.FRANCE)));
}
static void run(String label, Supplier<Object> action) {
try {
System.out.printf("%-38s OK: %s%n", label, action.get());
} catch (RuntimeException e) {
System.out.printf("%-38s %s: %s%n", label, e.getClass().getSimpleName(), e.getMessage());
}
}
}
@@ -0,0 +1,37 @@
package com.ankurm.datetimeformatter;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
/** The predefined constants. Reach for these before writing an ofPattern string for a machine. */
public class IsoFormatsDemo {
public static void main(String[] args) {
ZonedDateTime z = ZonedDateTime.of(2026, 12, 29, 1, 17, 5, 123_000_000, ZoneId.of("Asia/Kolkata"));
System.out.println("moment = " + z);
System.out.println();
show("ISO_LOCAL_DATE", DateTimeFormatter.ISO_LOCAL_DATE, z);
show("ISO_LOCAL_TIME", DateTimeFormatter.ISO_LOCAL_TIME, z);
show("ISO_LOCAL_DATE_TIME", DateTimeFormatter.ISO_LOCAL_DATE_TIME, z);
show("ISO_OFFSET_DATE_TIME", DateTimeFormatter.ISO_OFFSET_DATE_TIME, z);
show("ISO_ZONED_DATE_TIME", DateTimeFormatter.ISO_ZONED_DATE_TIME, z);
show("ISO_INSTANT", DateTimeFormatter.ISO_INSTANT, z);
show("ISO_DATE_TIME", DateTimeFormatter.ISO_DATE_TIME, z);
show("ISO_ORDINAL_DATE", DateTimeFormatter.ISO_ORDINAL_DATE, z);
show("ISO_WEEK_DATE", DateTimeFormatter.ISO_WEEK_DATE, z);
show("BASIC_ISO_DATE", DateTimeFormatter.BASIC_ISO_DATE, z);
show("RFC_1123_DATE_TIME", DateTimeFormatter.RFC_1123_DATE_TIME, z);
System.out.println();
System.out.println("== ISO_INSTANT on a ZonedDateTime converts to UTC ==");
System.out.println(DateTimeFormatter.ISO_INSTANT.format(z));
System.out.println();
System.out.println("== toString() is already ISO for the java.time types ==");
System.out.println(z.toLocalDate() + " | " + z.toLocalTime() + " | " + z.toOffsetDateTime());
}
static void show(String name, DateTimeFormatter f, ZonedDateTime z) {
System.out.printf("%-22s %s%n", name, f.format(z));
}
}
@@ -0,0 +1,96 @@
package com.ankurm.datetimeformatter;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.FormatStyle;
import java.util.Locale;
/**
* What locale changes: month and day names, localized styles, AM/PM text, and the character between
* the time and the AM/PM marker, which has been U+202F (narrow no-break space) rather than a plain
* space since the CLDR 42 update that arrived in JDK 20.
*/
public class LocaleDemo {
static final Locale EN_IN = Locale.of("en", "IN");
static final Locale HI_IN = Locale.of("hi", "IN");
static final Locale MR_IN = Locale.of("mr", "IN");
public static void main(String[] args) {
LocalDate d = LocalDate.of(2026, 9, 1);
LocalTime t = LocalTime.of(15, 30);
System.out.println("== the same pattern in four locales ==");
DateTimeFormatter names = DateTimeFormatter.ofPattern("EEEE, d MMMM uuuu");
for (Locale l : new Locale[]{Locale.US, EN_IN, HI_IN, MR_IN}) {
System.out.printf("%-6s %s%n", l, names.withLocale(l).format(d));
}
System.out.println();
System.out.println("== abbreviated month: not what every Indian or British reader expects ==");
DateTimeFormatter abbr = DateTimeFormatter.ofPattern("d MMM uuuu");
for (Locale l : new Locale[]{Locale.US, Locale.UK, EN_IN}) {
System.out.printf("%-6s %s%n", l, abbr.withLocale(l).format(d));
}
System.out.println();
System.out.println("== localized styles for en_IN (the pattern is chosen by the locale, not by you) ==");
for (FormatStyle s : FormatStyle.values()) {
if (s == FormatStyle.FULL || s == FormatStyle.LONG) continue;
System.out.printf("%-7s %s%n", s, DateTimeFormatter.ofLocalizedDate(s).withLocale(EN_IN).format(d));
}
System.out.printf("FULL %s%n", DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(EN_IN).format(d));
System.out.println();
System.out.println("== AM/PM text per locale, with the hex of every character ==");
String[] pats = {"hh:mm a"};
for (Locale l : new Locale[]{Locale.US, EN_IN}) {
String s = DateTimeFormatter.ofPattern(pats[0]).withLocale(l).format(t);
System.out.printf("%-6s [%s] %s%n", l, s, hex(s));
}
System.out.println();
System.out.println("== ofLocalizedTime(SHORT): the space before PM is not a space ==");
for (Locale l : new Locale[]{Locale.US, EN_IN}) {
String s = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(l).format(t);
System.out.printf("%-6s %s%n", l, hex(s));
}
System.out.println();
System.out.println("== parsing it back with a plain space ==");
String typed = "3:30 PM";
try {
System.out.println(LocalTime.parse(typed, DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(Locale.US)));
} catch (RuntimeException e) {
System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
}
System.out.println(LocalTime.parse(typed, DateTimeFormatter.ofPattern("h:mm a", Locale.US)));
System.out.println();
System.out.println("== the other direction: text that CONTAINS U+202F, read by a pattern with a plain space ==");
String withNnbsp = "3:30\u202FPM";
DateTimeFormatter plainPattern = DateTimeFormatter.ofPattern("h:mm a", Locale.US);
try {
System.out.println(LocalTime.parse(withNnbsp, plainPattern));
} catch (RuntimeException e) {
System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage().replace('\u202F', '_'));
}
System.out.println();
System.out.println("== parseLenient() accepts either kind of space, in both directions ==");
DateTimeFormatter lenientShort = new DateTimeFormatterBuilder()
.parseLenient().appendLocalized(null, FormatStyle.SHORT).toFormatter(Locale.US);
DateTimeFormatter lenientPattern = new DateTimeFormatterBuilder()
.parseLenient().appendPattern("h:mm a").toFormatter(Locale.US);
System.out.println("localized SHORT, input with plain space : " + LocalTime.parse(typed, lenientShort));
System.out.println("pattern h:mm a, input with U+202F : " + LocalTime.parse(withNnbsp, lenientPattern));
}
static String hex(String s) {
StringBuilder sb = new StringBuilder();
s.chars().forEach(c -> sb.append(String.format("%02X ", c)));
return sb.toString().trim();
}
}
@@ -0,0 +1,76 @@
package com.ankurm.datetimeformatter;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Locale;
/**
* Parsing is where DateTimeFormatter is least forgiving AND where its defaults are most generous.
* The default resolver style, SMART, accepts 31-02-2026 and quietly returns 28 February.
*/
public class ParsingDemo {
public static void main(String[] args) {
System.out.println("== 31 February under three resolver styles (pattern dd-MM-uuuu) ==");
for (ResolverStyle style : ResolverStyle.values()) {
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd-MM-uuuu").withResolverStyle(style);
System.out.printf("%-8s %s%n", style, attempt("31-02-2026", f));
}
System.out.println();
System.out.println("== 30 February and 32 January ==");
DateTimeFormatter smart = DateTimeFormatter.ofPattern("dd-MM-uuuu");
System.out.printf("SMART 30-02-2026 %s%n", attempt("30-02-2026", smart));
System.out.printf("SMART 32-01-2026 %s%n", attempt("32-01-2026", smart));
System.out.println();
System.out.println("== STRICT with yyyy: year-of-era needs an era, so it cannot resolve ==");
DateTimeFormatter strictY = DateTimeFormatter.ofPattern("dd-MM-yyyy").withResolverStyle(ResolverStyle.STRICT);
DateTimeFormatter strictU = DateTimeFormatter.ofPattern("dd-MM-uuuu").withResolverStyle(ResolverStyle.STRICT);
System.out.printf("yyyy %s%n", attempt("14-11-2026", strictY));
System.out.printf("uuuu %s%n", attempt("14-11-2026", strictU));
System.out.println();
System.out.println("== how many digits? MM wants two, M accepts one or two ==");
System.out.printf("yyyy-MM-dd 2026-1-5 %s%n", attempt("2026-1-5", DateTimeFormatter.ofPattern("yyyy-MM-dd")));
System.out.printf("yyyy-M-d 2026-1-5 %s%n", attempt("2026-1-5", DateTimeFormatter.ofPattern("yyyy-M-d")));
System.out.printf("yyyy-M-d 2026-01-05 %s%n", attempt("2026-01-05", DateTimeFormatter.ofPattern("yyyy-M-d")));
System.out.println();
System.out.println("== two-digit years are read as 2000-2099 ==");
DateTimeFormatter yy = DateTimeFormatter.ofPattern("dd-MM-yy");
System.out.printf("01-01-30 -> %s%n", attempt("01-01-30", yy));
System.out.printf("01-01-99 -> %s%n", attempt("01-01-99", yy));
System.out.println();
System.out.println("== month names are case-sensitive unless you say otherwise ==");
DateTimeFormatter mmm = DateTimeFormatter.ofPattern("dd MMM uuuu", Locale.US);
System.out.printf("14 Nov 2026 %s%n", attempt("14 Nov 2026", mmm));
System.out.printf("14 NOV 2026 %s%n", attempt("14 NOV 2026", mmm));
DateTimeFormatter insensitive = new java.time.format.DateTimeFormatterBuilder()
.parseCaseInsensitive().appendPattern("dd MMM uuuu").toFormatter(Locale.US);
System.out.printf("14 NOV 2026 (parseCaseInsensitive) %s%n", attempt("14 NOV 2026", insensitive));
System.out.println();
System.out.println("== the old SimpleDateFormat, same 31-02-2026 ==");
try {
System.out.println("lenient (default): " + new SimpleDateFormat("dd-MM-yyyy").parse("31-02-2026"));
SimpleDateFormat strict = new SimpleDateFormat("dd-MM-yyyy");
strict.setLenient(false);
System.out.println("setLenient(false): " + strict.parse("31-02-2026"));
} catch (java.text.ParseException e) {
System.out.println("setLenient(false): ParseException: " + e.getMessage());
}
}
static String attempt(String text, DateTimeFormatter f) {
try {
return LocalDate.parse(text, f).toString();
} catch (DateTimeParseException e) {
return e.getClass().getSimpleName() + ": " + e.getMessage();
}
}
}
@@ -0,0 +1,49 @@
package com.ankurm.datetimeformatter;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
/**
* Every pattern letter you are likely to need, applied to ONE fixed moment so the table in the post
* is a real run, not a remembered one. Repeat count changes the output: M, MM, MMM, MMMM, MMMMM.
*/
public class PatternLettersDemo {
static final ZonedDateTime MOMENT =
ZonedDateTime.of(2026, 11, 14, 1, 17, 5, 123_456_789, ZoneId.of("Asia/Kolkata"));
public static void main(String[] args) {
System.out.println("moment = " + MOMENT + " (a Saturday, day 318 of the year)");
System.out.println();
String[] patterns = {
"yyyy", "yy", "uuuu",
"M", "MM", "MMM", "MMMM", "MMMMM",
"d", "dd", "D", "DDD",
"E", "EEEE", "EEEEE", "e", "ee",
"H", "HH", "h", "hh", "k", "K",
"a", "B", "BBBB",
"m", "mm", "s", "ss",
"S", "SSS", "SSSSSS", "n", "A",
"G", "Q", "QQQ", "w", "W",
"z", "zzzz", "VV", "O", "OOOO", "X", "XXX", "x", "xxx", "Z", "ZZZZ", "ZZZZZ"
};
for (String p : patterns) {
System.out.printf("%-7s -> %s%n", p, MOMENT.format(DateTimeFormatter.ofPattern(p, Locale.US)));
}
System.out.println();
System.out.println("== the four hour letters, at midnight, 01:00, noon and 13:00 ==");
System.out.printf("%-6s %-3s %-3s %-3s %-3s%n", "time", "H", "h", "k", "K");
int[] hours = {0, 1, 12, 13};
for (int hour : hours) {
ZonedDateTime t = MOMENT.withHour(hour);
System.out.printf("%-6s %-3s %-3s %-3s %-3s%n", String.format("%02d:17", hour),
t.format(DateTimeFormatter.ofPattern("H", Locale.US)),
t.format(DateTimeFormatter.ofPattern("h", Locale.US)),
t.format(DateTimeFormatter.ofPattern("k", Locale.US)),
t.format(DateTimeFormatter.ofPattern("K", Locale.US)));
}
}
}
@@ -0,0 +1,74 @@
package com.ankurm.datetimeformatter;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* One shared SimpleDateFormat against one shared DateTimeFormatter, 8 threads, 20,000 parses each.
* SimpleDateFormat keeps its working state in the instance, so concurrent callers overwrite each
* other's work. The counts below drift between runs; DateTimeFormatter's count does not.
*/
public class ThreadSafetyDemo {
static final int THREADS = 8;
static final int ITERATIONS = 20_000;
public static void main(String[] args) throws Exception {
System.out.println("threads=" + THREADS + ", parses per thread=" + ITERATIONS);
Result old = runSimpleDateFormat();
System.out.println("SimpleDateFormat (shared) : exceptions=" + old.exceptions + ", wrong results=" + old.wrong);
Result modern = runDateTimeFormatter();
System.out.println("DateTimeFormatter (shared): exceptions=" + modern.exceptions + ", wrong results=" + modern.wrong);
}
record Result(int exceptions, int wrong) {}
static Result runSimpleDateFormat() throws Exception {
SimpleDateFormat shared = new SimpleDateFormat("dd-MM-yyyy");
return hammer(i -> {
int day = 1 + (i % 28);
String text = String.format("%02d-11-2026", day);
Date parsed = shared.parse(text);
return new java.text.SimpleDateFormat("dd").format(parsed).equals(String.format("%02d", day));
});
}
static Result runDateTimeFormatter() throws Exception {
DateTimeFormatter shared = DateTimeFormatter.ofPattern("dd-MM-uuuu");
return hammer(i -> {
int day = 1 + (i % 28);
String text = String.format("%02d-11-2026", day);
LocalDate parsed = LocalDate.parse(text, shared);
return parsed.getDayOfMonth() == day;
});
}
interface Check { boolean ok(int i) throws Exception; }
static Result hammer(Check check) throws Exception {
AtomicInteger exceptions = new AtomicInteger();
AtomicInteger wrong = new AtomicInteger();
CountDownLatch start = new CountDownLatch(1);
Thread[] threads = new Thread[THREADS];
for (int t = 0; t < THREADS; t++) {
threads[t] = new Thread(() -> {
try { start.await(); } catch (InterruptedException e) { return; }
for (int i = 0; i < ITERATIONS; i++) {
try {
if (!check.ok(i)) wrong.incrementAndGet();
} catch (Exception e) {
exceptions.incrementAndGet();
}
}
});
threads[t].start();
}
start.countDown();
for (Thread t : threads) t.join();
return new Result(exceptions.get(), wrong.get());
}
}
@@ -0,0 +1,83 @@
package com.ankurm.datetimeformatter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.WeekFields;
import java.util.Locale;
/**
* yyyy is the year; YYYY is the WEEK-based year. They agree on 360-ish days a year and disagree on
* the few days around New Year, which is exactly why the bug survives testing and ships.
*/
public class WeekYearBugDemo {
static final Locale EN_IN = Locale.of("en", "IN");
static final LocalDate DEC_29 = LocalDate.of(2026, 12, 29);
public static void main(String[] args) {
System.out.println("== one date, two letters ==");
LocalDate d = LocalDate.of(2026, 12, 29);
System.out.println("date = " + d + " (" + d.getDayOfWeek() + ")");
System.out.println("dd-MM-yyyy = " + DateTimeFormatter.ofPattern("dd-MM-yyyy").format(d));
System.out.println("dd-MM-YYYY = " + DateTimeFormatter.ofPattern("dd-MM-YYYY").format(d));
System.out.println();
System.out.println("== the same YYYY pattern, four locales, three dates ==");
LocalDate[] dates = {LocalDate.of(2026, 12, 28), LocalDate.of(2026, 12, 29), LocalDate.of(2027, 1, 1)};
Locale[] locales = {Locale.US, Locale.UK, Locale.FRANCE, EN_IN};
System.out.printf("%-12s", "date");
for (Locale l : locales) System.out.printf("%-10s", l);
System.out.println();
for (LocalDate x : dates) {
System.out.printf("%-12s", x);
for (Locale l : locales) {
System.out.printf("%-10s", DateTimeFormatter.ofPattern("YYYY", l).format(x));
}
System.out.println();
}
System.out.println();
System.out.println("== why: each locale defines what a week is (WeekFields) ==");
for (Locale l : locales) {
WeekFields wf = WeekFields.of(l);
System.out.printf("%-6s %-22s week-based-year of 2026-12-29 = %d, week number = %d%n", l, wf,
DEC_29.get(wf.weekBasedYear()), DEC_29.get(wf.weekOfWeekBasedYear()));
}
System.out.println();
System.out.println("== how often is YYYY wrong? every day of 2020-2030, compared with yyyy ==");
for (Locale l : locales) {
int wrong = 0;
int total = 0;
DateTimeFormatter plain = DateTimeFormatter.ofPattern("yyyy", l);
DateTimeFormatter week = DateTimeFormatter.ofPattern("YYYY", l);
for (LocalDate x = LocalDate.of(2020, 1, 1); x.isBefore(LocalDate.of(2031, 1, 1)); x = x.plusDays(1)) {
total++;
if (!plain.format(x).equals(week.format(x))) wrong++;
}
System.out.printf("%-6s %3d wrong days out of %d%n", l, wrong, total);
}
System.out.println();
System.out.println("== the days that are wrong in the US locale ==");
DateTimeFormatter plain = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.US);
DateTimeFormatter week = DateTimeFormatter.ofPattern("YYYY-MM-dd", Locale.US);
for (int year = 2024; year <= 2028; year++) {
StringBuilder sb = new StringBuilder();
for (LocalDate x = LocalDate.of(year, 12, 20); !x.isAfter(LocalDate.of(year + 1, 1, 10)); x = x.plusDays(1)) {
if (!plain.format(x).equals(week.format(x))) {
sb.append(sb.isEmpty() ? "" : ", ").append(plain.format(x)).append("->").append(week.format(x), 0, 4);
}
}
System.out.println(year + "/" + (year + 1) + ": " + (sb.isEmpty() ? "none" : sb));
}
System.out.println();
System.out.println("== yyyy vs uuuu: same for modern dates, different for the era ==");
LocalDate bc = LocalDate.of(-43, 3, 15);
System.out.println("date = " + bc);
System.out.println("yyyy = " + DateTimeFormatter.ofPattern("yyyy", Locale.US).format(bc));
System.out.println("uuuu = " + DateTimeFormatter.ofPattern("uuuu", Locale.US).format(bc));
System.out.println("yyyy G = " + DateTimeFormatter.ofPattern("yyyy G", Locale.US).format(bc));
}
}
@@ -0,0 +1,182 @@
package com.ankurm.datetimeformatter;
import static org.junit.jupiter.api.Assertions.*;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.FormatStyle;
import java.time.format.ResolverStyle;
import java.time.temporal.UnsupportedTemporalTypeException;
import java.util.Locale;
import org.junit.jupiter.api.Test;
/** Pins every claim the post makes. If a JDK update changes one, the build fails instead of the post lying. */
class DateTimeFormatterTest {
static final Locale EN_IN = Locale.of("en", "IN");
static final LocalDate DEC_29_2026 = LocalDate.of(2026, 12, 29);
@Test
void weekYearDiffersFromYearAtNewYear() {
assertEquals("29-12-2026", DateTimeFormatter.ofPattern("dd-MM-yyyy").format(DEC_29_2026));
assertEquals("29-12-2027", DateTimeFormatter.ofPattern("dd-MM-YYYY", Locale.US).format(DEC_29_2026));
}
@Test
void weekYearDependsOnLocale() {
assertEquals("2027", DateTimeFormatter.ofPattern("YYYY", Locale.US).format(DEC_29_2026));
assertEquals("2027", DateTimeFormatter.ofPattern("YYYY", EN_IN).format(DEC_29_2026));
assertEquals("2026", DateTimeFormatter.ofPattern("YYYY", Locale.UK).format(DEC_29_2026));
assertEquals("2026", DateTimeFormatter.ofPattern("YYYY", Locale.FRANCE).format(DEC_29_2026));
}
@Test
void weekDefinitionExplainsTheLocaleDifference() {
assertEquals("WeekFields[SUNDAY,1]", java.time.temporal.WeekFields.of(Locale.US).toString());
assertEquals("WeekFields[SUNDAY,1]", java.time.temporal.WeekFields.of(EN_IN).toString());
assertEquals("WeekFields[MONDAY,4]", java.time.temporal.WeekFields.of(Locale.UK).toString());
assertEquals("WeekFields[MONDAY,4]", java.time.temporal.WeekFields.of(Locale.FRANCE).toString());
}
@Test
void thirtySixWrongDaysInTenYearsForUsAndIndia() {
assertEquals(36, wrongDays(Locale.US));
assertEquals(36, wrongDays(EN_IN));
assertEquals(19, wrongDays(Locale.UK));
}
private static int wrongDays(Locale l) {
DateTimeFormatter plain = DateTimeFormatter.ofPattern("yyyy", l);
DateTimeFormatter week = DateTimeFormatter.ofPattern("YYYY", l);
int wrong = 0;
for (LocalDate x = LocalDate.of(2020, 1, 1); x.isBefore(LocalDate.of(2031, 1, 1)); x = x.plusDays(1)) {
if (!plain.format(x).equals(week.format(x))) wrong++;
}
return wrong;
}
@Test
void yyyyIsYearOfEraSoBcDatesPrintPositive() {
LocalDate bc = LocalDate.of(-43, 3, 15);
assertEquals("0044", DateTimeFormatter.ofPattern("yyyy", Locale.US).format(bc));
assertEquals("-0043", DateTimeFormatter.ofPattern("uuuu", Locale.US).format(bc));
}
@Test
void repeatCountChangesMonthText() {
ZonedDateTime z = PatternLettersDemo.MOMENT;
assertEquals("11", z.format(DateTimeFormatter.ofPattern("M", Locale.US)));
assertEquals("Nov", z.format(DateTimeFormatter.ofPattern("MMM", Locale.US)));
assertEquals("November", z.format(DateTimeFormatter.ofPattern("MMMM", Locale.US)));
assertEquals("N", z.format(DateTimeFormatter.ofPattern("MMMMM", Locale.US)));
}
@Test
void hourLettersDisagreeAtMidnightAndNoon() {
ZonedDateTime midnight = PatternLettersDemo.MOMENT.withHour(0);
ZonedDateTime noon = PatternLettersDemo.MOMENT.withHour(12);
assertEquals("0", midnight.format(DateTimeFormatter.ofPattern("H", Locale.US)));
assertEquals("12", midnight.format(DateTimeFormatter.ofPattern("h", Locale.US)));
assertEquals("24", midnight.format(DateTimeFormatter.ofPattern("k", Locale.US)));
assertEquals("0", midnight.format(DateTimeFormatter.ofPattern("K", Locale.US)));
assertEquals("12", noon.format(DateTimeFormatter.ofPattern("H", Locale.US)));
assertEquals("0", noon.format(DateTimeFormatter.ofPattern("K", Locale.US)));
}
@Test
void fractionAndMilliOfDayLetters() {
ZonedDateTime z = PatternLettersDemo.MOMENT;
assertEquals("123", z.format(DateTimeFormatter.ofPattern("SSS", Locale.US)));
assertEquals("123456789", z.format(DateTimeFormatter.ofPattern("n", Locale.US)));
assertEquals("4625123", z.format(DateTimeFormatter.ofPattern("A", Locale.US)));
}
@Test
void localizedTimeUsesNarrowNoBreakSpaceBeforeAmPm() {
LocalTime t = LocalTime.of(15, 30);
String us = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(Locale.US).format(t);
assertEquals("3:30 PM", us);
assertThrows(DateTimeParseException.class,
() -> LocalTime.parse("3:30 PM", DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(Locale.US)));
}
@Test
void parseLenientAcceptsEitherSpaceBeforeAmPm() {
DateTimeFormatter strictPattern = DateTimeFormatter.ofPattern("h:mm a", Locale.US);
assertThrows(DateTimeParseException.class, () -> LocalTime.parse("3:30\u202FPM", strictPattern));
DateTimeFormatter lenientShort = new java.time.format.DateTimeFormatterBuilder()
.parseLenient().appendLocalized(null, FormatStyle.SHORT).toFormatter(Locale.US);
DateTimeFormatter lenientPattern = new java.time.format.DateTimeFormatterBuilder()
.parseLenient().appendPattern("h:mm a").toFormatter(Locale.US);
assertEquals(LocalTime.of(15, 30), LocalTime.parse("3:30 PM", lenientShort));
assertEquals(LocalTime.of(15, 30), LocalTime.parse("3:30\u202FPM", lenientPattern));
}
@Test
void indianEnglishUsesLowercaseAmPmAndSept() {
assertEquals("03:30 pm", DateTimeFormatter.ofPattern("hh:mm a", EN_IN).format(LocalTime.of(15, 30)));
assertEquals("1 Sept 2026", DateTimeFormatter.ofPattern("d MMM uuuu", EN_IN).format(LocalDate.of(2026, 9, 1)));
assertEquals("1 Sep 2026", DateTimeFormatter.ofPattern("d MMM uuuu", Locale.US).format(LocalDate.of(2026, 9, 1)));
}
@Test
void smartAcceptsThirtyFirstOfFebruaryAndStrictRejectsIt() {
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd-MM-uuuu");
assertEquals(LocalDate.of(2026, 2, 28), LocalDate.parse("31-02-2026", f));
assertEquals(LocalDate.of(2026, 3, 3), LocalDate.parse("31-02-2026", f.withResolverStyle(ResolverStyle.LENIENT)));
assertThrows(DateTimeParseException.class,
() -> LocalDate.parse("31-02-2026", f.withResolverStyle(ResolverStyle.STRICT)));
}
@Test
void strictWithYyyyCannotResolveButUuuuCan() {
DateTimeFormatter y = DateTimeFormatter.ofPattern("dd-MM-yyyy").withResolverStyle(ResolverStyle.STRICT);
DateTimeFormatter u = DateTimeFormatter.ofPattern("dd-MM-uuuu").withResolverStyle(ResolverStyle.STRICT);
assertThrows(DateTimeParseException.class, () -> LocalDate.parse("14-11-2026", y));
assertEquals(LocalDate.of(2026, 11, 14), LocalDate.parse("14-11-2026", u));
}
@Test
void digitCountAndTwoDigitYear() {
assertThrows(DateTimeParseException.class,
() -> LocalDate.parse("2026-1-5", DateTimeFormatter.ofPattern("yyyy-MM-dd")));
assertEquals(LocalDate.of(2026, 1, 5), LocalDate.parse("2026-1-5", DateTimeFormatter.ofPattern("yyyy-M-d")));
assertEquals(LocalDate.of(2099, 1, 1), LocalDate.parse("01-01-99", DateTimeFormatter.ofPattern("dd-MM-yy")));
}
@Test
void errorsHaveTheDocumentedTypes() {
assertThrows(UnsupportedTemporalTypeException.class,
() -> DateTimeFormatter.ofPattern("HH:mm").format(DEC_29_2026));
assertThrows(IllegalArgumentException.class, () -> DateTimeFormatter.ofPattern("uuuu-MM-ddTHH:mm"));
assertThrows(IllegalArgumentException.class, () -> DateTimeFormatter.ofPattern("uuuu-MM-dd'T"));
}
@Test
void isoConstantsAndInstantConversion() {
ZonedDateTime z = ZonedDateTime.of(2026, 12, 29, 1, 17, 5, 123_000_000, ZoneId.of("Asia/Kolkata"));
assertEquals("2026-12-29T01:17:05.123+05:30", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(z));
assertEquals("2026-12-28T19:47:05.123Z", DateTimeFormatter.ISO_INSTANT.format(z));
assertEquals("2026-W53-2+05:30", DateTimeFormatter.ISO_WEEK_DATE.format(z));
}
@Test
void simpleDateFormatIsLenientByDefault() throws Exception {
assertNotNull(new SimpleDateFormat("dd-MM-yyyy").parse("31-02-2026"));
SimpleDateFormat strict = new SimpleDateFormat("dd-MM-yyyy");
strict.setLenient(false);
assertThrows(java.text.ParseException.class, () -> strict.parse("31-02-2026"));
}
@Test
void sharedDateTimeFormatterIsThreadSafe() throws Exception {
ThreadSafetyDemo.Result r = ThreadSafetyDemo.runDateTimeFormatter();
assertEquals(0, r.exceptions());
assertEquals(0, r.wrong());
}
}
+1
View File
@@ -39,6 +39,7 @@
<module>string-format</module> <module>string-format</module>
<module>equals-hashcode</module> <module>equals-hashcode</module>
<module>copying</module> <module>copying</module>
<module>datetimeformatter</module>
</modules> </modules>
<properties> <properties>