20.2 Temporal Arithmetic and Calculations
Temporal arithmetic is the process of adding, subtracting, and calculating differences between temporal values. Mastering these operations enables building complex date-time logic for real-world applications.
Basic Addition and Subtraction
The simplest form of temporal arithmetic involves adding or subtracting units from temporal objects.
// Basic addition operations
LocalDate startDate = LocalDate.of(2025, 1, 15);
LocalTime startTime = LocalTime.of(14, 30, 45);
// Addition using plus methods
LocalDate nextDay = startDate.plusDays(1);
LocalDate nextWeek = startDate.plusWeeks(1);
LocalDate nextMonth = startDate.plusMonths(1);
LocalDate nextYear = startDate.plusYears(1);
LocalTime hourLater = startTime.plusHours(1);
LocalTime minuteLater = startTime.plusMinutes(30);
LocalTime secondLater = startTime.plusSeconds(45);
LocalTime nanoLater = startTime.plusNanos(1_000_000); // 1 millisecond
// Subtraction using minus methods
LocalDate previousDay = startDate.minusDays(1);
LocalDate previousMonth = startDate.minusMonths(3);
LocalTime hourEarlier = startTime.minusHours(2);
// Chaining operations
LocalDateTime adjusted = LocalDateTime.of(startDate, startTime)
.plusDays(7)
.plusHours(2)
.minusMinutes(15);
System.out.println("Adjusted: " + adjusted);
Temporal Arithmetic with Units
Using ChronoUnit for more flexible and generalized temporal operations.
// Generic temporal operations using ChronoUnit
LocalDate date = LocalDate.of(2025, 1, 15);
LocalDateTime dateTime = LocalDateTime.now();
// Add/subtract any ChronoUnit
LocalDate future = date.plus(10, ChronoUnit.DAYS);
LocalDate past = date.minus(2, ChronoUnit.MONTHS);
LocalDateTime futureTime = dateTime.plus(5, ChronoUnit.HOURS);
LocalDateTime pastTime = dateTime.minus(30, ChronoUnit.MINUTES);
// Working with different precision units
Instant now = Instant.now();
Instant nanoAdded = now.plus(500_000_000, ChronoUnit.NANOS);
Instant microAdded = now.plus(100, ChronoUnit.MICROS);
Instant milliAdded = now.plus(1000, ChronoUnit.MILLIS);
// Duration-based arithmetic (semantic vs literal)
Duration d = Duration.ofHours(25);
LocalDateTime startOfDay = LocalDateTime.of(2025, 1, 15, 0, 0);
LocalDateTime after25Hours = startOfDay.plus(d);
System.out.println("After 25 hours: " + after25Hours); // Next day 01:00
Calculating Differences Between Temporals
Computing the amount of time between two temporal values is essential for duration calculations, aging, and date-based business logic.
// Basic temporal differences
LocalDate startDate = LocalDate.of(2025, 1, 1);
LocalDate endDate = LocalDate.of(2025, 12, 31);
// Using Period for date-based differences
Period period = Period.between(startDate, endDate);
System.out.println("Years: " + period.getYears()); // 0
System.out.println("Months: " + period.getMonths()); // 11
System.out.println("Days: " + period.getDays()); // 30
System.out.println("Total days (approximate): " + period.toTotalMonths() / 12.0);
// Using ChronoUnit for specific units
long daysBetween = ChronoUnit.DAYS.between(startDate, endDate);
long weeksBetween = ChronoUnit.WEEKS.between(startDate, endDate);
long monthsBetween = ChronoUnit.MONTHS.between(startDate, endDate);
long yearsBetween = ChronoUnit.YEARS.between(startDate, endDate);
System.out.println("Days between: " + daysBetween); // 364
System.out.println("Weeks between: " + weeksBetween); // 52
System.out.println("Months between: " + monthsBetween); // 11
System.out.println("Years between: " + yearsBetween); // 0
// Time-based differences using Duration
LocalTime start = LocalTime.of(9, 30);
LocalTime end = LocalTime.of(17, 45);
Duration duration = Duration.between(start, end);
System.out.println("Duration: " + duration); // PT8H15M
long hoursBetween = ChronoUnit.HOURS.between(start, end);
long minutesBetween = ChronoUnit.MINUTES.between(start, end);
System.out.println("Hours: " + hoursBetween); // 8
System.out.println("Minutes: " + minutesBetween); // 495
Age and Tenure Calculations
Real-world scenarios often require calculating age or tenure based on current date.
// Age calculation
LocalDate birthDate = LocalDate.of(1990, 5, 15);
LocalDate today = LocalDate.now();
Period ageInPeriod = Period.between(birthDate, today);
int years = ageInPeriod.getYears();
int months = ageInPeriod.getMonths();
int days = ageInPeriod.getDays();
System.out.printf("Age: %d years, %d months, %d days%n", years, months, days);
// Alternative: Years between for simple age
int simpleAge = (int) ChronoUnit.YEARS.between(birthDate, today);
System.out.println("Simple age: " + simpleAge);
// Check if birthday is today
int daysUntilBirthday = calculateDaysUntilBirthday(birthDate);
System.out.println("Days until next birthday: " + daysUntilBirthday);
// Anniversary check
LocalDate hireDate = LocalDate.of(2020, 3, 1);
int yearsOfService = (int) ChronoUnit.YEARS.between(hireDate, today);
System.out.println("Years of service: " + yearsOfService);
Business Hours and Duration Calculations
Practical calculations excluding weekends and business hours.
// Count business hours between two times
LocalDateTime startTime = LocalDateTime.of(2025, 1, 15, 9, 0);
LocalDateTime endTime = LocalDateTime.of(2025, 1, 17, 17, 0);
long businessHours = calculateBusinessHours(startTime, endTime);
System.out.println("Business hours: " + businessHours); // 22 hours (2.75 days)
static long calculateBusinessHours(LocalDateTime start, LocalDateTime end) {
long hours = 0;
LocalDateTime current = start;
while (current.isBefore(end)) {
DayOfWeek dow = current.getDayOfWeek();
int hour = current.getHour();
// Business hours: 9 AM to 5 PM, Monday to Friday
if (dow != DayOfWeek.SATURDAY && dow != DayOfWeek.SUNDAY &&
hour >= 9 && hour < 17) {
LocalDateTime nextHour = current.plusHours(1);
LocalDateTime businessDay = LocalDateTime.of(
current.toLocalDate(), LocalTime.of(17, 0)
);
if (nextHour.isAfter(businessDay)) {
// Jump to next business day 9 AM
current = current.toLocalDate().plusDays(1)
.atTime(9, 0);
} else {
hours++;
current = nextHour;
}
} else {
current = current.plusHours(1);
}
}
return hours;
}
// Calculate delivery date (add business days)
LocalDate orderDate = LocalDate.of(2025, 1, 15); // Wednesday
LocalDate deliveryDate = addBusinessDays(orderDate, 3);
System.out.println("Delivery by: " + deliveryDate); // 2025-01-20 (Monday)
static LocalDate addBusinessDays(LocalDate start, int days) {
LocalDate current = start;
int added = 0;
while (added < days) {
current = current.plusDays(1);
if (current.getDayOfWeek() != DayOfWeek.SATURDAY &&
current.getDayOfWeek() != DayOfWeek.SUNDAY) {
added++;
}
}
return current;
}
Working with Month and Year Boundaries
Operations that interact with calendar boundaries require careful handling.
// Month end calculations
LocalDate date = LocalDate.of(2025, 2, 15); // February 15
// Last day of current month
LocalDate lastDayOfMonth = date.withDayOfMonth(1)
.plusMonths(1)
.minusDays(1);
System.out.println("Last day of month: " + lastDayOfMonth); // 2025-02-28
// Days remaining in month
int daysInMonth = date.lengthOfMonth();
int daysRemaining = daysInMonth - date.getDayOfMonth();
System.out.println("Days remaining: " + daysRemaining); // 13 (28 - 15)
// Next month same day (handles end-of-month)
LocalDate nextMonthDate = date.plusMonths(1);
if (nextMonthDate.getDayOfMonth() != date.getDayOfMonth()) {
// Day doesn't exist in next month (Jan 31 + 1 month = Feb 28/29)
nextMonthDate = nextMonthDate.withDayOfMonth(1).minusDays(1);
}
System.out.println("Next month same day: " + nextMonthDate);
// Fiscal year calculations (July 1 based)
LocalDate fiscalYearStart = LocalDate.of(2024, 7, 1);
LocalDate today = LocalDate.now();
LocalDate currentFYStart = today.getMonthValue() >= 7 ?
LocalDate.of(today.getYear(), 7, 1) :
LocalDate.of(today.getYear() - 1, 7, 1);
long daysIntoFiscalYear = ChronoUnit.DAYS.between(currentFYStart, today);
System.out.println("Days into fiscal year: " + daysIntoFiscalYear);
Period Normalization and Operations
Period handles complex date-based calculations with support for years, months, and days.
// Period construction and normalization
Period p1 = Period.of(1, 14, 45); // 1 year, 14 months, 45 days
Period normalized = p1.normalized(); // 2 years, 2 months, 15 days
System.out.println("Original: " + p1);
System.out.println("Normalized: " + normalized);
// Period arithmetic
Period p2 = Period.of(2, 3, 5);
Period sum = p1.plus(p2); // Add two periods
System.out.println("Sum: " + sum);
// Negative periods (subtraction)
Period negative = p1.negated();
System.out.println("Negated: " + negative);
// Multiplying periods
Period doubled = p1.multipliedBy(2);
System.out.println("Doubled: " + doubled);
// Working with total months
long totalMonths = p1.toTotalMonths(); // Years * 12 + months
System.out.println("Total months: " + totalMonths); // 26 (1*12 + 14)
// Zero period handling
Period zero = Period.ZERO;
System.out.println("Is zero: " + zero.isZero()); // true
System.out.println("Is negative: " + zero.isNegative()); // false
Duration Operations and Conversions
Duration handles precise time-based calculations with nanosecond precision.
// Duration construction and operations
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 30);
Duration workday = Duration.between(start, end); // 8 hours 30 minutes
// Arithmetic operations
Duration overtime = workday.plusHours(2);
Duration shortDay = workday.minusMinutes(15);
System.out.println("Workday: " + workday); // PT8H30M
System.out.println("With overtime: " + overtime); // PT10H30M
// Conversions to different units
long seconds = workday.getSeconds(); // 30600
long millis = workday.toMillis(); // 30600000
long minutes = workday.toMinutes(); // 510
long hours = workday.toHours(); // 8
// Duration multiplication and division
Duration half = workday.dividedBy(2); // 4 hours 15 minutes
Duration triple = workday.multipliedBy(3); // 25 hours 30 minutes
System.out.println("Half: " + half);
System.out.println("Triple: " + triple);
// Absolute value
Duration negativeDuration = Duration.ofHours(-5);
Duration absolute = negativeDuration.abs();
System.out.println("Absolute: " + absolute); // PT5H
// Comparison
Duration duration1 = Duration.ofMinutes(90);
Duration duration2 = Duration.ofHours(1);
int comparison = duration1.compareTo(duration2);
System.out.println("90 minutes > 1 hour: " + (comparison > 0)); // true
Practical Business Logic Examples
Real-world scenarios combining multiple arithmetic operations.
// Subscription renewal calculation
class SubscriptionManager {
LocalDate getNextRenewalDate(LocalDate subscriptionStart, int billingMonths) {
return subscriptionStart.plus(billingMonths, ChronoUnit.MONTHS);
}
int calculateDaysUntilRenewal(LocalDate subscriptionStart, int billingMonths) {
LocalDate renewalDate = getNextRenewalDate(subscriptionStart, billingMonths);
return (int) ChronoUnit.DAYS.between(LocalDate.now(), renewalDate);
}
boolean isRenewalDueSoon(LocalDate subscriptionStart, int billingMonths, int daysThreshold) {
int daysUntilRenewal = calculateDaysUntilRenewal(subscriptionStart, billingMonths);
return daysUntilRenewal <= daysThreshold && daysUntilRenewal > 0;
}
}
// Project timeline calculations
class ProjectSchedule {
LocalDate calculateDeadline(LocalDate startDate, int weeksDuration) {
return startDate.plus(weeksDuration, ChronoUnit.WEEKS);
}
int getWeeksRemaining(LocalDate startDate, int weeksDuration) {
LocalDate deadline = calculateDeadline(startDate, weeksDuration);
long daysRemaining = ChronoUnit.DAYS.between(LocalDate.now(), deadline);
return (int) (daysRemaining / 7); // Approximate weeks
}
boolean isOnSchedule(LocalDate startDate, int weeksDuration, double completionPercent) {
LocalDate deadline = calculateDeadline(startDate, weeksDuration);
long totalDays = ChronoUnit.DAYS.between(startDate, deadline);
long elapsedDays = ChronoUnit.DAYS.between(startDate, LocalDate.now());
double expectedProgress = (double) elapsedDays / totalDays;
return completionPercent >= expectedProgress * 100;
}
}
// Insurance and policy date calculations
class PolicyManager {
LocalDate calculatePolicyExpiry(LocalDate effectiveDate, int yearsOfCoverage) {
return effectiveDate.plus(yearsOfCoverage, ChronoUnit.YEARS);
}
boolean isPolicyExpired(LocalDate expireDate) {
return LocalDate.now().isAfter(expireDate);
}
int getDaysUntilExpiry(LocalDate expireDate) {
return (int) ChronoUnit.DAYS.between(LocalDate.now(), expireDate);
}
boolean shouldRenew(LocalDate expireDate, int renewalWarningDays) {
int daysUntilExpiry = getDaysUntilExpiry(expireDate);
return daysUntilExpiry > 0 && daysUntilExpiry <= renewalWarningDays;
}
}
Best Practices
- Use specific plus/minus methods for clarity:
plusDays()is clearer thanplus(1, ChronoUnit.DAYS). - Prefer ChronoUnit for flexible calculations: When the unit is variable or determined at runtime.
- Use Period for date arithmetic: For calculations spanning months and years.
- Use Duration for time calculations: For precise, sub-day time operations.
- Handle calendar boundaries carefully: Month-end and year-end dates require special attention.
- Test leap years and DST transitions: Edge cases are critical for date-time logic.
- Cache calculated dates in business logic: Avoid recalculating dates in loops.
- Validate date ranges: Ensure intermediate calculations don't exceed valid ranges.