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.