19.3 Temporal Adjusters and Queries

This section covers advanced temporal manipulation using TemporalAdjusters, TemporalQueries, and custom implementations for complex date-time calculations.

TemporalAdjuster Interface

TemporalAdjuster provides a strategy for adjusting temporal objects through the adjustInto method.

Basic Adjusters:

public class BasicTemporalAdjusters {
    public void predefinedAdjusters() {
        LocalDate date = LocalDate.of(2025, 1, 15);

        // First/Last day adjusters
        LocalDate firstDay = date.with(TemporalAdjusters.firstDayOfMonth());
        LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth());
        LocalDate firstDayOfYear = date.with(TemporalAdjusters.firstDayOfYear());
        LocalDate lastDayOfYear = date.with(TemporalAdjusters.lastDayOfYear());
        LocalDate firstDayOfNextMonth = date.with(TemporalAdjusters.firstDayOfNextMonth());
        LocalDate firstDayOfNextYear = date.with(TemporalAdjusters.firstDayOfNextYear());

        System.out.println("First day of month: " + firstDay);
        System.out.println("Last day of month: " + lastDay);
        System.out.println("First day of next month: " + firstDayOfNextMonth);
    }

    public void dayOfWeekAdjusters() {
        LocalDate date = LocalDate.of(2025, 1, 15); // Wednesday

        // First/Last occurrence in month
        LocalDate firstMonday = date.with(
            TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)
        );
        LocalDate lastFriday = date.with(
            TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)
        );

        // Nth day of week in month (e.g., 2nd Tuesday)
        LocalDate secondTuesday = date.with(
            TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.TUESDAY)
        );

        // Next/Previous occurrence
        LocalDate nextMonday = date.with(
            TemporalAdjusters.next(DayOfWeek.MONDAY)
        );
        LocalDate previousFriday = date.with(
            TemporalAdjusters.previous(DayOfWeek.FRIDAY)
        );

        // Next or same
        LocalDate nextOrSameMonday = date.with(
            TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY)
        );
        LocalDate previousOrSameFriday = date.with(
            TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY)
        );

        System.out.println("Next Monday: " + nextMonday);
        System.out.println("Second Tuesday: " + secondTuesday);
    }
}

Custom TemporalAdjusters

Create custom adjusters for domain-specific date logic.

Simple Custom Adjusters:

public class CustomTemporalAdjusters {
    // Adjust to next business day (skip weekends)
    public static TemporalAdjuster nextBusinessDay() {
        return temporal -> {
            DayOfWeek dow = DayOfWeek.from(temporal);
            int daysToAdd = switch (dow) {
                case FRIDAY -> 3;
                case SATURDAY -> 2;
                default -> 1;
            };
            return temporal.plus(daysToAdd, ChronoUnit.DAYS);
        };
    }

    // Adjust to previous business day
    public static TemporalAdjuster previousBusinessDay() {
        return temporal -> {
            DayOfWeek dow = DayOfWeek.from(temporal);
            int daysToSubtract = switch (dow) {
                case MONDAY -> 3;
                case SUNDAY -> 2;
                default -> 1;
            };
            return temporal.minus(daysToSubtract, ChronoUnit.DAYS);
        };
    }

    // Adjust to end of quarter
    public static TemporalAdjuster endOfQuarter() {
        return temporal -> {
            int month = temporal.get(ChronoField.MONTH_OF_YEAR);
            int quarterEndMonth = ((month - 1) / 3 + 1) * 3;
            return temporal
                .with(ChronoField.MONTH_OF_YEAR, quarterEndMonth)
                .with(TemporalAdjusters.lastDayOfMonth());
        };
    }

    // Adjust to start of quarter
    public static TemporalAdjuster startOfQuarter() {
        return temporal -> {
            int month = temporal.get(ChronoField.MONTH_OF_YEAR);
            int quarterStartMonth = ((month - 1) / 3) * 3 + 1;
            return temporal
                .with(ChronoField.MONTH_OF_YEAR, quarterStartMonth)
                .with(TemporalAdjusters.firstDayOfMonth());
        };
    }

    // Usage examples
    public void demonstrateCustomAdjusters() {
        LocalDate date = LocalDate.of(2025, 1, 17); // Friday

        LocalDate nextBizDay = date.with(nextBusinessDay());
        System.out.println("Next business day from Friday: " + nextBizDay); // Monday

        LocalDate endQ1 = date.with(endOfQuarter());
        System.out.println("End of Q1 2025: " + endQ1); // 2025-03-31

        LocalDate startQ2 = endQ1.plusDays(1).with(startOfQuarter());
        System.out.println("Start of Q2 2025: " + startQ2); // 2025-04-01
    }
}

