19.2 Date and Time Operations

This section covers practical operations with LocalDate, LocalTime, LocalDateTime, ZonedDateTime, and OffsetDateTime, including creation, manipulation, conversion, and common patterns for date-time calculations.

LocalDate: Date Without Time

LocalDate represents a date without time or timezone information (year-month-day).

Creating LocalDate:

public class LocalDateCreation {
    public void factoryMethods() {
        // Current date in default timezone
        LocalDate today = LocalDate.now();

        // Current date in specific timezone
        LocalDate tokyoToday = LocalDate.now(ZoneId.of("Asia/Tokyo"));

        // Specific date
        LocalDate date1 = LocalDate.of(2025, 1, 6);
        LocalDate date2 = LocalDate.of(2025, Month.JANUARY, 6);

        // From year and day of year
        LocalDate dayOfYear = LocalDate.ofYearDay(2025, 100); // 100th day of 2025

        // From epoch day (days since 1970-01-01)
        LocalDate fromEpoch = LocalDate.ofEpochDay(19723); // 2024-01-01

        // Parsing from string
        LocalDate parsed = LocalDate.parse("2025-01-06");
        LocalDate parsedCustom = LocalDate.parse("06/01/2025", 
            DateTimeFormatter.ofPattern("dd/MM/yyyy"));

        System.out.println("Today: " + today);
        System.out.println("Parsed: " + parsed);
    }
}

LocalDate Operations:

public class LocalDateOperations {
    public void fieldAccess() {
        LocalDate date = LocalDate.of(2025, 1, 6);

        // Get individual fields
        int year = date.getYear();
        Month month = date.getMonth();
        int monthValue = date.getMonthValue(); // 1-12
        int dayOfMonth = date.getDayOfMonth();
        DayOfWeek dayOfWeek = date.getDayOfWeek();
        int dayOfYear = date.getDayOfYear();

        // Derived information
        boolean isLeapYear = date.isLeapYear();
        int lengthOfMonth = date.lengthOfMonth();
        int lengthOfYear = date.lengthOfYear();

        System.out.println(date + " is a " + dayOfWeek);
        System.out.println("Leap year: " + isLeapYear);
    }

    public void arithmetic() {
        LocalDate date = LocalDate.of(2025, 1, 15);

        // Add/subtract days, weeks, months, years
        LocalDate tomorrow = date.plusDays(1);
        LocalDate nextWeek = date.plusWeeks(1);
        LocalDate nextMonth = date.plusMonths(1);
        LocalDate nextYear = date.plusYears(1);

        LocalDate yesterday = date.minusDays(1);
        LocalDate lastWeek = date.minusWeeks(1);
        LocalDate lastMonth = date.minusMonths(1);
        LocalDate lastYear = date.minusYears(1);

        // Generic plus/minus with Period
        LocalDate inThreeMonths = date.plus(Period.ofMonths(3));
        LocalDate twoWeeksAgo = date.minus(Period.ofWeeks(2));

        // With method - replace specific field
        LocalDate firstOfMonth = date.withDayOfMonth(1);
        LocalDate firstOfYear = date.withDayOfYear(1);
        LocalDate sameMonth2026 = date.withYear(2026);

        System.out.println("Tomorrow: " + tomorrow);
        System.out.println("First of month: " + firstOfMonth);
    }

    public void comparisons() {
        LocalDate date1 = LocalDate.of(2025, 1, 1);
        LocalDate date2 = LocalDate.of(2025, 12, 31);

        // Comparison methods
        boolean isBefore = date1.isBefore(date2);
        boolean isAfter = date1.isAfter(date2);
        boolean isEqual = date1.isEqual(date2);

        // Comparable interface
        int comparison = date1.compareTo(date2); // negative if before

        // Calculate period between dates
        Period period = Period.between(date1, date2);
        System.out.println("Period: " + period.getMonths() + " months, " + 
                         period.getDays() + " days");

        // Calculate days between
        long daysBetween = ChronoUnit.DAYS.between(date1, date2);
        System.out.println("Days between: " + daysBetween);
    }
}

Common LocalDate Patterns:

public class LocalDatePatterns {
    public LocalDate getEndOfMonth(LocalDate date) {
        return date.withDayOfMonth(date.lengthOfMonth());
    }

    public LocalDate getStartOfMonth(LocalDate date) {
        return date.withDayOfMonth(1);
    }

    public LocalDate getEndOfYear(LocalDate date) {
        return LocalDate.of(date.getYear(), 12, 31);
    }

