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.