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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
Executable
+16
@@ -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
|
||||
Executable
+6
@@ -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));
|
||||
}
|
||||
}
|
||||
+182
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user