    public boolean isWeekend(LocalDate date) {
        DayOfWeek day = date.getDayOfWeek();
        return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
    }

    public LocalDate getNextBusinessDay(LocalDate date) {
        LocalDate next = date.plusDays(1);
        while (isWeekend(next)) {
            next = next.plusDays(1);
        }
        return next;
    }

    public List<LocalDate> getDateRange(LocalDate start, LocalDate end) {
        List<LocalDate> dates = new ArrayList<>();
        LocalDate current = start;

        while (!current.isAfter(end)) {
            dates.add(current);
            current = current.plusDays(1);
        }

        return dates;
    }

    public int getQuarter(LocalDate date) {
        return (date.getMonthValue() - 1) / 3 + 1;
    }

    public LocalDate addBusinessDays(LocalDate date, int businessDays) {
        LocalDate result = date;
        int added = 0;

        while (added < businessDays) {
            result = result.plusDays(1);
            if (!isWeekend(result)) {
                added++;
            }
        }

        return result;
    }
}

LocalTime: Time Without Date

LocalTime represents a time without date or timezone information (hour-minute-second-nanosecond).

Creating LocalTime:

public class LocalTimeCreation {
    public void factoryMethods() {
        // Current time
        LocalTime now = LocalTime.now();

        // Specific time
        LocalTime time1 = LocalTime.of(14, 30); // 14:30
        LocalTime time2 = LocalTime.of(14, 30, 45); // 14:30:45
        LocalTime time3 = LocalTime.of(14, 30, 45, 123456789); // with nanos

        // From seconds of day
        LocalTime fromSeconds = LocalTime.ofSecondOfDay(52200); // 14:30:00

        // From nanos of day
        LocalTime fromNanos = LocalTime.ofNanoOfDay(52200000000000L);

        // Parsing
        LocalTime parsed = LocalTime.parse("14:30:45");
        LocalTime parsedCustom = LocalTime.parse("2:30 PM", 
            DateTimeFormatter.ofPattern("h:mm a"));

        // Constants
        LocalTime midnight = LocalTime.MIDNIGHT; // 00:00
        LocalTime noon = LocalTime.NOON; // 12:00
        LocalTime minTime = LocalTime.MIN; // 00:00
        LocalTime maxTime = LocalTime.MAX; // 23:59:59.999999999

        System.out.println("Now: " + now);
    }
}

LocalTime Operations:

public class LocalTimeOperations {
    public void fieldAccess() {
        LocalTime time = LocalTime.of(14, 30, 45, 123456789);

        // Get fields
        int hour = time.getHour(); // 0-23
        int minute = time.getMinute(); // 0-59
        int second = time.getSecond(); // 0-59
        int nano = time.getNano(); // 0-999,999,999

        // Convert to totals
        long secondOfDay = time.toSecondOfDay();
        long nanoOfDay = time.toNanoOfDay();

        System.out.println("Second of day: " + secondOfDay);
    }

    public void arithmetic() {
        LocalTime time = LocalTime.of(14, 30);

        // Add/subtract time units
        LocalTime plus1Hour = time.plusHours(1);
        LocalTime plus30Min = time.plusMinutes(30);
        LocalTime plus45Sec = time.plusSeconds(45);
        LocalTime plusNanos = time.plusNanos(1000000);

        LocalTime minus2Hours = time.minusHours(2);
        LocalTime minus15Min = time.minusMinutes(15);

        // Wraps around midnight
        LocalTime late = LocalTime.of(23, 30);
        LocalTime afterMidnight = late.plusHours(2); // 01:30
        System.out.println("23:30 + 2 hours = " + afterMidnight);

        // With method
        LocalTime newHour = time.withHour(16);
        LocalTime newMinute = time.withMinute(45);
        LocalTime truncated = time.truncatedTo(ChronoUnit.MINUTES); // 14:30:00
    }

    public void comparisons() {
        LocalTime time1 = LocalTime.of(9, 0);
        LocalTime time2 = LocalTime.of(17, 0);

        boolean isBefore = time1.isBefore(time2);
        boolean isAfter = time1.isAfter(time2);

        // Duration between times
        Duration duration = Duration.between(time1, time2);
        System.out.println("Work hours: " + duration.toHours() + " hours");
    }
}

Common LocalTime Patterns:

