Skip to main content

Time Package

The org.apache.juneau.commons.time package provides a precision-aware date/time type for granular time operations. It currently contains a single class, GranularZonedDateTime.

Granular Date/Time​

GranularZonedDateTime​

GranularZonedDateTime pairs a java.time.ZonedDateTime with a java.time.temporal.ChronoField precision that records how fine-grained the value is. A value parsed from "2011" is a year-precision value, one parsed from "2011-01-15T12:30" is minute-precision, and so on. The precision then determines what "one step forward" means when you roll the value.

The class is immutable and thread safe. It exposes two public final fields, zdt and precision, and matching getters getZonedDateTime() and getPrecision().

// Parse a partial timestamp - precision is inferred from the input.
GranularZonedDateTime g = GranularZonedDateTime.of("2026-07"); // MONTH_OF_YEAR precision

ZonedDateTime zdt = g.getZonedDateTime(); // 2026-07-01T00:00 in the default zone
ChronoField precision = g.getPrecision(); // ChronoField.MONTH_OF_YEAR

Creating instances​

MethodDescription
of(String)Parse an ISO 8601 timestamp.
of(String, Clock)Parse, taking the zone and the "current date" (for time-only input) from the given Clock.
of(String, ZoneId, Clock)Parse with a default zone used only when the string has no offset, plus a Clock.
of(ZonedDateTime, ChronoField)Wrap an existing ZonedDateTime with an explicit precision.
of(Date, ChronoField)Convert a java.util.Date using the system default zone.
of(Date, ChronoField, ZoneId)Convert a java.util.Date using the given zone.
new GranularZonedDateTime(ZonedDateTime, ChronoField)Public constructor.
copy()Returns a new instance with the same values.
// From an existing ZonedDateTime at an explicit precision.
GranularZonedDateTime day = GranularZonedDateTime.of(ZonedDateTime.now(), ChronoField.DAY_OF_MONTH);

// From a Date in a specific zone.
GranularZonedDateTime fromDate = GranularZonedDateTime.of(new Date(), ChronoField.SECOND_OF_MINUTE, ZoneId.of("UTC"));

The ZonedDateTime and Date factories do not validate the precision; a null precision is stored as-is and only fails later (for example in roll(int)). A null Date or ZoneId throws NullPointerException.

Parsing ISO 8601 strings​

of(String) accepts these forms, and infers the precision from the finest component present:

InputPrecision
2011YEAR
2011-01MONTH_OF_YEAR
2011-01-15DAY_OF_MONTH
2011-01-15T12HOUR_OF_DAY
2011-01-15T12:30MINUTE_OF_HOUR
2011-01-15T12:30:45SECOND_OF_MINUTE
2011-01-15T12:30:45.123 (1-3 fractional digits)MILLI_OF_SECOND
2011-01-15T12:30:45.123456789 (4-9 fractional digits)NANO_OF_SECOND

Additional rules:

  • The year is exactly four digits. Missing date components default to 01 (month and day) and missing time components default to 0.
  • The fractional-seconds separator may be . or ,.
  • A time zone may follow any time component: Z, +HH, +HH:mm or +HHmm (and the - equivalents). An offset in the string always wins. When the string has no offset, the zone is the defaultZoneId argument if given, otherwise the Clock's zone (the system default zone when no clock is given).
  • A string that begins with T (T12, T12:30, T12:30:45) is time-only. The date is taken from the current date according to the Clock in the resolved zone.
  • A null string throws IllegalArgumentException. A malformed string throws java.time.format.DateTimeParseException.
// Explicit UTC offset in the string.
GranularZonedDateTime a = GranularZonedDateTime.of("2011-01-15T12:30:45Z");

// No offset - use New York as the default zone.
GranularZonedDateTime b = GranularZonedDateTime.of("2011-01-15T12:30:45", ZoneId.of("America/New_York"), null);

// Time-only with a fixed clock for deterministic results (useful in tests).
Clock clock = Clock.fixed(Instant.parse("2000-01-01T12:00:00Z"), ZoneOffset.UTC);
GranularZonedDateTime c = GranularZonedDateTime.of("T12:30:45", clock); // 2000-01-01T12:30:45Z

Rolling​

roll(int amount) returns a new instance moved by amount units of the value's own precision (negative moves backward). roll(ChronoField field, int amount) moves by amount units of an explicit field instead. In both cases the precision of the result is unchanged.

GranularZonedDateTime year = GranularZonedDateTime.of("2011");
GranularZonedDateTime next = year.roll(1); // 2012-01-01T00:00, still YEAR precision

GranularZonedDateTime hour = GranularZonedDateTime.of("2011-01-15T12Z");
GranularZonedDateTime later = hour.roll(ChronoField.HOUR_OF_DAY, 2); // 2011-01-15T14:00Z, still HOUR_OF_DAY precision

The supported roll fields are YEAR, MONTH_OF_YEAR, DAY_OF_MONTH, HOUR_OF_DAY, MINUTE_OF_HOUR, SECOND_OF_MINUTE, MILLI_OF_SECOND and NANO_OF_SECOND. Any other ChronoField makes roll(ChronoField, int) throw IllegalArgumentException. Because the underlying operation is ZonedDateTime.plus(amount, unit), the normal java.time rules apply (for example month-end clamping and daylight-saving transitions).

Rolling by one unit of the precision is a convenient way to compute the exclusive end of the period a partial timestamp denotes:

GranularZonedDateTime start = GranularZonedDateTime.of("2026-07");    // 2026-07-01T00:00
GranularZonedDateTime end = start.roll(1); // 2026-08-01T00:00 (exclusive end of July)

toString()​

toString() formats the value as ISO_OFFSET_DATE_TIME followed by the precision in parentheses, for example 2011-01-15T12:00:00Z(HourOfDay). It is intended for diagnostics, not for round-tripping.

See Also​