Complex Custom Adjusters:

public class AdvancedCustomAdjusters {
    // Adjust to Nth business day of month
    public static TemporalAdjuster nthBusinessDayOfMonth(int n) {
        return temporal -> {
            LocalDate date = LocalDate.from(temporal);
            LocalDate firstDay = date.with(TemporalAdjusters.firstDayOfMonth());

            int businessDaysFound = 0;
            LocalDate current = firstDay;

            while (businessDaysFound < n) {
                if (isBusinessDay(current)) {
                    businessDaysFound++;
                    if (businessDaysFound == n) {
                        return current;
                    }
                }
                current = current.plusDays(1);
            }

            return current;
        };
    }

    private static boolean isBusinessDay(LocalDate date) {
        DayOfWeek dow = date.getDayOfWeek();
        return dow != DayOfWeek.SATURDAY && dow != DayOfWeek.SUNDAY;
    }

    // Adjust to last business day of month
    public static TemporalAdjuster lastBusinessDayOfMonth() {
        return temporal -> {
            LocalDate lastDay = LocalDate.from(temporal)
                .with(TemporalAdjusters.lastDayOfMonth());

            while (!isBusinessDay(lastDay)) {
                lastDay = lastDay.minusDays(1);
            }

            return lastDay;
        };
    }

    // Adjust to specific day respecting holidays
    public static TemporalAdjuster nextBusinessDayAvoidingHolidays(
            Set<LocalDate> holidays) {
        return temporal -> {
            LocalDate date = LocalDate.from(temporal).plusDays(1);

            while (!isBusinessDay(date) || holidays.contains(date)) {
                date = date.plusDays(1);
            }

            return date;
        };
    }

    // Adjust to Easter Sunday (Computus algorithm)
    public static TemporalAdjuster easterSunday() {
        return temporal -> {
            int year = temporal.get(ChronoField.YEAR);

            int a = year % 19;
            int b = year / 100;
            int c = year % 100;
            int d = b / 4;
            int e = b % 4;
            int f = (b + 8) / 25;
            int g = (b - f + 1) / 3;
            int h = (19 * a + b - d - g + 15) % 30;
            int i = c / 4;
            int k = c % 4;
            int l = (32 + 2 * e + 2 * i - h - k) % 7;
            int m = (a + 11 * h + 22 * l) / 451;
            int month = (h + l - 7 * m + 114) / 31;
            int day = ((h + l - 7 * m + 114) % 31) + 1;

            return LocalDate.of(year, month, day);
        };
    }

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

        // Get 15th business day of month
        LocalDate day15 = date.with(nthBusinessDayOfMonth(15));
        System.out.println("15th business day of Jan 2025: " + day15);

        // Get last business day
        LocalDate lastBizDay = date.with(lastBusinessDayOfMonth());
        System.out.println("Last business day of Jan 2025: " + lastBizDay);

        // Get Easter Sunday
        LocalDate easter = date.with(easterSunday());
        System.out.println("Easter 2025: " + easter);
    }
}

TemporalQuery Interface

TemporalQuery extracts information from temporal objects.

Predefined Queries:

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

        // Extract components using queries
        LocalDate date = zdt.query(TemporalQueries.localDate());
        LocalTime time = zdt.query(TemporalQueries.localTime());
        ZoneId zone = zdt.query(TemporalQueries.zone());
        ZoneOffset offset = zdt.query(TemporalQueries.offset());
        ChronoLocalDate chronoDate = zdt.query(TemporalQueries.chronology());

        // Precision query
        TemporalUnit precision = zdt.query(TemporalQueries.precision());

        System.out.println("Date: " + date);
        System.out.println("Time: " + time);
        System.out.println("Zone: " + zone);
        System.out.println("Offset: " + offset);
    }
}

Custom TemporalQuery:

public class CustomTemporalQueries {
    // Query to check if date is a weekend
    public static TemporalQuery<Boolean> isWeekend() {
        return temporal -> {
            DayOfWeek dow = DayOfWeek.from(temporal);
            return dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY;
        };
    }

