التواريخ والأوقات المحلية
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Many facts about a business are plain calendar facts: a customer's birthday, an invoice due date, the time a shop opens. They do not need a time zone. "The shop opens at 09:00" is true in whatever city you are in, and "the deadline is 31 March" does not change with the clock on your wall.
For these facts Java gives you three local types: LocalDate (a date), LocalTime (a time of day) and LocalDateTime (both together). "Local" means no time zone and no offset: they describe what a calendar and a wall clock show, not a specific moment on the global timeline. That last point is the idea of this lesson, and the source of the module's most common mistake.
You will create, read, compare and change these values, learn how invalid dates are handled, and learn when a local type is the right choice. Lesson 1 explained why time is hard and that java.time objects are immutable. This lesson uses both ideas.
Learning Objectives
By the end of this lesson you will be able to:
- Choose between
LocalDate,LocalTimeandLocalDateTimefor a requirement. - Create them with
of(...),now()andparse(...). - Read parts of a value with methods such as
getYear()andgetDayOfWeek(). - Change values with
plusDays,minusMonthsand similar methods, remembering that each returns a new object. - Compare two local values with
isBefore,isAfterandequals. - Explain what happens with invalid dates and with "31 January plus one month".
- Explain why a
LocalDateTimeis not a specific moment.
Written Explanation
The three local types
| Type | Holds | Example text | Good for |
|---|---|---|---|
LocalDate | year, month, day | 2026-03-15 | birthdays, due dates, holidays |
LocalTime | hour, minute, second, nanosecond | 09:30 | opening hours, a daily alarm time |
LocalDateTime | a date and a time | 2026-03-15T09:30 | a form field "date and time" before you know the zone |
All three live in java.time, so start the file with import java.time.*;. They are immutable. The Javadoc for LocalDate states that it does not store a time or a time zone, and describes it as the date "as used for birthdays".
Creating values
There are three common ways.
LocalDate d = LocalDate.of(2026, 3, 15); // year, month, day
LocalTime t = LocalTime.of(9, 30); // hour, minute
LocalDateTime dt = LocalDateTime.of(d, t); // combine a date and a time
LocalDate today = LocalDate.now(); // from the system clock and default zone
LocalDate parsed = LocalDate.parse("2026-03-15");of(...)builds a value from numbers. The month is a normal number from 1 to 12, not zero-based like the legacyDate.now()reads the system clock. The answer depends on the machine's default zone, which is why tests that callnow()are hard to repeat.parse(text)reads text in the standard ISO formyyyy-MM-ddfor dates,HH:mmfor times, andyyyy-MM-ddTHH:mmfor date-times. Lesson 4 covers other formats.
Printing a value with println shows the same ISO form:
2026-03-15 | 09:30 | 2026-03-15T09:30Reading parts
d.getYear(); // 2026
d.getMonthValue(); // 3
d.getDayOfMonth(); // 15
d.getDayOfWeek(); // SUNDAY
d.getDayOfYear(); // 74getDayOfWeek() and getMonth() return named constants (for example SUNDAY and MARCH) which print as words. You can convert between a date-time and its parts: dt.toLocalDate() and dt.toLocalTime() split it, and LocalDateTime.of(date, time) joins it.
Changing values: new objects
Every change method returns a new value. The original stays as it was.
LocalDate d = LocalDate.of(2026, 3, 15);
d.plusDays(30); // result discarded
System.out.println(d); // 2026-03-15 (unchanged)
LocalDate later = d.plusDays(30).minusMonths(1);The names follow a pattern: plusDays, plusWeeks, plusMonths, plusYears, and the minus... versions; plusHours, plusMinutes on times; and withYear, withHour to replace one field. You can chain them because each call returns a value you can call the next method on.
Comparing
a.isBefore(b); // true if a is earlier than b
a.isAfter(b); // true if a is later
a.equals(b); // true if the same date (value comparison)Use these instead of ==, which compares object references (BEG-026). LocalDate objects with the same date are equal even when they are different objects. Because LocalDate also implements Comparable, you can sort a list of dates with the tools from BEG-032.
Invalid dates and clamping
The library refuses dates that do not exist.
LocalDate.of(2026, 2, 30);
// DateTimeException: Invalid date 'FEBRUARY 30'DateTimeException is an unchecked exception (BEG-029), so the compiler does not force you to catch it. Catch it where the date comes from user input.
Arithmetic is different: it does not throw. When adding months would land on a day that does not exist, the library moves to the last valid day of the month. Its Javadoc gives this rule with an example for 31 March plus one month.
LocalDate.of(2026, 1, 31).plusMonths(1); // 2026-02-28
LocalDate.of(2024, 2, 29).plusYears(1); // 2025-02-28A subtle consequence: adding then subtracting is not always a round trip. 31 January plus one month is 28 February; minus one month is 28 January, not 31 January. If a requirement says "the same day each month" you must work from the original date each time (start.plusMonths(n)), not chain one-month steps.
LocalDate due = LocalDate.of(2026, 1, 31);
due.plusMonths(1); // 2026-02-28
due.plusMonths(2); // 2026-03-31What a LocalDateTime is not
2026-03-15T09:30 is not a moment. In Riyadh it is one moment; in New York, another one, several hours later. Until you attach a zone, nothing tells you which. That is perfectly fine for facts like "the shop opens at 09:30", and wrong for facts like "the order was placed at 09:30". Lesson 3 adds the zone. The rule of thumb for now:
- If the value describes a calendar or wall clock fact: local type.
- If it describes when something happened or will happen: you need a zone or an
Instant.
Examples
Example 1: a deadline reminder (simple)
LocalDate d = LocalDate.of(2026, 3, 15);
LocalTime t = LocalTime.of(9, 30);
LocalDateTime dt = LocalDateTime.of(d, t);
System.out.println(d + " | " + t + " | " + dt);
System.out.println(d.getDayOfWeek() + " " + d.getMonth() + " " + d.getDayOfYear());
LocalDate reminder = d.minusDays(7);
System.out.println(reminder + " " + reminder.isBefore(d));Output:
2026-03-15 | 09:30 | 2026-03-15T09:30
SUNDAY MARCH 74
2026-03-08 trueString concatenation calls toString() on each value, which produces the ISO text. The reminder is a new LocalDate; d is unchanged.
Example 2: immutability and chaining (practical)
LocalDate d = LocalDate.of(2026, 3, 15);
d.plusDays(30);
System.out.println("unchanged: " + d);
LocalDate later = d.plusDays(30).minusMonths(1);
System.out.println(later + " " + d.isBefore(later)
+ " " + d.equals(LocalDate.parse("2026-03-15")));
LocalTime t = LocalTime.of(9, 30);
LocalDateTime dt = LocalDateTime.of(d, t);
System.out.println(t.plusHours(16) + " " + dt.toLocalDate() + " " + dt.withHour(18));Output:
unchanged: 2026-03-15
2026-03-14 false true
01:30 2026-03-15 2026-03-15T18:30Read them: the discarded plusDays(30) changed nothing; 15 March plus 30 days is 14 April, minus one month is 14 March, which is before the original so d.isBefore(later) is false; two separate LocalDate objects with the same date are equals. LocalTime.of(9, 30).plusHours(16) wraps past midnight to 01:30; a LocalTime has no date, so it just wraps around.
Example 3: invalid input and clamping in an invoicing service (realistic)
An invoicing service creates monthly due dates from a customer's start date and reads dates typed by users.
LocalDate start = LocalDate.of(2026, 1, 31);
for (int n = 1; n <= 3; n++) {
System.out.println(start.plusMonths(n));
}
try {
LocalDate.of(2026, 2, 30);
} catch (DateTimeException e) {
System.out.println(e.getMessage());
}
try {
LocalDate.parse("15/03/2026");
} catch (java.time.format.DateTimeParseException e) {
System.out.println(e.getMessage());
}Output:
2026-02-28
2026-03-31
2026-04-30
Invalid date 'FEBRUARY 30'
Text '15/03/2026' could not be parsed at index 0Using start.plusMonths(n) each time keeps the target day as close to the 31st as the month allows. Chaining plusMonths(1) three times would drift to the 28th and stay there. The last line shows that parse without a formatter accepts only the ISO form; the slash format needs the formatter of Lesson 4. DateTimeParseException is the subclass thrown for unparseable text.
Backend Insight
Backend code often passes local dates through APIs and databases: a birth_date column, a due_date field in JSON. Local types are exactly right when the meaning is a calendar day. In JSON, a LocalDate normally travels as the ISO string 2026-03-15, which every language can read, so sticking to the ISO form makes your interfaces easy to use.
Two habits will save you from production bugs:
- Do not use
LocalDateTimeas a "created at" timestamp. It looks convenient, but two servers in different zones will write different values for the same moment. - Be careful with
now()inside business logic. Pass the date in as a parameter, so a test can supplyLocalDate.of(2026, 3, 15)and always get the same answer.
Common Mistakes
Mistake 1: using LocalDateTime for events
- Incorrect:
LocalDateTime createdAt = LocalDateTime.now();for a database row. - Correct: record the moment as an
Instant(Lesson 3).
LocalDateTime is a mistake here because it does not say which zone "now" refers to.
Mistake 2: forgetting to use the result
date.plusDays(1); alone does nothing. Write date = date.plusDays(1); or assign to a new variable.
Mistake 3: chaining month arithmetic for recurring dates
Stepping 31 January, 28 February, 28 March gives the wrong day. Compute each date from the start date with plusMonths(n).
Mistake 4: using == to compare dates
== compares references. Use equals, isBefore or isAfter.
Mistake 5: assuming zero-based months
LocalDate.of(2026, 3, 15) is March. Do not subtract 1.
Exercises
Try each exercise yourself before opening the solutions. Where code is involved, running it depends on your IDE or terminal.
Level A: Foundation
A1. Which local type fits each: (a) a customer's date of birth, (b) the time a library opens, (c) the date and time written on a paper form?
أظهر الحل A1
A1. (a) LocalDate. (b) LocalTime. (c) LocalDateTime, but only as a form value; once you know the user's zone you can combine it with that zone (Lesson 3).
A2. Write a statement that creates the date 15 March 2026 and another that creates a date 100 days later. What does the second print?
أظهر الحل A2
A2.
LocalDate d = LocalDate.of(2026, 3, 15);
LocalDate later = d.plusDays(100);
System.out.println(later); // 2026-06-23A3. What does this print, and why? LocalDate d = LocalDate.of(2026, 3, 15); d.minusDays(5); System.out.println(d);
أظهر الحل A3
A3. It prints 2026-03-15. minusDays(5) returned a new date that was thrown away, and d is immutable.
Level B: Application
B1. What is LocalDate.of(2026, 1, 31).plusMonths(1)? Why does it not throw?
أظهر الحل B1
B1. 2026-02-28. Arithmetic never creates an invalid date; when the target day does not exist the library chooses the last valid day of the month.
B2. Which day of the week is 20 November 2026? Write the expression.
أظهر الحل B2
B2. LocalDate.of(2026, 11, 20).getDayOfWeek() gives FRIDAY.
B3. What exception does LocalDate.of(2026, 2, 30) throw, and what does its message say?
أظهر الحل B3
B3. DateTimeException with the message Invalid date 'FEBRUARY 30'.
Level C: Challenge
C1. Write a method static LocalDate nextBirthday(LocalDate birthday, LocalDate today) that returns the first occurrence of the birthday's month and day that is not before today. Test it with birthday 20 November 1990 and today 21 November 2026, then with today 20 November 2026. (Ignore 29 February birthdays; explain in a comment what happens to them.)
أظهر الحل C1
C1.
static LocalDate nextBirthday(LocalDate birthday, LocalDate today) {
// A 29 February birthday is clamped to 28 February in non-leap years by withYear.
LocalDate thisYear = birthday.withYear(today.getYear());
return thisYear.isBefore(today) ? thisYear.plusYears(1) : thisYear;
}With today 21 November 2026 the result is 2027-11-20; with today 20 November 2026 the result is 2026-11-20 (today counts). Both were checked by running the code on JDK 21. For a 29 February birthday, withYear gives 28 February in a non-leap year, so a leap-day person is treated as 28 February; the rule can be changed if the business needs 1 March.
C2. A teammate stores the opening of a flash sale as LocalDateTime.of(2026, 3, 15, 9, 0) in a shared system used in Riyadh and New York. Explain the problem in two sentences.
أظهر الحل C2
C2. A LocalDateTime has no zone, so 09:00 is not a single moment: Riyadh users would see the sale start at one moment and New York users, reading the same value as their local 09:00, at another several hours later. The sale should be stored as an exact moment (an Instant, or a ZonedDateTime in a chosen zone) so every user converts the same moment for display.
Mini Challenge
Write InvoicePlan with a main method. Given a start date 2026-01-31, print the due dates for the first four months using start.plusMonths(n). Then print the day of the week of each due date.
Check your work:
- The dates are 28 Feb, 31 Mar, 30 Apr, 31 May (the real values for 2026).
- You never chained one-month steps.
- You can explain why one-month steps would have drifted to the 28th.
Bonus: add a method isWeekend(LocalDate) that compares getDayOfWeek() with the constants DayOfWeek.SATURDAY and DayOfWeek.SUNDAY. Constants are written like that in the JDK; you can compare them with == because they are enum constants (enums will be revisited later in the course).
Summary & Next Lesson
Key takeaways
LocalDate,LocalTimeandLocalDateTimedescribe calendar and wall-clock values with no zone.- Create with
of,now,parse; change withplus...,minus...,with...; each returns a new object. - Invalid dates throw
DateTimeException; arithmetic clamps to the last valid day of the month. - Compare with
isBefore,isAfter,equals, not==. - A
LocalDateTimeis not a moment; do not use it for events seen in several zones.
Next lesson
The next lesson, "Instants and time zones", introduces Instant, ZoneId and ZonedDateTime: how to name an exact moment and show it on any clock.
Self-check
- Does
LocalDateTimeknow its time zone? - What does
d.plusDays(1);do when you do not store the result? - What is 31 January 2026 plus one month?
- Which method compares two dates for "earlier than"?
- Why should
now()be avoided deep inside business logic?
أظهر الإجابات
- No.
- Nothing visible: it computes a new date and discards it.
- 28 February 2026.
isBefore.- Its result depends on the clock and zone of the machine, so the code is hard to test and may behave differently from server to server. Pass the date in as a parameter instead.
References
- Oracle, java.time.LocalDate (Java SE 21). Supports that
LocalDateholds no time or zone, is immutable, expects ISOyyyy-MM-ddinparse, and clamps to the last valid day inplusMonths. - Oracle, java.time package summary (Java SE 21). Supports the roles of
LocalDate,LocalTimeandLocalDateTime. - All printed outputs were produced by compiling and running the code on JDK 21. The statement that a
LocalDatenormally travels as an ISO string in JSON depends on the JSON library and was not checked here.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.