public class LocalTimePatterns {
    public boolean isBusinessHours(LocalTime time) {
        LocalTime start = LocalTime.of(9, 0);
        LocalTime end = LocalTime.of(17, 0);
        return !time.isBefore(start) && time.isBefore(end);
    }

    public LocalTime roundToNearestMinute(LocalTime time) {
        int seconds = time.getSecond();
        if (seconds >= 30) {
            return time.plusMinutes(1).truncatedTo(ChronoUnit.MINUTES);
        } else {
            return time.truncatedTo(ChronoUnit.MINUTES);
        }
    }

    public LocalTime roundToNearest15Minutes(LocalTime time) {
        int minutes = time.getMinute();
        int remainder = minutes % 15;

        if (remainder < 8) {
            return time.withMinute(minutes - remainder).truncatedTo(ChronoUnit.MINUTES);
        } else {
            return time.withMinute(minutes + (15 - remainder)).truncatedTo(ChronoUnit.MINUTES);
        }
    }
}

LocalDateTime: Date and Time Without Timezone

LocalDateTime combines LocalDate and LocalTime without timezone information.

Creating LocalDateTime:

public class LocalDateTimeCreation {
    public void factoryMethods() {
        // Current date-time
        LocalDateTime now = LocalDateTime.now();

        // From components
        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, 123456789);

        // From date and time
        LocalDate date = LocalDate.of(2025, 1, 6);
        LocalTime time = LocalTime.of(14, 30);
        LocalDateTime combined = LocalDateTime.of(date, time);

        // Convenience methods on LocalDate/LocalTime
        LocalDateTime atTime = date.atTime(14, 30);
        LocalDateTime atStartOfDay = date.atStartOfDay();
        LocalDateTime atDate = time.atDate(date);

        // Parsing
        LocalDateTime parsed = LocalDateTime.parse("2025-01-06T14:30:45");

        System.out.println("Now: " + now);
    }
}

LocalDateTime Operations:

public class LocalDateTimeOperations {
    public void fieldAccess() {
        LocalDateTime dt = LocalDateTime.of(2025, 1, 6, 14, 30, 45);

        // Access date components
        LocalDate date = dt.toLocalDate();
        int year = dt.getYear();
        Month month = dt.getMonth();
        int dayOfMonth = dt.getDayOfMonth();

        // Access time components
        LocalTime time = dt.toLocalTime();
        int hour = dt.getHour();
        int minute = dt.getMinute();
        int second = dt.getSecond();

        // Derived information
        DayOfWeek dayOfWeek = dt.getDayOfWeek();
        int dayOfYear = dt.getDayOfYear();

        System.out.println("Date: " + date + ", Time: " + time);
    }

    public void arithmetic() {
        LocalDateTime dt = LocalDateTime.of(2025, 1, 6, 14, 30);

        // Date arithmetic
        LocalDateTime tomorrow = dt.plusDays(1);
        LocalDateTime nextMonth = dt.plusMonths(1);
        LocalDateTime nextYear = dt.plusYears(1);

        // Time arithmetic
        LocalDateTime plus2Hours = dt.plusHours(2);
        LocalDateTime plus30Min = dt.plusMinutes(30);
        LocalDateTime plus45Sec = dt.plusSeconds(45);

        // Combined arithmetic
        LocalDateTime later = dt.plus(Duration.ofHours(2).plusMinutes(30));
        LocalDateTime muchLater = dt.plus(Period.ofMonths(3).plusDays(15));

        // With methods
        LocalDateTime newDate = dt.withYear(2026).withMonth(7);
        LocalDateTime newTime = dt.withHour(9).withMinute(0);

        System.out.println("Original: " + dt);
        System.out.println("Tomorrow same time: " + tomorrow);
    }

    public void conversions() {
        LocalDateTime dt = LocalDateTime.now();

        // Extract date and time
        LocalDate date = dt.toLocalDate();
        LocalTime time = dt.toLocalTime();

        // Convert to zoned date-time
        ZoneId zone = ZoneId.of("America/New_York");
        ZonedDateTime zoned = dt.atZone(zone);

        // Convert to offset date-time
        ZoneOffset offset = ZoneOffset.of("-05:00");
        OffsetDateTime offsetDt = dt.atOffset(offset);

        System.out.println("LocalDateTime: " + dt);
        System.out.println("ZonedDateTime: " + zoned);
        System.out.println("OffsetDateTime: " + offsetDt);
    }
}

ZonedDateTime: Date-Time with Timezone

ZonedDateTime represents a date-time with timezone, including DST rules.

Creating ZonedDateTime:

