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,
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
| Method | Description |
|---|---|
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:
| Input | Precision |
|---|---|
2011 | YEAR |
2011-01 | MONTH_OF_YEAR |
2011-01-15 | DAY_OF_MONTH |
2011-01-15T12 | HOUR_OF_DAY |
2011-01-15T12:30 | MINUTE_OF_HOUR |
2011-01-15T12:30:45 | SECOND_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 to0. - The fractional-seconds separator may be
.or,. - A time zone may follow any time component:
Z,+HH,+HH:mmor+HHmm(and the-equivalents). An offset in the string always wins. When the string has no offset, the zone is thedefaultZoneIdargument if given, otherwise theClock'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 theClockin the resolved zone. - A
nullstring throwsIllegalArgumentException. A malformed string throwsjava.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.