    // Query to get quarter number
    public static TemporalQuery<Integer> quarter() {
        return temporal -> {
            int month = temporal.get(ChronoField.MONTH_OF_YEAR);
            return (month - 1) / 3 + 1;
        };
    }

    // Query to get week of year
    public static TemporalQuery<Integer> weekOfYear() {
        return temporal -> {
            return temporal.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR);
        };
    }

    // Query to check if date is in leap year
    public static TemporalQuery<Boolean> isLeapYear() {
        return temporal -> {
            int year = temporal.get(ChronoField.YEAR);
            return Year.of(year).isLeap();
        };
    }

    // Query to get business days until end of month
    public static TemporalQuery<Long> businessDaysUntilEndOfMonth() {
        return temporal -> {
            LocalDate date = LocalDate.from(temporal);
            LocalDate endOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());

            long businessDays = 0;
            LocalDate current = date;

            while (!current.isAfter(endOfMonth)) {
                if (isBusinessDay(current)) {
                    businessDays++;
                }
                current = current.plusDays(1);
            }

            return businessDays;
        };
    }

    private static boolean isBusinessDay(LocalDate date) {
        DayOfWeek dow = date.getDayOfWeek();
        return dow != DayOfWeek.SATURDAY && dow != DayOfWeek.SUNDAY;
    }

    // Query to get days until specific day of week
    public static TemporalQuery<Long> daysUntil(DayOfWeek targetDay) {
        return temporal -> {
            DayOfWeek current = DayOfWeek.from(temporal);
            int currentValue = current.getValue();
            int targetValue = targetDay.getValue();

            if (targetValue > currentValue) {
                return (long) (targetValue - currentValue);
            } else {
                return (long) (7 - currentValue + targetValue);
            }
        };
    }

    public void demonstrateCustomQueries() {
        LocalDate date = LocalDate.of(2025, 1, 17); // Friday

        Boolean isWeekend = date.query(isWeekend());
        System.out.println("Is weekend: " + isWeekend); // false

        Integer quarter = date.query(quarter());
        System.out.println("Quarter: " + quarter); // 1

        Boolean isLeap = date.query(isLeapYear());
        System.out.println("Is leap year: " + isLeap); // false

        Long businessDaysLeft = date.query(businessDaysUntilEndOfMonth());
        System.out.println("Business days until end of month: " + businessDaysLeft);

        Long daysUntilMonday = date.query(daysUntil(DayOfWeek.MONDAY));
        System.out.println("Days until Monday: " + daysUntilMonday); // 3
    }
}

Combining Adjusters and Queries

Create powerful date-time logic by combining adjusters and queries.

Business Day Calculator:

public class BusinessDayCalculator {
    private final Set<LocalDate> holidays;

    public BusinessDayCalculator(Set<LocalDate> holidays) {
        this.holidays = holidays;
    }

    public boolean isBusinessDay(LocalDate date) {
        DayOfWeek dow = date.getDayOfWeek();
        return dow != DayOfWeek.SATURDAY && 
               dow != DayOfWeek.SUNDAY && 
               !holidays.contains(date);
    }

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

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

        return result;
    }

    public LocalDate subtractBusinessDays(LocalDate startDate, int businessDays) {
        LocalDate result = startDate;
        int subtracted = 0;

        while (subtracted < businessDays) {
            result = result.minusDays(1);
            if (isBusinessDay(result)) {
                subtracted++;
            }
        }

        return result;
    }

    public long countBusinessDays(LocalDate start, LocalDate end) {
        long count = 0;
        LocalDate current = start;

        while (!current.isAfter(end)) {
            if (isBusinessDay(current)) {
                count++;
            }
            current = current.plusDays(1);
        }

        return count;
    }

    // Custom adjuster that respects holidays
    public TemporalAdjuster nextBusinessDay() {
        return temporal -> {
            LocalDate date = LocalDate.from(temporal).plusDays(1);
            while (!isBusinessDay(date)) {
                date = date.plusDays(1);
            }
            return date;
        };
    }

    // Custom query for business days remaining in month
    public TemporalQuery<Long> businessDaysRemainingInMonth() {
        return temporal -> {
            LocalDate date = LocalDate.from(temporal);
            LocalDate endOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());
            return countBusinessDays(date, endOfMonth);
        };
    }
}

Fiscal Calendar Support:

public class FiscalCalendar {
    private final Month fiscalYearStartMonth;

    public FiscalCalendar(Month fiscalYearStartMonth) {
        this.fiscalYearStartMonth = fiscalYearStartMonth;
    }

