20.1 Temporal Fields and Units
Temporal fields and units are the foundational components for querying and manipulating temporal values in Java. Understanding TemporalField, TemporalUnit, and ChronoField/ChronoUnit is essential for working with dates and times programmatically.
TemporalField Overview
TemporalField is an interface that represents a specific field within a temporal object, such as the day of month, month of year, or hour of day. It provides the contract for getting and setting field values on temporal objects.
// TemporalField basic usage
LocalDate date = LocalDate.of(2025, 1, 15);
// Get field value using TemporalField
int dayOfMonth = date.get(ChronoField.DAY_OF_MONTH); // 15
int monthValue = date.get(ChronoField.MONTH_OF_YEAR); // 1
int yearValue = date.get(ChronoField.YEAR); // 2025
System.out.println("Day: " + dayOfMonth);
System.out.println("Month: " + monthValue);
System.out.println("Year: " + yearValue);
// Set field value
LocalDate adjusted = date.with(ChronoField.DAY_OF_MONTH, 25);
System.out.println("Adjusted: " + adjusted); // 2025-01-25
ChronoField Reference
ChronoField is an enum implementing TemporalField that provides all standard calendar and clock fields. Each field has specific characteristics including minimum and maximum values, units, and range information.
// Common ChronoField values
LocalDateTime now = LocalDateTime.now();
// Year fields
int year = now.get(ChronoField.YEAR); // 2025
int weekBasedYear = now.get(ChronoField.WEEK_BASED_YEAR); // ISO week year
int yearOfEra = now.get(ChronoField.YEAR_OF_ERA); // Year within era (BC/AD)
// Month and day fields
int monthValue = now.get(ChronoField.MONTH_OF_YEAR); // 1-12
int dayOfMonth = now.get(ChronoField.DAY_OF_MONTH); // 1-31
int dayOfWeek = now.get(ChronoField.DAY_OF_WEEK); // 1=Monday, 7=Sunday
int dayOfYear = now.get(ChronoField.DAY_OF_YEAR); // 1-366
// Week fields
int weekOfWeekBasedYear = now.get(ChronoField.WEEK_OF_WEEK_BASED_YEAR); // 1-53
int weekOfMonth = now.get(ChronoField.WEEK_OF_MONTH); // 1-5
// Time fields
int hour = now.get(ChronoField.HOUR_OF_DAY); // 0-23
int minuteOfHour = now.get(ChronoField.MINUTE_OF_HOUR); // 0-59
int secondOfMinute = now.get(ChronoField.SECOND_OF_MINUTE); // 0-59
int nanoOfSecond = now.get(ChronoField.NANO_OF_SECOND); // 0-999999999
// Derived time fields
long secondOfDay = now.getLong(ChronoField.SECOND_OF_DAY);
long nanoOfDay = now.getLong(ChronoField.NANO_OF_DAY);
// AM/PM fields
int amPmOfDay = now.get(ChronoField.AMPM_OF_DAY); // 0=AM, 1=PM
int hourOfAmPm = now.get(ChronoField.HOUR_OF_AMPM); // 0-11
TemporalUnit Overview
TemporalUnit represents a standard length of time, such as seconds, days, or years. Units define how temporal quantities can be added to or subtracted from temporal objects.
// TemporalUnit basic operations
LocalDate date = LocalDate.of(2025, 1, 15);
LocalTime time = LocalTime.of(14, 30, 45);
// Add units to temporal objects
LocalDate nextWeek = date.plus(1, ChronoUnit.WEEKS);
LocalDate nextMonth = date.plus(1, ChronoUnit.MONTHS);
LocalDate nextYear = date.plus(1, ChronoUnit.YEARS);
LocalTime nextHour = time.plus(1, ChronoUnit.HOURS);
LocalTime nextMinute = time.plus(1, ChronoUnit.MINUTES);
// Subtract units
LocalDate lastQuarter = date.minus(3, ChronoUnit.MONTHS);
LocalTime twoHoursAgo = time.minus(2, ChronoUnit.HOURS);
// Calculate between dates
long daysBetween = ChronoUnit.DAYS.between(date, date.plusMonths(1));
long hoursBetween = ChronoUnit.HOURS.between(time, time.plusHours(5));
System.out.println("Days between: " + daysBetween);
System.out.println("Hours between: " + hoursBetween);
ChronoUnit Reference
ChronoUnit is an enum implementing TemporalUnit that provides all standard time units from nanoseconds to decades.
// ChronoUnit comprehensive reference
LocalDateTime base = LocalDateTime.of(2025, 1, 15, 14, 30, 45);
// Nanosecond to second units
LocalDateTime nanoAdded = base.plus(500_000_000, ChronoUnit.NANOS); // 0.5 seconds
LocalDateTime microAdded = base.plus(500_000, ChronoUnit.MICROS); // 0.5 seconds
LocalDateTime milliAdded = base.plus(500, ChronoUnit.MILLIS); // 0.5 seconds
LocalDateTime secondAdded = base.plus(30, ChronoUnit.SECONDS);
// Minute to day units
LocalDateTime minuteAdded = base.plus(30, ChronoUnit.MINUTES);
LocalDateTime hourAdded = base.plus(8, ChronoUnit.HOURS);
LocalDateTime dayAdded = base.plus(7, ChronoUnit.DAYS);
LocalDateTime weekAdded = base.plus(2, ChronoUnit.WEEKS);
// Month to decade units
LocalDateTime monthAdded = base.plus(6, ChronoUnit.MONTHS);
LocalDateTime yearAdded = base.plus(1, ChronoUnit.YEARS);
LocalDateTime decadeAdded = base.plus(1, ChronoUnit.DECADES); // 10 years
LocalDateTime centuryAdded = base.plus(1, ChronoUnit.CENTURIES); // 100 years
LocalDateTime millenniumAdded = base.plus(1, ChronoUnit.MILLENNIA); // 1000 years
// Unit duration and estimated duration
System.out.println("DAYS duration: " + ChronoUnit.DAYS.getDuration()); // PT24H
System.out.println("YEARS estimated duration: " + ChronoUnit.YEARS.getEstimatedDuration());
System.out.println("DAYS is date-based: " + ChronoUnit.DAYS.isDateBased());
System.out.println("HOURS is time-based: " + ChronoUnit.HOURS.isTimeBased());
Working with Field Ranges
Every temporal field has a range defining its minimum and maximum values. Understanding field ranges is critical for validation and calculations.
// Field range queries
LocalDate date = LocalDate.of(2025, 2, 28); // February 2025
// Get field ranges
ValueRange dayRange = ChronoField.DAY_OF_MONTH.rangeRefinedBy(date);
System.out.println("Days in February 2025: " + dayRange); // 1-28
// Check field support
boolean hasField = date.isSupported(ChronoField.YEAR);
System.out.println("LocalDate supports YEAR: " + hasField); // true
// Validate against range before setting
int proposedDay = 30;
if (ChronoField.DAY_OF_MONTH.rangeRefinedBy(date).isValidValue(proposedDay)) {
date = date.with(ChronoField.DAY_OF_MONTH, proposedDay);
} else {
System.out.println("Invalid day for " + date.getMonth());
}
// Get max value for field in temporal object
int maxDay = (int) ChronoField.DAY_OF_MONTH.rangeRefinedBy(date).getMaximum();
LocalDate lastDay = date.with(ChronoField.DAY_OF_MONTH, maxDay);
System.out.println("Last day of month: " + lastDay); // 2025-02-28
Advanced Field Queries
Custom field implementations and complex queries enable sophisticated temporal reasoning.
// Implementing custom TemporalField
class QuarterField implements TemporalField {
@Override
public String getDisplayName(TextStyle style, Locale locale) {
return style == TextStyle.FULL ? "Quarter" : "Q";
}
@Override
public TemporalUnit getBaseUnit() {
return ChronoUnit.MONTHS;
}
@Override
public TemporalUnit getRangeUnit() {
return ChronoUnit.YEARS;
}
@Override
public ValueRange range() {
return ValueRange.of(1, 4);
}
@Override
public boolean isDateBased() {
return true;
}
@Override
public boolean isTimeBased() {
return false;
}
@Override
public long getFrom(TemporalAccessor temporal) {
int month = temporal.get(ChronoField.MONTH_OF_YEAR);
return (month - 1) / 3 + 1; // Q1-Q4
}
@Override
public <R extends Temporal> R adjustInto(R temporal, long newValue) {
long currentQuarter = getFrom(temporal);
long monthDiff = (newValue - currentQuarter) * 3;
return (R) temporal.plus(monthDiff, ChronoUnit.MONTHS);
}
}
// Using custom field
QuarterField quarterField = new QuarterField();
LocalDate date = LocalDate.of(2025, 3, 15);
int quarter = date.get(quarterField); // 1
LocalDate q2Start = date.with(quarterField, 2); // First month of Q2 (April)
System.out.println("Q2 start: " + q2Start); // 2025-04-15
TemporalUnit Implementation
Creating custom temporal units for domain-specific time concepts.
// Custom TemporalUnit implementation
class BusinessDayUnit implements TemporalUnit {
private Set<LocalDate> holidays;
public BusinessDayUnit(Set<LocalDate> holidays) {
this.holidays = holidays;
}
@Override
public Duration getDuration() {
return Duration.ofHours(8); // Typical business day
}
@Override
public boolean isDurationEstimated() {
return true;
}
@Override
public boolean isDateBased() {
return true;
}
@Override
public boolean isTimeBased() {
return false;
}
@Override
public <R extends Temporal> R addTo(R temporal, long amount) {
if (!(temporal instanceof LocalDate)) {
throw new UnsupportedTemporalTypeException("Only LocalDate supported");
}
LocalDate date = (LocalDate) temporal;
long businessDaysToAdd = amount;
while (businessDaysToAdd > 0) {
date = date.plusDays(1);
if (!isWeekendOrHoliday(date)) {
businessDaysToAdd--;
}
}
return (R) date;
}
private boolean isWeekendOrHoliday(LocalDate date) {
DayOfWeek dow = date.getDayOfWeek();
return dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY ||
holidays.contains(date);
}
@Override
public long between(Temporal temporal1Inclusive, Temporal temporal2Exclusive) {
LocalDate start = LocalDate.from(temporal1Inclusive);
LocalDate end = LocalDate.from(temporal2Exclusive);
long count = 0;
LocalDate current = start;
while (current.isBefore(end)) {
if (!isWeekendOrHoliday(current)) {
count++;
}
current = current.plusDays(1);
}
return count;
}
@Override
public String toString() {
return "BusinessDays";
}
}
// Using custom unit
Set<LocalDate> holidays = Set.of(
LocalDate.of(2025, 1, 1), // New Year
LocalDate.of(2025, 12, 25) // Christmas
);
BusinessDayUnit bdUnit = new BusinessDayUnit(holidays);
LocalDate workStart = LocalDate.of(2025, 1, 6); // Monday
LocalDate deadline = workStart.plus(5, bdUnit); // 5 business days later
// Count business days between dates
LocalDate projectEnd = LocalDate.of(2025, 2, 15);
long businessDays = bdUnit.between(workStart, projectEnd);
System.out.println("Business days: " + businessDays);
Field and Unit Utilities
Practical helper methods for working with fields and units.
// Utility methods for field operations
class TemporalFieldUtils {
// Check if temporal supports field
public static boolean supportsField(Temporal temporal, TemporalField field) {
try {
temporal.get(field);
return true;
} catch (UnsupportedTemporalTypeException e) {
return false;
}
}
// Safely get field value with default
public static int getFieldOrDefault(Temporal temporal, TemporalField field, int defaultValue) {
try {
return temporal.get(field);
} catch (UnsupportedTemporalTypeException e) {
return defaultValue;
}
}
// Check if value is valid for temporal and field
public static boolean isValidFieldValue(Temporal temporal, TemporalField field, int value) {
try {
ValueRange range = field.rangeRefinedBy(temporal);
return range.isValidValue(value);
} catch (UnsupportedTemporalTypeException e) {
return false;
}
}
// Get all fields for temporal object
public static List<TemporalField> getSupportedFields(Temporal temporal) {
return Arrays.stream(ChronoField.values())
.filter(field -> temporal.isSupported(field))
.collect(Collectors.toList());
}
}
// Using utilities
LocalDate date = LocalDate.now();
int year = TemporalFieldUtils.getFieldOrDefault(date, ChronoField.YEAR, 2000);
boolean isValid = TemporalFieldUtils.isValidFieldValue(date, ChronoField.DAY_OF_MONTH, 31);
List<TemporalField> fields = TemporalFieldUtils.getSupportedFields(date);
Best Practices
- Choose the right field/unit: Use
ChronoFieldandChronoUnitfor standard operations; create custom implementations only for domain-specific needs. - Validate field ranges: Always check
ValueRangebefore setting field values to prevent silent errors. - Prefer methods to fields: Use convenience methods like
plusDays()instead of manually manipulating fields when possible. - Handle date-based vs time-based: Understand the distinction between date-based and time-based units for correct arithmetic.
- Cache custom implementations: Custom field and unit instances are expensive; reuse singleton instances.
- Test edge cases: Leap years, month-end dates, and DST transitions require special attention.