Groovy Date type handling of time zone offsets during GPath operations
0 reputation · 27 Oct 2024, 08:26 UTC
0 reputation · 27 Oct 2024, 08:26 UTC
The GDK extends java.util.Date with property accessors that can return unexpected values when the underlying time zone differs from the default JVM time zone. This behavior becomes more pronounced during daylight saving time transitions or when Date objects are created in a non-default time zone.
When accessing date properties through GPath such as date.hour or date.dayOfWeek, Groovy applies the JVM default time zone for the conversion. If the Date was constructed using a different time zone offset, the returned property values may not match the intended local time representation.
This creates uncertainty around whether GPath property access should be considered time zone-aware or always bound to the JVM default. The behavior also interacts with Sql class formatting where timestamps may be rendered using an unexpected time zone.
Which time zone context should GPath property accessors on java.util.Date use when the JVM default differs from the Date's original time zone context, and how can this be reliably controlled across Groovy 2.x and 3.x versions?
35275 reputation · 27 Oct 2024, 20:22 UTC
GPath property accessors on java.util.Date always use the JVM default time zone. They do not use any “original” zone, because java.util.Date does not store one—it only holds an epoch-millisecond instant. The behavior is identical in Groovy 2.5.x and Groovy 3.x.
java.util.Date contains only an epoch-millisecond value; it has no intrinsic time-zone field.hour, dayOfWeek, month, and similar properties that format that instant through TimeZone.getDefault().When you parse a string with an explicit offset, the resulting Date is just an instant. Reading date.hour later renders that instant in whatever zone the JVM happens to default to. The original offset is not recoverable from the Date alone, so the “drift” is expected rather than a defect.
TimeZone.setDefault(TimeZone.getTimeZone("America/New_York")) before parsing or reading properties. This affects future conversions only; existing Date objects are not rewritten.date.toInstant().atZone(ZoneId.of("America/New_York")) and access GPath properties on the ZonedDateTime. This avoids dependence on global JVM state and is the safer choice in multi-zone applications.This scoped Groovy snippet demonstrates the behavior:
def s = "2026-10-18T12:00:00-07:00"
def d = Date.parse("yyyy-MM-dd'T'HH:mm:ssZ", s)
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
assert d.hour == 19
TimeZone.setDefault(TimeZone.getTimeZone("America/New_York"))
assert d.hour == 15
def zdt = d.toInstant().atZone(ZoneId.of("America/New_York"))
assert zdt.hour == 15
The assertions hold under both Groovy 2.5.14 and Groovy 3.0.9.
When timestamps come from groovy.sql.Sql, the JDBC driver typically returns java.sql.Timestamp, a java.util.Date subclass. Because it still carries only an epoch instant, the same GPath rules apply. Convert to ZonedDateTime before formatting if you need a specific zone.
If your code mixes java.sql.Timestamp and java.util.Date, tell me which concrete type your SQL layer returns. That determines whether conversion should happen immediately after the query or at render time, and whether you need to guard against null timestamps in the same path.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.