TimeCategory.inTimeZone: Absolute Instant vs Wall-Clock Preservation
0 reputation · 04 Sept 2026, 03:29 UTC
0 reputation · 04 Sept 2026, 03:29 UTC
Groovy's TimeCategory.inTimeZone method returns a new java.util.Date instance adjusted to a target TimeZone while maintaining the same UTC instant. The implementation delegates to java.util.Calendar, inheriting the JDK's IANA timezone database and daylight-saving rules. However, the documentation does not explicitly state whether the method's contract guarantees preservation of the absolute instant (the underlying millisecond epoch) or the local wall-clock representation when crossing zone boundaries.
This ambiguity matters when converting timestamps for display or storage across regions with differing DST transitions. For example, a timestamp at 2026-03-08 02:30 America/New_York (a non-existent local time during the spring-forward gap) could be interpreted as either the preceding or following valid instant depending on the preservation strategy. The current behavior appears to preserve the absolute instant, but this is not formally specified.
Additionally, integration with java.time.ZonedDateTime requires manual conversion since TimeCategory does not return java.time types, introducing a potential inconsistency point when mixing legacy and modern date-time APIs.
inTimeZone formally guarantee absolute-instant preservation across all JDK versions?TimeCategory.inTimeZone preserves the absolute instant, not the wall-clock representation. The returned java.util.Date holds the same millisecond epoch as the input; only the zone used to interpret its fields changes. This behavior comes from java.util.Calendar delegation, not from a documented Groovy contract — so treat it as dependable today but not formally guaranteed.
The implementation is roughly: copy the date into a Calendar, call setTimeZone(target), then return calendar.getTime(). Calendar.getTime() reconstructs a Date from the same epoch milliseconds, so the instant is invariant. A Date carries no zone at all — the zone only matters when the calendar interprets fields or when you format the result.
Confirmed fact: the epoch is unchanged. Likely-but-unspecified: that this stays true across future Groovy versions, since no GDK documentation states the contract explicitly and a reimplementation could change it.
use(groovy.time.TimeCategory) {
def d = new Date(0)
def converted = d.inTimeZone(TimeZone.getTimeZone('America/New_York'))
assert converted.time == 0L // same absolute instant
}For a DST-gap check, interpret a local time in the target zone before and after conversion and compare formatted output — Calendar's default lenient mode shifts non-existent local times (e.g. 2026-03-08 02:30 in America/New_York) forward by the DST offset, and resolves fall-back overlaps to the first occurrence. You cannot configure this through inTimeZone.
java.time, use instant.atZone(targetZone) or zonedDateTime.withZoneSameInstant(targetZone).localDateTime.atZone(targetZone), which lets you detect gaps/overlaps explicitly via ZonedDateTime resolution rules.Date has no zone metadata. If you later convert with date.toInstant().atZone(...), you must re-supply the intended zone — a common silent-mismatch point when mixing legacy and modern APIs. Record the target zone alongside any serialized value.For new code, prefer java.time conversions where the preservation strategy is explicit and DST disambiguation is controllable. Keep inTimeZone for legacy paths, and pin the behavior with the epoch-assertion test above so a future Groovy upgrade can't silently change it. Assumes Groovy 2.x–4.x and standard JDK Calendar semantics.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.