انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-033-L02

التواريخ والأوقات المحلية

  • الدرس 2 من 5
  • 11 دقيقة قراءة
  • مبتدئ
في هذا الدرس 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, LocalTime and LocalDateTime for a requirement.
  • Create them with of(...), now() and parse(...).
  • Read parts of a value with methods such as getYear() and getDayOfWeek().
  • Change values with plusDays, minusMonths and similar methods, remembering that each returns a new object.
  • Compare two local values with isBefore, isAfter and equals.
  • Explain what happens with invalid dates and with "31 January plus one month".
  • Explain why a LocalDateTime is not a specific moment.

Written Explanation

The three local types

TypeHoldsExample textGood for
LocalDateyear, month, day2026-03-15birthdays, due dates, holidays
LocalTimehour, minute, second, nanosecond09:30opening hours, a daily alarm time
LocalDateTimea date and a time2026-03-15T09:30a 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.

java
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 legacy Date.
  • now() reads the system clock. The answer depends on the machine's default zone, which is why tests that call now() are hard to repeat.
  • parse(text) reads text in the standard ISO form yyyy-MM-dd for dates, HH:mm for times, and yyyy-MM-ddTHH:mm for date-times. Lesson 4 covers other formats.

Printing a value with println shows the same ISO form:

text
2026-03-15 | 09:30 | 2026-03-15T09:30

Reading parts

java
d.getYear();          // 2026
d.getMonthValue();    // 3
d.getDayOfMonth();    // 15
d.getDayOfWeek();     // SUNDAY
d.getDayOfYear();     // 74

getDayOfWeek() 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.

java
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

java
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.

java
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.

java
LocalDate.of(2026, 1, 31).plusMonths(1);   // 2026-02-28
LocalDate.of(2024, 2, 29).plusYears(1);    // 2025-02-28

A 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.

java
LocalDate due = LocalDate.of(2026, 1, 31);
due.plusMonths(1);   // 2026-02-28
due.plusMonths(2);   // 2026-03-31

What 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)

java
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:

text
2026-03-15 | 09:30 | 2026-03-15T09:30
SUNDAY MARCH 74
2026-03-08 true

String 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)

java
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:

text
unchanged: 2026-03-15
2026-03-14 false true
01:30 2026-03-15 2026-03-15T18:30

Read 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.

java
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:

text
2026-02-28
2026-03-31
2026-04-30
Invalid date 'FEBRUARY 30'
Text '15/03/2026' could not be parsed at index 0

Using 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 LocalDateTime as 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 supply LocalDate.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.

java
LocalDate d = LocalDate.of(2026, 3, 15);
LocalDate later = d.plusDays(100);
System.out.println(later);       // 2026-06-23

A3. 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.

java
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:

  1. The dates are 28 Feb, 31 Mar, 30 Apr, 31 May (the real values for 2026).
  2. You never chained one-month steps.
  3. 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, LocalTime and LocalDateTime describe calendar and wall-clock values with no zone.
  • Create with of, now, parse; change with plus..., 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 LocalDateTime is 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

  1. Does LocalDateTime know its time zone?
  2. What does d.plusDays(1); do when you do not store the result?
  3. What is 31 January 2026 plus one month?
  4. Which method compares two dates for "earlier than"?
  5. Why should now() be avoided deep inside business logic?
أظهر الإجابات
  1. No.
  2. Nothing visible: it computes a new date and discards it.
  3. 28 February 2026.
  4. isBefore.
  5. 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 LocalDate holds no time or zone, is immutable, expects ISO yyyy-MM-dd in parse, and clamps to the last valid day in plusMonths.
  • Oracle, java.time package summary (Java SE 21). Supports the roles of LocalDate, LocalTime and LocalDateTime.
  • All printed outputs were produced by compiling and running the code on JDK 21. The statement that a LocalDate normally travels as an ISO string in JSON depends on the JSON library and was not checked here.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.