    // Adjuster to get fiscal year start
    public TemporalAdjuster fiscalYearStart() {
        return temporal -> {
            LocalDate date = LocalDate.from(temporal);
            int calendarYear = date.getYear();
            int fiscalStartMonth = fiscalYearStartMonth.getValue();
            int currentMonth = date.getMonthValue();

            if (currentMonth < fiscalStartMonth) {
                calendarYear--;
            }

            return LocalDate.of(calendarYear, fiscalStartMonth, 1);
        };
    }

    // Adjuster to get fiscal year end
    public TemporalAdjuster fiscalYearEnd() {
        return temporal -> {
            LocalDate fyStart = LocalDate.from(temporal).with(fiscalYearStart());
            return fyStart.plusYears(1).minusDays(1);
        };
    }

    // Query to get fiscal quarter
    public TemporalQuery<Integer> fiscalQuarter() {
        return temporal -> {
            LocalDate date = LocalDate.from(temporal);
            LocalDate fyStart = date.with(fiscalYearStart());

            long monthsFromStart = ChronoUnit.MONTHS.between(fyStart, date);
            return (int) (monthsFromStart / 3) + 1;
        };
    }

    // Query to get fiscal year
    public TemporalQuery<Integer> fiscalYear() {
        return temporal -> {
            LocalDate date = LocalDate.from(temporal);
            LocalDate fyStart = date.with(fiscalYearStart());
            return fyStart.getYear();
        };
    }

    public void demonstrateFiscalCalendar() {
        // Fiscal year starts in April
        FiscalCalendar fiscal = new FiscalCalendar(Month.APRIL);

        LocalDate date = LocalDate.of(2025, 1, 15);

        LocalDate fyStart = date.with(fiscal.fiscalYearStart());
        LocalDate fyEnd = date.with(fiscal.fiscalYearEnd());
        Integer quarter = date.query(fiscal.fiscalQuarter());
        Integer year = date.query(fiscal.fiscalYear());

        System.out.println("Date: " + date);
        System.out.println("Fiscal year: " + year);
        System.out.println("Fiscal quarter: " + quarter);
        System.out.println("FY start: " + fyStart);
        System.out.println("FY end: " + fyEnd);
    }
}

ISO Week Date Support

Support for ISO week date system using IsoFields.

ISO Week Operations:

public class IsoWeekOperations {
    public void isoWeekBasics() {
        LocalDate date = LocalDate.of(2025, 1, 15);

        // Get ISO week fields
        int weekBasedYear = date.get(IsoFields.WEEK_BASED_YEAR);
        int weekOfYear = date.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR);
        int dayOfWeek = date.get(ChronoField.DAY_OF_WEEK);

        System.out.println("Week-based year: " + weekBasedYear);
        System.out.println("Week of year: " + weekOfYear);
        System.out.println("Day of week: " + dayOfWeek);
    }

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

        // Adjust to start of ISO week (Monday)
        LocalDate weekStart = date.with(ChronoField.DAY_OF_WEEK, 1);

        // Adjust to end of ISO week (Sunday)
        LocalDate weekEnd = date.with(ChronoField.DAY_OF_WEEK, 7);

        // Move to specific week of year
        LocalDate week10 = date.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 10);

        System.out.println("Week start: " + weekStart);
        System.out.println("Week end: " + weekEnd);
        System.out.println("Week 10: " + week10);
    }

    public TemporalAdjuster startOfIsoWeek() {
        return temporal -> temporal.with(ChronoField.DAY_OF_WEEK, 1);
    }

    public TemporalAdjuster endOfIsoWeek() {
        return temporal -> temporal.with(ChronoField.DAY_OF_WEEK, 7);
    }

    public TemporalQuery<LocalDate> isoWeekStart() {
        return temporal -> {
            LocalDate date = LocalDate.from(temporal);
            return date.with(startOfIsoWeek());
        };
    }
}

Summary

Temporal adjusters and queries provide:

  • Predefined Adjusters: First/last day, day of week operations
  • Custom Adjusters: Business days, quarters, fiscal calendars, holidays
  • Predefined Queries: Extract date, time, zone, offset components
  • Custom Queries: Weekend detection, quarter calculation, business day counts
  • Complex Logic: Combining adjusters and queries for sophisticated calculations
  • ISO Week Support: Week-based year operations

These tools enable expressing complex temporal logic in clean, reusable, testable patterns.