Date Formatting
Formatting converts a date or time value into text: 28/12/2026, Monday, 28 December 2026, 06:05 PM, or 2026-12-28T18:05:09+05:30. In java.time, formatting is done by java.time.format.DateTimeFormatter, which supports predefined standard formats, custom patterns, and locale-aware styles that follow each language's conventions automatically. Unlike the legacy SimpleDateFormat, DateTimeFormatter is immutable and thread-safe, so one instance can be shared across an entire application.
Formatting Basics
Every java.time type has a format method:
import java.time.*;
import java.time.format.DateTimeFormatter;
LocalDate date = LocalDate.of(2026, 12, 28);
String s = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); // "28/12/2026"
Equivalently, formatter.format(temporal). Calling toString() produces ISO-8601 text (2026-12-28), which is ideal for machines but not usually what users should see.
Predefined Formatters
DateTimeFormatter provides constants for standard formats:
| Constant | Example output |
|---|---|
ISO_LOCAL_DATE | 2026-12-28 |
ISO_LOCAL_TIME | 18:05:09 |
ISO_LOCAL_DATE_TIME | 2026-12-28T18:05:09 |
ISO_OFFSET_DATE_TIME | 2026-12-28T18:05:09+05:30 |
ISO_ZONED_DATE_TIME | 2026-12-28T18:05:09+05:30[Asia/Kolkata] |
ISO_INSTANT | 2026-12-28T12:35:09Z |
BASIC_ISO_DATE | 20261228 |
RFC_1123_DATE_TIME | Mon, 28 Dec 2026 18:05:09 +0530 (HTTP headers, email) |
Use the ISO formats for anything read by other programs: JSON APIs, log files, database exports, file names. They are unambiguous, sortable as text, and understood everywhere.
Custom Patterns
DateTimeFormatter.ofPattern(pattern) builds a formatter from letters that stand for date and time fields. The number of repeated letters controls the width or style.
Using 28 December 2026, 18:05:09 in Asia/Kolkata:
| Pattern | Output | Meaning |
|---|---|---|
dd/MM/yyyy | 28/12/2026 | Day, month, year |
d MMM yyyy | 28 Dec 2026 | Abbreviated month name |
EEEE, d MMMM yyyy | Monday, 28 December 2026 | Full day and month names |
HH:mm:ss | 18:05:09 | 24-hour clock |
hh:mm a | 06:05 PM | 12-hour clock with AM/PM |
yyyy-MM-dd'T'HH:mm | 2026-12-28T18:05 | Literal text in single quotes |
E, dd MMM yyyy HH:mm z | Mon, 28 Dec 2026 18:05 IST | Zone abbreviation |
xxx VV | +05:30 Asia/Kolkata | Offset and zone ID |
QQQ yyyy | Q4 2026 | Quarter |
D | 362 | Day of the year |
Common pattern letters
| Letter | Field | Examples |
|---|---|---|
y | Year | yy → 26, yyyy → 2026 |
M | Month | M → 12, MM → 12, MMM → Dec, MMMM → December |
d | Day of month | d → 8, dd → 08 |
E | Day of week | E → Mon, EEEE → Monday |
H | Hour, 0–23 | HH → 18 |
h | Hour, 1–12 | hh → 06 |
a | AM/PM | PM |
m | Minute | mm → 05 |
s | Second | ss → 09 |
S | Fraction of second | SSS → milliseconds |
z | Zone name | IST, India Standard Time (zzzz) |
VV | Zone ID | Asia/Kolkata |
x, X, Z | Offset | +05:30, +0530 |
'...' | Literal text | 'T', 'at' |
Pattern letters are case-sensitive
Several letters differ only by case, and swapping them produces output that looks plausible but is wrong:
| Wrong | Right | Result of the mistake |
|---|---|---|
dd/mm/yyyy | dd/MM/yyyy | 28/05/2026: mm is minutes, not months |
YYYY-MM-dd | yyyy-MM-dd | 2027-12-28: YYYY is the week-based year |
hh:mm | HH:mm | 06:05 for 18:05 without an AM/PM marker |
DD | dd | Day of year, not day of month |
The YYYY mistake is particularly dangerous: it gives the correct year for most of the year and fails only in the last days of December (or the first days of January), when the week containing the date belongs to the next (or previous) year. Systems have printed tickets and invoices with the wrong year because of this.
Formatting a type that lacks a field
A pattern can only use fields the value contains:
LocalDate.of(2026, 1, 1).format(DateTimeFormatter.ofPattern("HH:mm"));
// UnsupportedTemporalTypeException: Unsupported field: HourOfDay
A LocalDate has no hour; a LocalDateTime has no zone, so z and VV require a ZonedDateTime (or a formatter with .withZone(...)).
Locale-Aware Formatting
Month and day names, the order of day and month, separators, and the 12- or 24-hour convention all vary by language and country. ofLocalizedDate, ofLocalizedTime, and ofLocalizedDateTime produce a format appropriate to a locale:
DateTimeFormatter medium = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
DateTimeFormatter full = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
| Locale | MEDIUM | FULL |
|---|---|---|
en-US | Dec 28, 2026 | Monday, December 28, 2026 |
en-GB | 28 Dec 2026 | Monday, 28 December 2026 |
hi-IN | 28 दिस॰ 2026 | सोमवार, 28 दिसंबर 2026 |
ta-IN | 28 டிச., 2026 | திங்கள், 28 டிசம்பர், 2026 |
de-DE | 28.12.2026 | Montag, 28. Dezember 2026 |
The styles are SHORT, MEDIUM, LONG, and FULL. Use .withLocale(locale) to choose the language; without it, the JVM default locale is used. Custom patterns also use the locale for names: ofPattern("EEEE", Locale.GERMANY) produces Montag.
Prefer localized styles for user-facing text. A custom pattern such as MM/dd/yyyy imposes one country's convention on everyone, and 03/04/2026 means 4 March in the United States but 3 April in India and the UK.
Formatting Instants
An Instant has no time zone, so field-based patterns cannot format it directly. Attach a zone to the formatter:
DateTimeFormatter fileStamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").withZone(ZoneOffset.UTC);
fileStamp.format(Instant.parse("2026-12-28T12:35:00Z")); // "20261228_123500"
DateTimeFormatter.ISO_INSTANT formats an instant in UTC without any extra setup.
Reusing Formatters
DateTimeFormatter is immutable and thread-safe. Create each formatter once, typically as a static final constant, and reuse it everywhere:
private static final DateTimeFormatter DISPLAY = DateTimeFormatter.ofPattern("dd MMM yyyy, hh:mm a", Locale.ENGLISH);
This is a major improvement over SimpleDateFormat, which is mutable and produced corrupted output when one instance was shared between threads.
Practical Example: Exam Reminders for International Students
An online exam starts at a single moment. Each student should see the time in their own zone and language, while logs and file names use standard, machine-friendly formats.
import java.time.*;
import java.time.format.*;
import java.util.*;
public class Notify {
record User(String name, ZoneId zone, Locale locale) { }
private static final DateTimeFormatter LOG = DateTimeFormatter.ISO_INSTANT;
private static final DateTimeFormatter FILE = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").withZone(ZoneOffset.UTC);
static String message(User u, Instant when) {
ZonedDateTime local = when.atZone(u.zone());
DateTimeFormatter date = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(u.locale());
DateTimeFormatter time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(u.locale());
return u.name() + ": " + local.format(date) + ", " + local.format(time) + " (" + u.zone().getId() + ")";
}
public static void main(String[] args) {
Instant examStart = Instant.parse("2026-12-28T12:35:00Z");
List<User> users = List.of(
new User("Ada", ZoneId.of("Europe/London"), Locale.UK),
new User("Ravi", ZoneId.of("Asia/Kolkata"), Locale.forLanguageTag("en-IN")),
new User("Meena", ZoneId.of("Asia/Kolkata"), Locale.forLanguageTag("ta-IN")),
new User("Lukas", ZoneId.of("Europe/Berlin"), Locale.GERMANY),
new User("Grace", ZoneId.of("America/New_York"), Locale.US)
);
for (User u : users) System.out.println(message(u, examStart));
System.out.println("Log: " + LOG.format(examStart));
System.out.println("File: report_" + FILE.format(examStart) + ".pdf");
}
}
Output:
Ada: Monday, 28 December 2026, 12:35 (Europe/London)
Ravi: Monday, 28 December, 2026, 6:05 pm (Asia/Kolkata)
Meena: திங்கள், 28 டிசம்பர், 2026, பிற்பகல் 6:05 (Asia/Kolkata)
Lukas: Montag, 28. Dezember 2026, 13:35 (Europe/Berlin)
Grace: Monday, December 28, 2026, 7:35 AM (America/New_York)
Log: 2026-12-28T12:35:00Z
File: report_20261228_123500.pdf
The exam is stored once, as an Instant. For each student, atZone converts it to local time, and localized formatters render it in the student's language: British English uses a 24-hour clock, Indian English a 12-hour clock with lowercase "pm," Tamil shows the day, month, and "afternoon" (பிற்பகல்) in Tamil script, German puts the day before the month with a period, and US English puts the month first. Two students in Chennai see the same local time in different languages, because zone and locale are independent settings. For machines, the log line uses ISO_INSTANT and the file name uses a compact UTC pattern, which also sorts chronologically as text.
Localized output comes from the JDK's locale data and can change slightly between Java versions (for example, the space before "pm" in Indian English is a narrow non-breaking space). Do not parse or compare localized text in code; use it only for display.