public class ZonedDateTimeCreation {
    public void factoryMethods() {
        // Current zoned date-time
        ZonedDateTime now = ZonedDateTime.now();
        ZonedDateTime nyNow = ZonedDateTime.now(ZoneId.of("America/New_York"));

        // From components
        ZoneId zone = ZoneId.of("Europe/Paris");
        ZonedDateTime zdt1 = ZonedDateTime.of(2025, 1, 6, 14, 30, 0, 0, zone);
        ZonedDateTime zdt2 = ZonedDateTime.of(
            LocalDate.of(2025, 1, 6),
            LocalTime.of(14, 30),
            zone
        );

        // From LocalDateTime
        LocalDateTime ldt = LocalDateTime.of(2025, 1, 6, 14, 30);
        ZonedDateTime zdt3 = ldt.atZone(zone);

        // From Instant
        Instant instant = Instant.now();
        ZonedDateTime fromInstant = instant.atZone(zone);

        // Parsing
        ZonedDateTime parsed = ZonedDateTime.parse("2025-01-06T14:30:00+01:00[Europe/Paris]");

        System.out.println("Now: " + now);
        System.out.println("Paris: " + zdt1);
    }

    public void ambiguousTimeHandling() {
        // DST transitions can create ambiguous or invalid local times
        ZoneId newYork = ZoneId.of("America/New_York");

        // Spring forward: 2:00 AM becomes 3:00 AM (gap)
        LocalDateTime springForward = LocalDateTime.of(2025, 3, 9, 2, 30);
        ZonedDateTime resolved = springForward.atZone(newYork);
        System.out.println("Spring forward: " + springForward + " -> " + resolved);

        // Fall back: 2:00 AM occurs twice (overlap)
        LocalDateTime fallBack = LocalDateTime.of(2025, 11, 2, 1, 30);
        ZonedDateTime firstOccurrence = fallBack.atZone(newYork);
        System.out.println("Fall back: " + fallBack + " -> " + firstOccurrence);

        // Use ofStrict to detect ambiguity
        try {
            ZonedDateTime strict = ZonedDateTime.ofStrict(
                springForward,
                ZoneOffset.of("-05:00"),
                newYork
            );
        } catch (DateTimeException e) {
            System.out.println("Invalid time during DST gap");
        }
    }
}

ZonedDateTime Operations:

public class ZonedDateTimeOperations {
    public void timezoneConversions() {
        ZoneId newYork = ZoneId.of("America/New_York");
        ZoneId tokyo = ZoneId.of("Asia/Tokyo");
        ZoneId london = ZoneId.of("Europe/London");

        // Create a meeting time in New York
        ZonedDateTime nyMeeting = ZonedDateTime.of(
            2025, 1, 6, 14, 0, 0, 0, newYork
        );

        // Convert to other timezones (same instant, different local time)
        ZonedDateTime tokyoMeeting = nyMeeting.withZoneSameInstant(tokyo);
        ZonedDateTime londonMeeting = nyMeeting.withZoneSameInstant(london);

        System.out.println("New York: " + nyMeeting);
        System.out.println("Tokyo: " + tokyoMeeting);
        System.out.println("London: " + londonMeeting);

        // All represent the same instant
        Instant instant = nyMeeting.toInstant();
        System.out.println("All meetings at instant: " + instant);
    }

    public void withZoneSameLocal() {
        ZoneId newYork = ZoneId.of("America/New_York");
        ZoneId london = ZoneId.of("Europe/London");

        ZonedDateTime nyTime = ZonedDateTime.of(
            2025, 1, 6, 14, 0, 0, 0, newYork
        );

        // Keep local time but change zone (different instant!)
        ZonedDateTime londonSameLocal = nyTime.withZoneSameLocal(london);

        System.out.println("NY 14:00: " + nyTime);
        System.out.println("London 14:00: " + londonSameLocal);

        // These are different instants
        Duration difference = Duration.between(
            nyTime.toInstant(),
            londonSameLocal.toInstant()
        );
        System.out.println("Time difference: " + difference.toHours() + " hours");
    }

    public void offsetInformation() {
        ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("America/New_York"));

        // Get current offset from UTC
        ZoneOffset offset = zdt.getOffset();
        int totalSeconds = offset.getTotalSeconds();

        System.out.println("Current offset: " + offset);
        System.out.println("Total seconds: " + totalSeconds);

