19.4 Formatting and Parsing
This section covers DateTimeFormatter for formatting and parsing date-time values, including predefined formatters, custom patterns, localization, and parsing strategies for robust production code.
DateTimeFormatter Basics
DateTimeFormatter provides thread-safe, immutable formatting and parsing of date-time values.
Predefined Formatters:
public class PredefinedFormatters {
public void isoFormatters() {
ZonedDateTime zdt = ZonedDateTime.now();
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
// ISO-8601 formatters
String isoDate = date.format(DateTimeFormatter.ISO_DATE);
String isoTime = time.format(DateTimeFormatter.ISO_TIME);
String isoDateTime = dateTime.format(DateTimeFormatter.ISO_DATE_TIME);
String isoZoned = zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
String isoInstant = zdt.format(DateTimeFormatter.ISO_INSTANT);
String isoOffset = zdt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
System.out.println("ISO_DATE: " + isoDate);
System.out.println("ISO_TIME: " + isoTime);
System.out.println("ISO_DATE_TIME: " + isoDateTime);
System.out.println("ISO_ZONED_DATE_TIME: " + isoZoned);
System.out.println("ISO_INSTANT: " + isoInstant);
}
public void basicFormatters() {
LocalDate date = LocalDate.of(2025, 1, 6);
LocalTime time = LocalTime.of(14, 30, 45);
// Basic ISO formatters
String basicDate = date.format(DateTimeFormatter.BASIC_ISO_DATE);
System.out.println("BASIC_ISO_DATE: " + basicDate); // 20250106
// RFC 1123 formatter (HTTP date header)
ZonedDateTime zdt = ZonedDateTime.of(date, time, ZoneId.of("GMT"));
String rfc1123 = zdt.format(DateTimeFormatter.RFC_1123_DATE_TIME);
System.out.println("RFC_1123: " + rfc1123);
}
}
Creating Custom Formatters:
public class CustomFormatters {
public void patternBasedFormatters() {
LocalDateTime dt = LocalDateTime.of(2025, 1, 6, 14, 30, 45);
// Using pattern strings
DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted1 = dt.format(formatter1);
System.out.println("Custom format: " + formatted1);
// US date format
DateTimeFormatter usFormat = DateTimeFormatter.ofPattern("MM/dd/yyyy");
String usFormatted = dt.toLocalDate().format(usFormat);
System.out.println("US format: " + usFormatted); // 01/06/2025
// European date format
DateTimeFormatter euFormat = DateTimeFormatter.ofPattern("dd/MM/yyyy");
String euFormatted = dt.toLocalDate().format(euFormat);
System.out.println("EU format: " + euFormatted); // 06/01/2025
// Time with AM/PM
DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("hh:mm a");
String timeFormatted = dt.toLocalTime().format(timeFormat);
System.out.println("Time 12h: " + timeFormatted); // 02:30 PM
// Full date-time with timezone
ZonedDateTime zdt = dt.atZone(ZoneId.of("America/New_York"));
DateTimeFormatter fullFormat = DateTimeFormatter.ofPattern(
"EEEE, MMMM dd, yyyy 'at' hh:mm a z"
);
String fullFormatted = zdt.format(fullFormat);
System.out.println("Full format: " + fullFormatted);
}
}
Pattern Symbols
Understanding pattern symbols is essential for custom formatting.
Common Pattern Symbols:
public class PatternSymbols {
public void demonstrateSymbols() {
ZonedDateTime zdt = ZonedDateTime.of(
2025, 1, 6, 14, 30, 45, 123456789,
ZoneId.of("America/New_York")
);
// Year patterns
System.out.println("y: " + zdt.format(DateTimeFormatter.ofPattern("y"))); // 2025
System.out.println("yy: " + zdt.format(DateTimeFormatter.ofPattern("yy"))); // 25
System.out.println("yyyy: " + zdt.format(DateTimeFormatter.ofPattern("yyyy"))); // 2025
// Month patterns
System.out.println("M: " + zdt.format(DateTimeFormatter.ofPattern("M"))); // 1
System.out.println("MM: " + zdt.format(DateTimeFormatter.ofPattern("MM"))); // 01
System.out.println("MMM: " + zdt.format(DateTimeFormatter.ofPattern("MMM"))); // Jan
System.out.println("MMMM: " + zdt.format(DateTimeFormatter.ofPattern("MMMM"))); // January
// Day patterns
System.out.println("d: " + zdt.format(DateTimeFormatter.ofPattern("d"))); // 6
System.out.println("dd: " + zdt.format(DateTimeFormatter.ofPattern("dd"))); // 06
System.out.println("D: " + zdt.format(DateTimeFormatter.ofPattern("D"))); // 6 (day of year)
System.out.println("E: " + zdt.format(DateTimeFormatter.ofPattern("E"))); // Mon
System.out.println("EEEE: " + zdt.format(DateTimeFormatter.ofPattern("EEEE"))); // Monday
// Hour patterns
System.out.println("H: " + zdt.format(DateTimeFormatter.ofPattern("H"))); // 14 (0-23)
System.out.println("HH: " + zdt.format(DateTimeFormatter.ofPattern("HH"))); // 14
System.out.println("h: " + zdt.format(DateTimeFormatter.ofPattern("h"))); // 2 (1-12)
System.out.println("hh: " + zdt.format(DateTimeFormatter.ofPattern("hh"))); // 02
// Minute and second patterns
System.out.println("m: " + zdt.format(DateTimeFormatter.ofPattern("m"))); // 30
System.out.println("mm: " + zdt.format(DateTimeFormatter.ofPattern("mm"))); // 30
System.out.println("s: " + zdt.format(DateTimeFormatter.ofPattern("s"))); // 45
System.out.println("ss: " + zdt.format(DateTimeFormatter.ofPattern("ss"))); // 45
// Nanosecond patterns
System.out.println("S: " + zdt.format(DateTimeFormatter.ofPattern("S"))); // 1
System.out.println("SSS: " + zdt.format(DateTimeFormatter.ofPattern("SSS"))); // 123
System.out.println("SSSSSSSSS: " + zdt.format(DateTimeFormatter.ofPattern("SSSSSSSSS"))); // 123456789
System.out.println("n: " + zdt.format(DateTimeFormatter.ofPattern("n"))); // 123456789
// AM/PM
System.out.println("a: " + zdt.format(DateTimeFormatter.ofPattern("a"))); // PM
// Timezone patterns
System.out.println("z: " + zdt.format(DateTimeFormatter.ofPattern("z"))); // EST
System.out.println("zzzz: " + zdt.format(DateTimeFormatter.ofPattern("zzzz"))); // Eastern Standard Time
System.out.println("Z: " + zdt.format(DateTimeFormatter.ofPattern("Z"))); // -0500
System.out.println("X: " + zdt.format(DateTimeFormatter.ofPattern("X"))); // -05
System.out.println("XX: " + zdt.format(DateTimeFormatter.ofPattern("XX"))); // -0500
System.out.println("XXX: " + zdt.format(DateTimeFormatter.ofPattern("XXX"))); // -05:00
System.out.println("VV: " + zdt.format(DateTimeFormatter.ofPattern("VV"))); // America/New_York
}
}
Pattern Examples:
public class PatternExamples {
public Map<String, DateTimeFormatter> getCommonPatterns() {
Map<String, DateTimeFormatter> patterns = new LinkedHashMap<>();
// Date patterns
patterns.put("ISO Date", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
patterns.put("US Date", DateTimeFormatter.ofPattern("MM/dd/yyyy"));
patterns.put("EU Date", DateTimeFormatter.ofPattern("dd/MM/yyyy"));
patterns.put("Long Date", DateTimeFormatter.ofPattern("MMMM dd, yyyy"));
patterns.put("Full Date", DateTimeFormatter.ofPattern("EEEE, MMMM dd, yyyy"));
// Time patterns
patterns.put("24-hour Time", DateTimeFormatter.ofPattern("HH:mm:ss"));
patterns.put("12-hour Time", DateTimeFormatter.ofPattern("hh:mm:ss a"));
patterns.put("Time with millis", DateTimeFormatter.ofPattern("HH:mm:ss.SSS"));
// DateTime patterns
patterns.put("ISO DateTime", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
patterns.put("Readable DateTime", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
patterns.put("Full DateTime", DateTimeFormatter.ofPattern("EEEE, MMMM dd, yyyy 'at' hh:mm a"));
// With timezone
patterns.put("DateTime with Zone", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"));
patterns.put("DateTime with Offset", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss XXX"));
return patterns;
}
public void demonstratePatterns() {
ZonedDateTime zdt = ZonedDateTime.now();
Map<String, DateTimeFormatter> patterns = getCommonPatterns();
patterns.forEach((name, formatter) -> {
try {
String formatted = zdt.format(formatter);
System.out.println(name + ": " + formatted);
} catch (Exception e) {
System.out.println(name + ": (not applicable)");
}
});
}
}
Localized Formatting
Format date-time values according to locale-specific conventions.
Locale-Based Formatting:
public class LocalizedFormatting {
public void localizedStyles() {
LocalDateTime dt = LocalDateTime.of(2025, 1, 6, 14, 30, 45);
ZonedDateTime zdt = dt.atZone(ZoneId.of("America/New_York"));
// FormatStyle: FULL, LONG, MEDIUM, SHORT
DateTimeFormatter fullFormatter = DateTimeFormatter.ofLocalizedDateTime(
FormatStyle.FULL
).withLocale(Locale.US);
DateTimeFormatter longFormatter = DateTimeFormatter.ofLocalizedDateTime(
FormatStyle.LONG
).withLocale(Locale.US);
DateTimeFormatter mediumFormatter = DateTimeFormatter.ofLocalizedDateTime(
FormatStyle.MEDIUM
).withLocale(Locale.US);
DateTimeFormatter shortFormatter = DateTimeFormatter.ofLocalizedDateTime(
FormatStyle.SHORT
).withLocale(Locale.US);
System.out.println("FULL: " + zdt.format(fullFormatter));
System.out.println("LONG: " + zdt.format(longFormatter));
System.out.println("MEDIUM: " + zdt.format(mediumFormatter));
System.out.println("SHORT: " + zdt.format(shortFormatter));
}
public void multiLocaleFormatting() {
LocalDate date = LocalDate.of(2025, 1, 6);
// Different locales
Locale[] locales = {
Locale.US,
Locale.UK,
Locale.FRANCE,
Locale.GERMANY,
Locale.JAPAN,
Locale.CHINA,
new Locale("es", "ES") // Spanish
};
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
for (Locale locale : locales) {
String formatted = date.format(formatter.withLocale(locale));
System.out.println(locale.getDisplayName() + ": " + formatted);
}
}
public void localizedPatterns() {
LocalDateTime dt = LocalDateTime.of(2025, 1, 6, 14, 30);
// Pattern with locale affects month/day names
DateTimeFormatter pattern = DateTimeFormatter.ofPattern("EEEE, dd MMMM yyyy");
System.out.println("English: " + dt.format(pattern.withLocale(Locale.ENGLISH)));
System.out.println("French: " + dt.format(pattern.withLocale(Locale.FRENCH)));
System.out.println("German: " + dt.format(pattern.withLocale(Locale.GERMAN)));
System.out.println("Japanese: " + dt.format(pattern.withLocale(Locale.JAPANESE)));
}
}
Parsing Date-Time Values
Parse text into temporal objects with error handling.
Basic Parsing:
public class DateTimeParsing {
public void basicParsing() {
// Parse with ISO formatters
LocalDate date1 = LocalDate.parse("2025-01-06");
LocalTime time1 = LocalTime.parse("14:30:45");
LocalDateTime dt1 = LocalDateTime.parse("2025-01-06T14:30:45");
ZonedDateTime zdt1 = ZonedDateTime.parse("2025-01-06T14:30:45-05:00[America/New_York]");
Instant instant1 = Instant.parse("2025-01-06T19:30:45Z");
System.out.println("Parsed date: " + date1);
System.out.println("Parsed time: " + time1);
System.out.println("Parsed datetime: " + dt1);
System.out.println("Parsed zoned: " + zdt1);
System.out.println("Parsed instant: " + instant1);
}
public void customPatternParsing() {
// Parse with custom patterns
DateTimeFormatter usDateFormat = DateTimeFormatter.ofPattern("MM/dd/yyyy");
LocalDate usDate = LocalDate.parse("01/06/2025", usDateFormat);
DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("hh:mm a");
LocalTime time = LocalTime.parse("02:30 PM", timeFormat);
DateTimeFormatter fullFormat = DateTimeFormatter.ofPattern(
"EEEE, MMMM dd, yyyy 'at' hh:mm a"
);
LocalDateTime dt = LocalDateTime.parse(
"Monday, January 06, 2025 at 02:30 PM",
fullFormat
);
System.out.println("Parsed US date: " + usDate);
System.out.println("Parsed time: " + time);
System.out.println("Parsed full: " + dt);
}
}
Lenient and Strict Parsing:
public class ParsingModes {
public void strictVsLenientParsing() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// Strict parsing (default) - rejects invalid dates
try {
LocalDate invalid = LocalDate.parse("2025-02-30", formatter);
} catch (DateTimeParseException e) {
System.out.println("Strict parsing rejected: " + e.getMessage());
}
// Lenient parsing - attempts to resolve invalid dates
DateTimeFormatter lenient = formatter.withResolverStyle(ResolverStyle.LENIENT);
LocalDate resolved = LocalDate.parse("2025-02-30", lenient);
System.out.println("Lenient parsing resolved to: " + resolved); // 2025-03-02
// Smart parsing - reasonable resolution
DateTimeFormatter smart = formatter.withResolverStyle(ResolverStyle.SMART);
try {
LocalDate smartParsed = LocalDate.parse("2025-02-30", smart);
System.out.println("Smart parsing: " + smartParsed);
} catch (DateTimeException e) {
System.out.println("Smart parsing rejected: " + e.getMessage());
}
}
public void caseInsensitiveParsing() {
// Case-sensitive by default
DateTimeFormatter caseSensitive = DateTimeFormatter.ofPattern("MMM yyyy");
try {
LocalDate parsed = LocalDate.parse("jan 2025", caseSensitive);
} catch (DateTimeParseException e) {
System.out.println("Case-sensitive parsing failed");
}
// Case-insensitive parsing
DateTimeFormatter caseInsensitive = DateTimeFormatter.ofPattern("MMM yyyy")
.withResolverStyle(ResolverStyle.SMART);
DateTimeFormatter relaxed = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.appendPattern("MMM yyyy")
.toFormatter();
LocalDate parsed = LocalDate.parse("jan 2025", relaxed);
System.out.println("Case-insensitive parsing: " + parsed);
}
}
Robust Parsing with Error Handling:
public class RobustParsing {
public Optional<LocalDate> parseDate(String text, DateTimeFormatter formatter) {
try {
return Optional.of(LocalDate.parse(text, formatter));
} catch (DateTimeParseException e) {
System.err.println("Failed to parse date: " + text + " - " + e.getMessage());
return Optional.empty();
}
}
public LocalDate parseDateWithFallback(String text, DateTimeFormatter... formatters) {
for (DateTimeFormatter formatter : formatters) {
try {
return LocalDate.parse(text, formatter);
} catch (DateTimeParseException e) {
// Try next formatter
}
}
throw new DateTimeParseException("Unable to parse date: " + text, text, 0);
}
public void demonstrateMultiFormatParsing() {
DateTimeFormatter[] formatters = {
DateTimeFormatter.ISO_DATE,
DateTimeFormatter.ofPattern("MM/dd/yyyy"),
DateTimeFormatter.ofPattern("dd/MM/yyyy"),
DateTimeFormatter.ofPattern("yyyy-MM-dd"),
DateTimeFormatter.ofPattern("MMMM dd, yyyy")
};
String[] testDates = {
"2025-01-06",
"01/06/2025",
"06/01/2025",
"January 06, 2025"
};
for (String dateStr : testDates) {
try {
LocalDate parsed = parseDateWithFallback(dateStr, formatters);
System.out.println("Parsed '" + dateStr + "' as: " + parsed);
} catch (DateTimeParseException e) {
System.out.println("Failed to parse: " + dateStr);
}
}
}
}
DateTimeFormatterBuilder
Build complex formatters programmatically.
Builder Patterns:
public class FormatterBuilder {
public void basicBuilder() {
// Build formatter step by step
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4)
.appendLiteral('-')
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
.appendLiteral('-')
.appendValue(ChronoField.DAY_OF_MONTH, 2)
.toFormatter();
LocalDate date = LocalDate.of(2025, 1, 6);
String formatted = date.format(formatter);
System.out.println("Built format: " + formatted); // 2025-01-06
}
public void advancedBuilder() {
// Complex formatter with optional sections
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4)
.appendLiteral('-')
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
.appendLiteral('-')
.appendValue(ChronoField.DAY_OF_MONTH, 2)
.appendLiteral(' ')
.appendValue(ChronoField.HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
.optionalStart()
.appendLiteral(':')
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
.optionalEnd()
.optionalStart()
.appendLiteral('.')
.appendFraction(ChronoField.NANO_OF_SECOND, 3, 3, false)
.optionalEnd()
.toFormatter();
LocalDateTime dt1 = LocalDateTime.of(2025, 1, 6, 14, 30);
LocalDateTime dt2 = LocalDateTime.of(2025, 1, 6, 14, 30, 45);
LocalDateTime dt3 = LocalDateTime.of(2025, 1, 6, 14, 30, 45, 123000000);
System.out.println("No seconds: " + dt1.format(formatter));
System.out.println("With seconds: " + dt2.format(formatter));
System.out.println("With millis: " + dt3.format(formatter));
}
public DateTimeFormatter createFlexibleParser() {
return new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.parseLenient()
.appendValue(ChronoField.YEAR, 4)
.appendOptional(DateTimeFormatter.ofPattern("-MM-dd"))
.appendOptional(DateTimeFormatter.ofPattern("/MM/dd"))
.appendOptional(DateTimeFormatter.ofPattern(".MM.dd"))
.toFormatter();
}
}
Production Patterns
Best practices for formatting and parsing in production code.
Formatter Cache:
public class FormatterCache {
private static final Map<String, DateTimeFormatter> CACHE = new ConcurrentHashMap<>();
public static DateTimeFormatter getOrCreate(String pattern) {
return CACHE.computeIfAbsent(pattern, DateTimeFormatter::ofPattern);
}
public static DateTimeFormatter getOrCreate(String pattern, Locale locale) {
String key = pattern + "_" + locale;
return CACHE.computeIfAbsent(key, k ->
DateTimeFormatter.ofPattern(pattern).withLocale(locale)
);
}
}
API Serialization:
public class ApiSerialization {
// Use ISO-8601 for API communication
private static final DateTimeFormatter API_FORMATTER =
DateTimeFormatter.ISO_OFFSET_DATE_TIME;
public String serializeForApi(ZonedDateTime dateTime) {
return dateTime.format(API_FORMATTER);
}
public OffsetDateTime deserializeFromApi(String text) {
return OffsetDateTime.parse(text, API_FORMATTER);
}
// Store as Instant for database
public Instant toDatabase(String apiTimestamp) {
return OffsetDateTime.parse(apiTimestamp, API_FORMATTER).toInstant();
}
// Convert from database to API format
public String fromDatabase(Instant instant, ZoneId userZone) {
return instant.atZone(userZone)
.toOffsetDateTime()
.format(API_FORMATTER);
}
}
Summary
Formatting and parsing provides:
- Predefined Formatters: ISO-8601, RFC-1123, and localized formats
- Custom Patterns: Flexible pattern symbols for any format
- Localization: Locale-aware formatting with FormatStyle
- Parsing Modes: Strict, lenient, and smart resolution strategies
- Builder API: Programmatic formatter construction
- Production Patterns: Caching, API serialization, robust error handling
Proper formatting and parsing ensures consistent, locale-aware temporal data handling across applications.