JAVA / DATES, NUMBERS, REGEX AND CORE LIBRARIES
Formatting and parsing dates without SimpleDateFormat
Format and parse dates with java.time's DateTimeFormatter: pattern letters, locales, resolver styles, and parsing into the type that owns the fields.
What you will learn
- Build a DateTimeFormatter once and share it across threads; it is immutable
- Parse with LocalDate.parse(text, formatter) so the target type checks the fields
- Pair uuuu with ResolverStyle.STRICT to reject impossible dates like 30/02/2026
- Supply absent fields with parseDefaulting instead of editing the input string
Understanding Formatting and parsing dates without SimpleDateFormat
A DateTimeFormatter holds no state about the value being handled: it is a description of a text layout, built once and then used as a read-only recipe. That is why a single instance can live in a static final field and be used from many threads at once, whereas SimpleDateFormat kept a Calendar inside itself and mutated it during every call, so a shared instance produced garbled output or exceptions under concurrency. The other half of the design is that the formatter does not decide what you get back: you call LocalDate.parse(text, formatter) or LocalTime.parse(text, formatter), and the type you name is what verifies that the text actually supplied the fields it needs.
The pattern letters resemble the old ones but the alphabet is different, and those differences cause most of the bugs. u is the proleptic year while y is year-of-era and is incomplete without an era; Y is the week-based year, which can be one off from the calendar year around New Year; M is month and m is minute; H is the 0-23 hour while h is the 1-12 clock hour that is meaningless without a; D is day-of-year, not day-of-month. Every one of these compiles, so a wrong letter surfaces as wrong text or a runtime exception, sometimes only for a handful of dates each year.
Parsing happens in two phases. First the parser walks the text and records raw field values such as DAY_OF_MONTH=31 and MONTH_OF_YEAR=4 without judging them, then the chronology resolves those fields into a date under a ResolverStyle. The default is SMART, which clamps an out-of-range day-of-month to the end of that month, so 31/04 quietly becomes 30 April; STRICT rejects it and LENIENT rolls it forward into May. Anything the text does not supply stays missing, because java.time will not invent a midnight for you, which is why a date-only string cannot become a LocalDateTime unless you parse it as a LocalDate or default the time fields explicitly.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
public class DateText {
private static final DateTimeFormatter UK_DATE =
DateTimeFormatter.ofPattern("dd/MM/uuuu").withResolverStyle(ResolverStyle.STRICT);
public static void main(String[] args) {
LocalDate d = LocalDate.parse("03/09/2026", UK_DATE);
System.out.println(d);
System.out.println(d.format(UK_DATE));
LocalDateTime stamp = LocalDateTime.parse("2026-09-03T21:30:55");
System.out.println(stamp.format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm")));
try {
LocalDate.parse("30/02/2026", UK_DATE);
} catch (DateTimeParseException e) {
System.out.println("rejected: " + e.getParsedString());
}
}
}A DateTimeFormatter only describes text layout; the temporal type you call parse on decides which fields must be present and how the parsed values resolve.
Worked examples
Locale decides the words
Shows that text-producing pattern letters depend on a Locale carried by the formatter, and that withLocale returns a new formatter.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class MonthNames {
public static void main(String[] args) {
LocalDate day = LocalDate.of(2026, 9, 3);
DateTimeFormatter longForm = DateTimeFormatter.ofPattern("EEEE d MMMM uuuu", Locale.US);
System.out.println(longForm.format(day));
System.out.println(longForm.withLocale(Locale.FRANCE).format(day));
System.out.println(DateTimeFormatter.ofPattern("EEE d MMM uu", Locale.US).format(day));
}
}Example explained
Line 1The second argument to ofPattern supplies the language for EEEE and MMMM; omit it and the JVM default locale is used, so the same code prints different words on another machine.
Line 2withLocale(Locale.FRANCE) does not change longForm, it returns a second formatter, because DateTimeFormatter is immutable.
Line 3Four-letter EEEE and MMMM ask for full names while three letters ask for the abbreviations, which is why line three shortens to Thu and Sep.
Line 4Two-letter uu is a reduced year: it prints only the last two digits, and on parsing it maps 00-99 into 2000-2099.
Text that lacks a field the type needs
Demonstrates parsing month-and-year text into YearMonth, and using parseDefaulting when a LocalDate is required.
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
public class PartialText {
public static void main(String[] args) {
DateTimeFormatter monthYear = DateTimeFormatter.ofPattern("MM/uuuu");
System.out.println(YearMonth.parse("09/2026", monthYear));
try {
LocalDate.parse("09/2026", monthYear);
} catch (DateTimeParseException e) {
System.out.println("LocalDate needs a day-of-month");
}
DateTimeFormatter firstOfMonth = new DateTimeFormatterBuilder()
.appendPattern("MM/uuuu")
.parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
.toFormatter();
System.out.println(LocalDate.parse("09/2026", firstOfMonth));
}
}Example explained
Line 1YearMonth.parse succeeds because the pattern supplies exactly the two fields YearMonth is made of, and its toString prints the ISO form 2026-09.
Line 2The LocalDate.parse call fails during resolution, not during scanning: the characters matched, but no day-of-month exists and java.time refuses to guess one.
Line 3parseDefaulting inserts DAY_OF_MONTH=1 into the parsed field set before resolution, so the identical text now yields a real date.
Line 4appendPattern lets you keep the familiar pattern syntax inside a DateTimeFormatterBuilder instead of appending each field by hand.
The three resolver styles on 31 April
Compares STRICT, SMART and LENIENT resolution of an impossible day-of-month with the same pattern.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
public class Resolvers {
public static void main(String[] args) {
String text = "31/04/2026";
for (ResolverStyle style : ResolverStyle.values()) {
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd/MM/uuuu").withResolverStyle(style);
try {
System.out.println(style + " -> " + LocalDate.parse(text, f));
} catch (DateTimeParseException e) {
System.out.println(style + " -> refused");
}
}
}
}Example explained
Line 1The pattern splits the text into DAY_OF_MONTH=31, MONTH_OF_YEAR=4 and YEAR=2026; at that point nothing has asked whether April has 31 days.
Line 2STRICT hands the raw triple straight to date construction, which rejects it, so parse throws DateTimeParseException.
Line 3SMART clamps the day to the length of the month, turning 31 April into 30 April, and this is the answer you get by default if you never set a resolver style.
Line 4LENIENT treats the values as offsets, adding 30 days to 1 April and landing in May, the same rolling behaviour SimpleDateFormat had switched on by default.
Important notes
ofPattern without a Locale uses Locale.getDefault(Locale.Category.FORMAT), so month and weekday text varies between machines; pass a Locale explicitly, and avoid asserting on localized names in tests because that data comes from CLDR and can change between JDK releases.
Parsing of text fields is case-sensitive by default, so sep or SEP will not match the Sep the formatter expects, and literal characters must be quoted inside the pattern, as in uuuu-MM-dd'T'HH:mm.
Common mistakes
Pasting yyyy from an old SimpleDateFormat pattern and then switching on ResolverStyle.STRICT: year-of-era with no era in the text cannot be resolved, so every single parse throws DateTimeParseException until the letter is changed to uuuu.
Using YYYY for the calendar year: it is the week-based year, so a US-locale formatter renders 31 December 2026 as 2027-12-31, and log lines or filenames jump a year for a few days each winter.
Reusing one date-and-time pattern for a LocalDate: formatting a LocalDate with HH:mm throws UnsupportedTemporalTypeException, and parsing a date-only string into LocalDateTime fails outright instead of silently assuming midnight the way the old API did.
Try it yourself
Change, predict, then run
Build DateTimeFormatter.ofPattern("dd-MMM-uuuu HH:mm", Locale.US) with ResolverStyle.STRICT, parse "03-Sep-2026 21:30" into a LocalDateTime and print it in ISO form, then parse "31-Feb-2026 21:30" and print the message you would show a user when DateTimeParseException is thrown.
Open the Java workspaceCheck your understanding
A formatter built as DateTimeFormatter.ofPattern("dd/MM/yyyy").withResolverStyle(ResolverStyle.STRICT) throws when parsing "03/09/2026", though the same pattern worked before the resolver style was set. What is going on?
- The pattern dd/MM/yyyy is illegal in java.time, so no text can ever match it.
- STRICT parsing accepts only the ISO layout uuuu-MM-dd, so slashes are not allowed.
- yyyy is year-of-era, and strict resolution refuses to assume the missing era, leaving no usable year.
- LocalDate.parse ignores the formatter argument unless the text ends with a zone offset.
Show answer
Under the default SMART resolver the resolver quietly assumes the current era and turns year-of-era 2026 into year 2026; STRICT removes that assumption, so the parsed fields are YearOfEra=2026 with no era and no LocalDate can be built. The first option is tempting because the failure looks like a bad pattern, but the pattern is perfectly legal and even formats correctly; the fix is the letter, uuuu, the proleptic year that needs no era.