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 ChronoField and ChronoUnit for standard operations; create custom implementations only for domain-specific needs.
  • Validate field ranges: Always check ValueRange before 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.