        // Check if currently in DST
        ZoneRules rules = zdt.getZone().getRules();
        boolean isDst = rules.isDaylightSavings(zdt.toInstant());
        System.out.println("In DST: " + isDst);
    }
}

OffsetDateTime: Date-Time with Fixed Offset

OffsetDateTime represents a date-time with a fixed UTC offset, without timezone rules.

Creating OffsetDateTime:

public class OffsetDateTimeCreation {
    public void factoryMethods() {
        // Current offset date-time
        OffsetDateTime now = OffsetDateTime.now();
        OffsetDateTime utcNow = OffsetDateTime.now(ZoneOffset.UTC);

        // From components
        ZoneOffset offset = ZoneOffset.of("+05:30");
        OffsetDateTime odt1 = OffsetDateTime.of(2025, 1, 6, 14, 30, 0, 0, offset);
        OffsetDateTime odt2 = OffsetDateTime.of(
            LocalDate.of(2025, 1, 6),
            LocalTime.of(14, 30),
            offset
        );

        // From LocalDateTime
        LocalDateTime ldt = LocalDateTime.of(2025, 1, 6, 14, 30);
        OffsetDateTime odt3 = ldt.atOffset(offset);

        // From Instant
        Instant instant = Instant.now();
        OffsetDateTime fromInstant = instant.atOffset(offset);

        // Parsing
        OffsetDateTime parsed = OffsetDateTime.parse("2025-01-06T14:30:00+05:30");

        System.out.println("Now: " + now);
        System.out.println("With +05:30: " + odt1);
    }

    public void conversionExamples() {
        OffsetDateTime odt = OffsetDateTime.now();

        // Convert to other types
        ZonedDateTime zdt = odt.toZonedDateTime();
        LocalDateTime ldt = odt.toLocalDateTime();
        LocalDate date = odt.toLocalDate();
        LocalTime time = odt.toLocalTime();
        Instant instant = odt.toInstant();
        OffsetTime offsetTime = odt.toOffsetTime();

        // Change offset (adjusts local time to maintain instant)
        ZoneOffset newOffset = ZoneOffset.of("+08:00");
        OffsetDateTime differentOffset = odt.withOffsetSameInstant(newOffset);

        System.out.println("Original: " + odt);
        System.out.println("Different offset: " + differentOffset);
    }
}

Choosing the Right Type

Decision Matrix:

public class TypeSelection {
    public void storageRecommendations() {
        // For database storage: Use Instant
        Instant timestamp = Instant.now();
        // Store as: BIGINT (epoch seconds) or TIMESTAMP WITH TIME ZONE

        // For user display: Convert to ZonedDateTime
        ZoneId userZone = ZoneId.of("America/New_York");
        ZonedDateTime displayTime = timestamp.atZone(userZone);

        // For API communication: Use OffsetDateTime (ISO-8601)
        OffsetDateTime apiTime = OffsetDateTime.now();
        String json = String.format("{\"timestamp\":\"%s\"}", apiTime);
    }

    public void useCaseExamples() {
        // Use LocalDate for: birthdays, holidays, due dates
        LocalDate birthday = LocalDate.of(1990, 5, 15);
        LocalDate dueDate = LocalDate.now().plusDays(30);

        // Use LocalTime for: daily schedules, opening hours
        LocalTime openingTime = LocalTime.of(9, 0);
        LocalTime closingTime = LocalTime.of(17, 0);

        // Use LocalDateTime for: log timestamps (same timezone system)
        LocalDateTime logEntry = LocalDateTime.now();

        // Use ZonedDateTime for: user appointments, meetings across zones
        ZonedDateTime meeting = ZonedDateTime.of(
            2025, 1, 15, 14, 0, 0, 0,
            ZoneId.of("America/New_York")
        );

        // Use Instant for: event timestamps, audit trails, database storage
        Instant eventTime = Instant.now();

        // Use OffsetDateTime for: REST API, JSON serialization
        OffsetDateTime apiTimestamp = OffsetDateTime.now();
    }
}

Summary

Date and time operations in Java provide:

  • LocalDate: Date-only operations (birthdays, due dates, holidays)
  • LocalTime: Time-only operations (schedules, opening hours)
  • LocalDateTime: Combined date-time without timezone (logs, local events)
  • ZonedDateTime: Timezone-aware operations (meetings, appointments with DST)
  • OffsetDateTime: Fixed offset operations (API timestamps, serialization)
  • Type Selection: Choose based on storage, display, and timezone requirements

Understanding these types enables correct temporal handling across all application scenarios.