Lesson 50Phase 5: Errors, files and the standard library
Dates and times with java.time
LocalDate, LocalTime and LocalDateTime, adding and comparing dates, periods and durations, and formatting them for people.
By BasithUpdated 45 min
- LocalDate
- LocalTime
- LocalDateTime
- Period
- Duration
- ChronoUnit
A library has to work out when a borrowed book is due back, and a timetable app has to know which classes fall on a Saturday. Both are questions about dates. Java answers them with the classes in its java.time package. In this lesson you create dates and times, move them forward and back, compare them, count the time between two of them, and print them in the form people expect to read.
- Create dates and times with
ofandparse, and read parts such as the month or the day of the week. - Add days, weeks, months and years to a date, and keep the result, because a date never changes.
- Predict what Java does with 31 January plus one month, and with 29 February in a year that is not a leap year.
- Compare and sort dates, and count the time between two of them with
ChronoUnit,PeriodandDuration. - Switch on
DayOfWeekandMonth, and print their names for people. - Format a date with a
DateTimeFormatterpattern, and read the exception Java throws for text it cannot parse.
String never changes, and its methods hand back a new string, as you saw in strings. Dates behave the same way. You switched on your own enums in enums and lambdas and met the arrow form of switch in sealed types and pattern matching. You will also sort with Comparable and Comparator, keep data in a record, and catch exceptions as in exception handling. Every program starts with import lines, from packages and imports.A date, a time, or both#
Three classes in java.time cover most everyday jobs:
- LocalDate: a day on the calendar, such as 14 March 2025. It has no time of day.
- LocalTime: a time on the clock, such as half past nine. It has no date.
- LocalDateTime: a date and a time together, such as half past nine on 14 March 2025.
Localin these names means the value has no time zone attached. It is the date printed on a wall calendar and the time shown on a wall clock, wherever that wall happens to be. That is what you want for a birthday, a due date or a shop's opening hours. Time zones get a short section at the end of the lesson.
import java.time.LocalDate;import java.time.LocalDateTime;import java.time.LocalTime; public class Main { public static void main(String[] args) { LocalDate launch = LocalDate.of(2025, 3, 14); LocalTime opening = LocalTime.of(9, 30); LocalDateTime meeting = LocalDateTime.of(2025, 3, 14, 9, 30); System.out.println(launch); System.out.println(opening); System.out.println(meeting); }}This prints 2025-03-14, then 09:30, then 2025-03-14T09:30. There is no new anywhere. You ask the class for a value with its static method of: the year, month and day for a date, the hour and minute for a time (a third number adds seconds). Months are numbered the way people number them, so 3 is March.
Printing uses the international standard text form, called ISO 8601: year, month and day joined by dashes, hours and minutes joined by a colon, and a capital T between the date and the time. Every part has a fixed number of digits, with a zero in front where needed, so sorting dates written this way as plain text also puts them in date order. The seconds appear only when they are not zero: LocalTime.of(9, 30, 15) prints 09:30:15.
java.util.Date and java.util.Calendar. Both are mutable: any code holding a reference to a date can change it underneath you. Calendar also counts months from 0, so January is 0 and December is 11, a common source of bugs. The java.time classes replaced them in Java 8, in 2014. You will still meet Date in old code and old answers online; new code uses java.time.Month constants and imports#
Instead of a month number you can pass a constant of the Month enum, which reads better and cannot be mixed up with the day: LocalDate.of(2025, Month.MARCH, 14). Each class needs an import line, because java.time is a separate package. Import the classes one at a time, as in the program above, or the whole package with import java.time.*;.
Reading text with parse#
When a date arrives as text, for example from a file, parse turns it into a value. Without any other instructions it expects the ISO form you just saw. And because a date has to be a real day, Java checks the numbers you give of:
import java.time.DateTimeException;import java.time.LocalDate;import java.time.LocalTime; public class Main { public static void main(String[] args) { LocalDate christmas = LocalDate.parse("2025-12-25"); LocalTime kickoff = LocalTime.parse("18:45"); System.out.println(christmas + " at " + kickoff); try { LocalDate.of(2025, 2, 30); } catch (DateTimeException e) { System.out.println(e.getMessage()); } try { LocalDate.of(2025, 13, 1); } catch (DateTimeException e) { System.out.println(e.getMessage()); } }}The first line prints 2025-12-25 at 18:45. The next two show Java refusing dates that cannot exist. There is no 30 February and no month 13, so of throws a DateTimeException, and the messages say Invalid date 'FEBRUARY 30' and Invalid value for MonthOfYear (valid values 1 - 12): 13. Since every value is checked when it is made, a LocalDate you hold is always a real day.
Today's date#
LocalDate.now() reads today's date from the clock of the computer the program runs on. LocalTime.now() and LocalDateTime.now() work the same way.
import java.time.LocalDate;import java.time.LocalTime; public class Main { public static void main(String[] args) { LocalDate today = LocalDate.now(); LocalTime time = LocalTime.now(); System.out.println(today); // the date on your computer, such as 2025-03-14 System.out.println(time); // the time to a fraction of a second, such as 09:41:07.123456 }}Its output changes every time you run it, so this one is read-along. Every other example on this page uses fixed dates, and you get the same output as the page whenever you run them.
LocalDate.now() inside it gives a different answer every day, which makes it hard to check. Give the method a LocalDate today parameter instead. The real program passes LocalDate.now(), and you can try the method with any date you like.Reading the parts#
A date can tell you its year, its month, the day of the week it falls on, and more. Step through this one and compare each line with the printed value.
import java.time.LocalDate; public class Main { public static void main(String[] args) { LocalDate date = LocalDate.of(2024, 2, 29); System.out.println(date.getYear()); System.out.println(date.getMonthValue()); System.out.println(date.getMonth()); System.out.println(date.getDayOfMonth()); System.out.println(date.getDayOfWeek()); System.out.println(date.getDayOfYear()); System.out.println(date.lengthOfMonth()); System.out.println(date.isLeapYear()); }}It prints 2024, 2, FEBRUARY, 29, THURSDAY, 60, 29 and true. getMonthValue() gives the month as a number, while getMonth() gives FEBRUARY, a constant of the Month enum. getDayOfWeek() is an enum constant too, and a later section puts both to work. getDayOfYear() counts from 1 January, so 29 February is day 60. A LocalTime has the matching getHour(), getMinute() and getSecond(), and a LocalDateTime has all of them.
Moving a date: plus and minus#
The library lends books for three weeks. A book borrowed on 14 March is due back 21 days later, and plusDays works out the date, counting across the end of March for you.
import java.time.LocalDate; public class Main { public static void main(String[] args) { LocalDate borrowed = LocalDate.of(2025, 3, 14); LocalDate due = borrowed.plusDays(21); System.out.println("Borrowed: " + borrowed); System.out.println("Due back: " + due); }}It prints Borrowed: 2025-03-14 and Due back: 2025-04-04. Step through it and read the narration on the plusDays line. plusDays did not change borrowed. It made a new LocalDate for 4 April and handed it back, and the program stored that in due. borrowed still holds 14 March.
Key ideaA
LocalDate,LocalTimeorLocalDateTimenever changes after it is made.plusDaysand every other plus, minus and with method hand back a new value and leave the original as it was.
A value that never changes is called immutable, as you saw with strings: toUpperCase() gave you a new string and left the old one alone. Immutability makes dates safe to share, because a method you pass your date to cannot move it behind your back. The cost is that you must store every result you want to keep.
import java.time.LocalDate; public class Main { public static void main(String[] args) { LocalDate due = LocalDate.of(2025, 4, 4); due.plusDays(7); System.out.println(due); due = due.plusDays(7); System.out.println(due); }}The reader renews the loan for one more week, but the result of the first plusDays call is never stored. What do the two lines print?
It prints 2025-04-04 and then 2025-04-11. The first plusDays(7) builds 11 April and throws it away, because nothing stores it. The second stores the new date back in due. The compiler does not warn you about the first line, so look for this mistake whenever a date refuses to move.
The plus, minus and with methods#
| Methods | Example | Gives |
|---|---|---|
plusDays, minusDays | date.plusDays(30) | 2025-04-13 |
plusWeeks, minusWeeks | date.plusWeeks(2) | 2025-03-28 |
plusMonths, minusMonths | date.plusMonths(1) | 2025-04-14 |
plusYears, minusYears | date.minusYears(5) | 2020-03-14 |
withDayOfMonth | date.withDayOfMonth(1) | 2025-03-01 |
withYear | date.withYear(2030) | 2030-03-14 |
A with method replaces one part and keeps the rest, so withDayOfMonth(1) gives the first day of the same month. Every call returns a date, which means you can chain calls and read them left to right: date.plusMonths(1).withDayOfMonth(1) is the first day of next month, 2025-04-01.
Times wrap around midnight#
import java.time.LocalDateTime;import java.time.LocalTime; public class Main { public static void main(String[] args) { LocalTime start = LocalTime.of(23, 30); System.out.println(start.plusMinutes(45)); LocalDateTime party = LocalDateTime.of(2025, 12, 31, 23, 30); System.out.println(party.plusMinutes(45)); }}A LocalTime is a position on a clock face, so 45 minutes after 23:30 is 00:15, and nothing records that a day went by. A LocalDateTime carries its date along, so the same 45 minutes takes the party into the new year: 2026-01-01T00:15. When the day matters, use a LocalDateTime.
Month ends and leap years#
Adding days needs no special rule: the day after 31 January is 1 February. Months are different, because they have 28, 29, 30 or 31 days. So what is one month after 31 January, when February has no 31st?
import java.time.LocalDate; public class Main { public static void main(String[] args) { LocalDate endOfJanuary = LocalDate.of(2025, 1, 31); System.out.println(endOfJanuary.plusMonths(1)); System.out.println(LocalDate.of(2024, 1, 31).plusMonths(1)); System.out.println(LocalDate.of(2025, 3, 31).minusMonths(1)); }}It prints 2025-02-28, 2024-02-29 and 2025-02-28. Java changes the month first. Then, if that day does not exist in the new month, it uses the month's last day instead. So 31 January plus one month is 28 February in 2025, and 29 February in 2024, which was a leap year. Going backwards follows the same rule. Java never throws an exception here, and it never spills the extra days over into March.
import java.time.LocalDate; public class Main { public static void main(String[] args) { LocalDate start = LocalDate.of(2025, 1, 31); System.out.println(start.plusMonths(1).plusMonths(1)); System.out.println(start.plusMonths(2)); }}Is adding one month twice the same as adding two months at once?
The first line prints 2025-03-28. One month takes 31 January to 28 February, and the second month starts from that 28 and keeps it. The second line jumps straight from January to March, which has a 31st, and prints 2025-03-31. With months, taking the steps one at a time can change the answer.
Repeating dates: count from the first one#
That matters for anything that repeats every month, such as a subscription that renews on the 31st. Work out each renewal from the first date, not from the previous renewal:
import java.time.LocalDate; public class Main { public static void main(String[] args) { LocalDate first = LocalDate.of(2025, 1, 31); for (int i = 0; i < 6; i++) { System.out.println(first.plusMonths(i)); } }}The renewals are 2025-01-31, 2025-02-28, 2025-03-31, 2025-04-30, 2025-05-31 and 2025-06-30: the 31st whenever the month has one, and the month's last day when it does not. Had the loop added one month to the previous renewal each time, February's 28 would have stuck, and every renewal after it would fall on the 28th.
Leap years and 29 February#
A leap year has 366 days, because February gets a 29th. The rule: a year divisible by 4 is a leap year, except a year divisible by 100, which is not, except a year divisible by 400, which is. So 2024 and 2000 were leap years, 1900 was not, and 2100 will not be. isLeapYear() applies the whole rule for you.
import java.time.DateTimeException;import java.time.LocalDate; public class Main { public static void main(String[] args) { LocalDate leapDay = LocalDate.of(2024, 2, 29); System.out.println(leapDay.plusYears(1)); System.out.println(leapDay.plusYears(4)); System.out.println(LocalDate.of(2100, 1, 1).isLeapYear()); try { LocalDate.of(2025, 2, 29); } catch (DateTimeException e) { System.out.println(e.getMessage()); } }}plusYears follows the same rule as plusMonths. There is no 29 February in 2025, so 29 February 2024 plus one year is 2025-02-28, while four years lands on 2028-02-29. The third line is false, because 2100 is divisible by 100 and not by 400. of makes no adjustment: you asked for a day that does not exist, so it throws, with the message Invalid date 'February 29' as '2025' is not a leap year.
People born on 29 February celebrate on 28 February or on 1 March in the other years, depending on the family. plusYears always picks 28 February. If your program needs 1 March, you write that rule yourself.
LocalDate.of(2025, 5, 31).plusMonths(1) give?Comparing and sorting dates#
Is a book overdue? That is a question about order: does the due date come before today?
import java.time.LocalDate; public class Main { public static void main(String[] args) { LocalDate today = LocalDate.of(2025, 3, 14); LocalDate due = LocalDate.of(2025, 3, 11); System.out.println(due.isBefore(today)); System.out.println(due.isAfter(today)); System.out.println(due.equals(LocalDate.of(2025, 3, 11))); System.out.println(due.compareTo(today) < 0); }}It prints true, false, true, true. isBefore and isAfter answer the question in plain words. Both are strict, so a date is neither before nor after itself. equals is true when two values hold the same date. compareTo is the method that sorting uses, as in comparing and sorting: it returns a negative number when the first date is earlier, zero when the two are equal, and a positive number when the first is later. Only the sign of that number means anything.
== does not compare the dates; it asks whether two variables point at the same object. LocalDate.of(2025, 3, 11) == LocalDate.of(2025, 3, 11) is false, because each call makes its own object, even though the dates are equal. Use equals or isEqual.Sorting dates#
LocalDate has a natural order, earliest first, so Collections.sort(dates) puts a list of dates in calendar order without any comparator. That order also works when the dates sit inside other objects. Here the library keeps each loan in a record and sorts the loans by due date:
import java.time.LocalDate;import java.util.ArrayList;import java.util.Comparator;import java.util.List; public class Main { public static void main(String[] args) { LocalDate today = LocalDate.of(2025, 3, 14); List<Loan> loans = new ArrayList<>(); loans.add(new Loan("Dune", LocalDate.of(2025, 3, 20))); loans.add(new Loan("Emma", LocalDate.of(2025, 3, 2))); loans.add(new Loan("Ulysses", LocalDate.of(2025, 3, 11))); loans.sort(Comparator.comparing(Loan::due)); for (Loan loan : loans) { String status = loan.due().isBefore(today) ? "overdue" : "on loan"; System.out.println(loan.due() + " " + loan.title() + " (" + status + ")"); } }} record Loan(String title, LocalDate due) { }constructor Loan in class Loan cannot be applied to given types: it needs (String, LocalDate)
It prints 2025-03-02 Emma (overdue), 2025-03-11 Ulysses (overdue) and 2025-03-20 Dune (on loan). Comparator.comparing(Loan::due)orders the loans by their due dates, using the dates' own order. Step through and watch the list change order in the memory view, then watch the loop ask each due date whether it comes before today.
How long between two dates or times?#
A gap in time can be measured in different ways: as the number of days until the summer holiday, as someone's age in years, months and days, or as the hours and minutes of a work shift. Each way has its own tool.
ChronoUnit: a total in one unit#
ChronoUnit is an enum of units of time: DAYS, WEEKS, MONTHS, YEARS, HOURS, MINUTES and more. Each unit has a between method that counts how many whole units fit from one date to another. It lives in java.time.temporal, so it needs its own import.
import java.time.LocalDate;import java.time.temporal.ChronoUnit; public class Main { public static void main(String[] args) { LocalDate today = LocalDate.of(2025, 3, 14); LocalDate holiday = LocalDate.of(2025, 8, 1); long days = ChronoUnit.DAYS.between(today, holiday); long weeks = ChronoUnit.WEEKS.between(today, holiday); long months = ChronoUnit.MONTHS.between(today, holiday); System.out.println(days + " days"); System.out.println(weeks + " weeks"); System.out.println(months + " whole months"); System.out.println(ChronoUnit.DAYS.between(holiday, today)); }}The answers are long values: 140 days, 20 weeks and 4 whole months. The days happen to make exactly 20 weeks. The months stop at 4, because four months after 14 March is 14 July, and a fifth month would reach 14 August, past the holiday. Swap the two dates and the answer turns negative: -140.
The count includes the start date and leaves out the end date, the way a hotel counts nights: arrive on 1 March, leave on 2 March, and you stayed one night. ChronoUnit.DAYS.between for those two dates is 1.
Period: years, months and days#
import java.time.LocalDate;import java.time.Period; public class Main { public static void main(String[] args) { LocalDate born = LocalDate.of(2008, 7, 20); LocalDate today = LocalDate.of(2025, 3, 14); Period age = Period.between(born, today); System.out.println(age); System.out.println(age.getYears() + " years, " + age.getMonths() + " months, " + age.getDays() + " days"); }}A Period is an amount of time in calendar terms (years, months and days), the way people state an age. This one prints as P16Y7M22D, the ISO form: P for period, then 16 years, 7 months and 22 days. getYears(), getMonths() and getDays() return each part, so the second line reads 16 years, 7 months, 22 days. You can also make a period yourself, with Period.ofDays(10), Period.ofWeeks(2) or Period.of(1, 2, 3), and add it to a date: date.plus(Period.ofWeeks(2)).
import java.time.LocalDate;import java.time.Period;import java.time.temporal.ChronoUnit; public class Main { public static void main(String[] args) { LocalDate start = LocalDate.of(2025, 1, 1); LocalDate end = LocalDate.of(2025, 3, 15); System.out.println(Period.between(start, end).getDays()); System.out.println(ChronoUnit.DAYS.between(start, end)); }}Both lines ask how many days lie between 1 January and 15 March 2025. Do they print the same number?
They print 14 and 73. The period between the two dates is P2M14D: two whole months, then 14 more days. getDays() returns only that last part. The total, 73, comes from ChronoUnit.DAYS. Using getDays() when you want the total is an easy mistake to make, and because the wrong answer still looks like a believable number, it can go unnoticed for a long time.
until: the same answers, asked by the date#
A date can also ask the question itself. start.until(end) returns the same Period as Period.between(start, end), and start.until(end, ChronoUnit.DAYS) returns the same number as ChronoUnit.DAYS.between(start, end). Use whichever reads better in your code.
| You want | Write | For 1 January to 15 March 2025 |
|---|---|---|
| The total number of days | ChronoUnit.DAYS.between(start, end) | 73 |
| The same, asked by the date | start.until(end, ChronoUnit.DAYS) | 73 |
| Whole weeks | ChronoUnit.WEEKS.between(start, end) | 10 |
| Years, months and days | Period.between(start, end) | P2M14D |
| The same, asked by the date | start.until(end) | P2M14D |
Key idea
ChronoUnit.DAYS.betweencounts the total number of days.Period.betweensplits the gap into years, months and days, and itsgetDays()is only the days left over after the whole months.
daysUntilBirthday so it returns how many days are left until the person's next birthday. If the birthday is today, the answer is 0. If this year's birthday has already passed, count to next year's.Duration: hours and minutes#
A period counts calendar units. For time on a clock, such as the length of a work shift, use a Duration, which counts hours, minutes and seconds.
import java.time.Duration;import java.time.LocalTime; public class Main { public static void main(String[] args) { LocalTime clockIn = LocalTime.of(9, 0); LocalTime clockOut = LocalTime.of(17, 30); Duration shift = Duration.between(clockIn, clockOut); System.out.println(shift); System.out.println(shift.toMinutes() + " minutes"); System.out.println(shift.toHours() + "h " + shift.toMinutesPart() + "m"); Duration worked = shift.minus(Duration.ofMinutes(45)); System.out.println(worked); }}A duration prints as PT8H30M: P for period, T for time, then 8 hours and 30 minutes. toMinutes() gives the whole length in minutes, 510 minutes. toHours() gives the whole hours, and toMinutesPart() the minutes left over after them, so the third line is 8h 30m. Durations can be added and subtracted too: taking off a 45-minute lunch leaves PT7H45M.
LocalDate values.LocalTime or two LocalDateTime values.Period.between(date1, date2)Duration.between(time1, time2)P1Y2M3D.PT8H30M.Duration.between two LocalDate values and it throws UnsupportedTemporalTypeException: Unsupported unit: Seconds. A date has no hours or seconds to count. Between dates, use ChronoUnit or Period.Duration.between(LocalTime.of(22, 0), LocalTime.of(6, 0)) give?Days and months are enums#
getDayOfWeek() returns a DayOfWeek, an enum with the seven constants MONDAY to SUNDAY, and getMonth() returns a Month, JANUARY to DECEMBER. Everything you know about enums works with them, switchincluded. A shop's opening hours depend on the day of the week:
import java.time.DayOfWeek;import java.time.LocalDate; public class Main { public static void main(String[] args) { LocalDate start = LocalDate.of(2025, 3, 14); for (int i = 0; i < 3; i++) { LocalDate day = start.plusDays(i); DayOfWeek dow = day.getDayOfWeek(); String hours = switch (dow) { case SATURDAY -> "10:00 to 16:00"; case SUNDAY -> "closed"; default -> "09:00 to 18:00"; }; System.out.println(day + " " + dow + ": " + hours); } }}It prints 2025-03-14 FRIDAY: 09:00 to 18:00, 2025-03-15 SATURDAY: 10:00 to 16:00 and 2025-03-16 SUNDAY: closed. The case labels are the bare constant names, as with your own enums. One case can list several constants, as in case SATURDAY, SUNDAY -> "weekend";, and a switch expression that names all seven days needs no default, because the compiler can see that every day is covered.
Each day also has a number, from getValue(): 1 for Monday up to 7 for Sunday, following the ISO standard, where a week starts on Monday. Months run from 1 for January to 12 for December. Use getValue() for these numbers, not ordinal(), which counts from 0. Both enums wrap around when you add to them: DayOfWeek.SUNDAY.plus(1) is MONDAY, and Month.DECEMBER.plus(1) is JANUARY.
Names for people#
import java.time.DayOfWeek;import java.time.LocalDate;import java.time.Month;import java.time.format.TextStyle;import java.util.Locale; public class Main { public static void main(String[] args) { LocalDate date = LocalDate.of(2025, 3, 14); DayOfWeek day = date.getDayOfWeek(); Month month = date.getMonth(); System.out.println(day); System.out.println(day.getDisplayName(TextStyle.FULL, Locale.US)); System.out.println(day.getDisplayName(TextStyle.SHORT, Locale.US)); System.out.println(month.getDisplayName(TextStyle.FULL, Locale.US)); System.out.println(day.getValue() + " and " + month.getValue()); }}It prints FRIDAY, Friday, Fri, March and 5 and 3. Printing a constant shows its name in capitals, which suits a program better than a person. getDisplayName gives the word people use. TextStyle picks the length: FULL for Friday, SHORT for Fri, NARROW for the single letter F.
A Locale names a language, usually together with a country: Locale.US means English as written in the United States. On a real JVM, Locale.FRANCE gives vendredi and mars for the same day and month.
Formatting dates for people#
The ISO form, 2025-03-14, is right for files and for programs talking to each other. People expect something else, and what they expect depends on where they live: 14/03/2025 in the UK and India, 03/14/2025 in the US, or Friday 14 March 2025 at the top of a letter. A DateTimeFormatter turns a date into any of these, following a pattern you write.
import java.time.LocalDate;import java.time.format.DateTimeFormatter; public class Main { public static void main(String[] args) { LocalDate date = LocalDate.of(2025, 3, 14); DateTimeFormatter uk = DateTimeFormatter.ofPattern("dd/MM/yyyy"); DateTimeFormatter us = DateTimeFormatter.ofPattern("MM/dd/yyyy"); System.out.println(date.format(uk)); System.out.println(date.format(us)); System.out.println(date); }}It prints 14/03/2025, 03/14/2025 and 2025-03-14. In a pattern, letters stand for parts of the date: dd is the day of the month in two digits, MM the month in two digits, and yyyy the year. Anything that is not a letter, such as the slash, is copied into the text as it is. date.format(uk) and uk.format(date) do the same job. The date itself is untouched, so the last line still prints the ISO form.
The pattern letters#
| Letter | Means | For Wednesday 5 March 2025 at 15:09:26 |
|---|---|---|
y | Year | yyyy prints 2025, yy prints 25 |
M | Month | M prints 3, MM prints 03, MMM prints Mar, MMMM prints March |
d | Day of the month | d prints 5, dd prints 05 |
E | Day of the week | E prints Wed, EEEE prints Wednesday |
H | Hour on a 24-hour clock, 0 to 23 | H and HH print 15 |
h | Hour on a 12-hour clock, 1 to 12 | h prints 3, hh prints 03 |
m | Minute | m prints 9, mm prints 09 |
s | Second | s and ss print 26 |
a | AM or PM | a prints PM |
The case of each letter matters: M is the month and m the minute, H the 24-hour clock and hthe 12-hour one. Try patterns in the playground below. Edit the pattern or pick another date or time, and the text updates as you type. Underneath, each piece of the pattern is listed with what it printed. The playground formats with the same copy of Java's formatter that runs the snippets on this page, so a pattern Java would reject shows Java's own error message.
Try one
Spot the mistake
Friday, 14 March 2025 at 3:09 PM
Piece by piece
EEEE"Friday"day name, in full
,", "not a letter, so printed as it is
d"14"day of the month, no zero in front
" "not a letter, so printed as it is
MMMM"March"month name, in full
" "not a letter, so printed as it is
yyyy"2025"year, at least 4 digits
" "not a letter, so printed as it is
'at'"at"text in single quotes, printed as it is
" "not a letter, so printed as it is
h"3"hour on a 12-hour clock, 1 to 12
:":"not a letter, so printed as it is
mm"09"minute, two digits
" "not a letter, so printed as it is
a"PM"AM or PM
Two of the “Spot the mistake” patterns deserve a closer look, because they compile, run and print something that looks like a date:
import java.time.LocalDateTime;import java.time.format.DateTimeFormatter; public class Main { public static void main(String[] args) { LocalDateTime when = LocalDateTime.of(2024, 12, 30, 15, 9); System.out.println(when.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))); System.out.println(when.format(DateTimeFormatter.ofPattern("dd/mm/yyyy"))); System.out.println(when.format(DateTimeFormatter.ofPattern("dd/MM/YYYY"))); }}Only the first line, 30/12/2024, is right. The second prints 30/09/2024, because mm is the minute and the time is 15:09. The third prints 30/12/2025: capital Y is the week-based year, the year that a whole week belongs to, and the week holding 30 December 2024 also holds 1 January 2025, so it counts as part of 2025. YYYY agrees with yyyy except in the days around New Year, which is why this mistake can hide for most of the year.
Key idea
toStringgives the ISO form, which suits files and programs. For people, format with a pattern, and check the case of every letter:MMis the month,mmthe minute.
Fri 14 Mar, 3:09 PM for LocalDateTime.of(2025, 3, 14, 15, 9)?Words in a pattern#
import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.Locale; public class Main { public static void main(String[] args) { LocalDateTime paid = LocalDateTime.of(2025, 3, 14, 15, 9); DateTimeFormatter receipt = DateTimeFormatter.ofPattern("EEEE d MMMM yyyy 'at' h:mm a", Locale.US); DateTimeFormatter clock = DateTimeFormatter.ofPattern("h 'o''clock'", Locale.US); System.out.println(paid.format(receipt)); System.out.println(paid.format(clock)); }}This prints Friday 14 March 2025 at 3:09 PM and 3 o'clock. Letters inside single quotes are plain text, so 'at' prints the word at. Without the quotes, Java would read a as AM or PM and then reject t, which is not a pattern letter. To print a single quote mark, write two of them, as in 'o''clock'.
The second argument, Locale.US, sets the language of the names. Without it, Java uses the language your computer is set to, so the same receipt could come out as vendredi 14 mars 2025 at 3:09 PM on a computer set to French. Pass a locale whenever a pattern prints words.
DateTimeFormatter never changes after it is made, like a date. Create each one once, for example in a static final field, and use it everywhere in the program.Reading text back into a date#
Formatting has a reverse. When a person types 14/03/2025 into a form, LocalDate.parse with the same formatter turns the text into a LocalDate. Text that does not fit the pattern makes it throw a DateTimeParseException:
import java.time.LocalDate;import java.time.format.DateTimeFormatter;import java.time.format.DateTimeParseException; public class Main { public static void main(String[] args) { DateTimeFormatter uk = DateTimeFormatter.ofPattern("dd/MM/yyyy"); String[] typed = { "14/03/2025", "14/3/2025", "14/13/2025", "31/04/2025" }; for (String text : typed) { try { LocalDate date = LocalDate.parse(text, uk); System.out.println("OK: " + date); } catch (DateTimeParseException e) { System.out.println(e.getMessage()); } } }}The first text gives OK: 2025-03-14. The next two fail, and their messages are worth reading:
Text '14/3/2025' could not be parsed at index 3. The index is the position where Java gave up, counting from 0. At index 3,MMwants two digits and finds3/.Text '14/13/2025' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 13. The text has the right shape, but 13 is not a month.
DateTimeParseException is an unchecked exception, so the compiler does not make you catch it. Catch it anyway whenever the text comes from a person, and ask them to type the date again. To accept 14/3/2025 as well, use one letter each, d/M/yyyy: when reading, a single letter accepts the number with or without a zero in front.
OK: 2025-04-30. The text 31/04/2025 was accepted. A formatter made by ofPatternaccepts any day from 1 to 31 and then moves a day the month does not have back to the month's last day, the same rule that plusMonths uses. A form should usually refuse such a date instead.Due back on 21/03/2025 in the same format. Put the lines in order.Tap a line, then tap where it should go. Or use the arrows.
nextWorkingDay so it returns the first day after date that falls on Monday to Friday. Friday 14 March should give Monday 17 March.Time zones, briefly#
A LocalDateTimesuch as 2025-03-14T09:00 does not say where in the world it is nine o'clock. For a birthday or a shop's hours that does not matter. It matters as soon as people in different places share one moment, such as a video call between London and Tokyo, or the exact time a payment was made. For those jobs java.time has more classes:
ZoneId: a time zone, named by a region such asEurope/LondonorAsia/Tokyo.ZonedDateTime: a date and time in a particular zone.Instant: one moment on a single timeline for the whole world, counted in UTC (Coordinated Universal Time), the standard that every zone is measured from.
The in-browser engine does not have the world's time zone rules, so this program is read-along, with the output from real Java:
import java.time.Instant;import java.time.LocalDateTime;import java.time.ZoneId;import java.time.ZonedDateTime; public class Main { public static void main(String[] args) { LocalDateTime call = LocalDateTime.of(2025, 3, 14, 9, 0); ZonedDateTime london = call.atZone(ZoneId.of("Europe/London")); ZonedDateTime tokyo = london.withZoneSameInstant(ZoneId.of("Asia/Tokyo")); Instant moment = london.toInstant(); System.out.println(london); System.out.println(tokyo); System.out.println(moment); }}2025-03-14T09:00Z[Europe/London] 2025-03-14T18:00+09:00[Asia/Tokyo] 2025-03-14T09:00:00Z
The call starts at 9:00 in London, which is 18:00 the same day in Tokyo, nine hours ahead. The Zmeans an offset of zero from UTC. London keeps UTC in winter and moves its clocks an hour forward at the end of March. A change like that, called daylight saving time, is the reason a zone has a region's name and not a fixed number of hours: Java looks up the region's rules for each date.
Try it
Answer each one in your head or on paper first, then open the answer.
date.plusDays(1); on a line of its own and then prints date. Why has the date not moved, and what is the fix?DateTimeFormatter.ofPattern("dd/mm/yyyy") print for 14 March 2025 at 15:09, and why?LocalDate.parse(text), and someone types 2025-02-29. What happens?Practice
Work through these on your own. Each one builds on this page.
3 weeks and 3 days. Then change today to 26 December 2025 and make the program count to Christmas 2026 instead.loans.csv with lines such as Dune,2025-03-20. Read it with Files.readAllLines from the last lesson, split each line at the comma, parse the date, and print the overdue books sorted by due date, for a fixed today of 14 March 2025.Mo Tu We Th Fr Sa Su, then the dates in their columns. 1 March 2025 is a Saturday, so the first row starts with five empty places.boolean isAdult(LocalDate born, LocalDate today) that is true from a person's 18th birthday on. Try it for someone born on 29 February 2008, with today set to 28 February 2026 and then to 1 March 2026. Print the two periods as well. Do you agree with Java about when this person turns 18?Further reading#
- The java.time package in the Java SE 25 API documentation, with LocalDate, Period, Duration, DayOfWeek and the rest of the core classes
- DateTimeFormatter in the Java SE 25 API documentation, with the full table of pattern letters
- The Date Time API tutorial on dev.java, the official Java learning site, including time zones and instants
Spotted a mistake, or a part that did not make sense? Report it by email