Skip to content

Dates and time zones

Read this when a time or date in an expression comes out one or two hours wrong, or one day off. It applies to Deem Java Expression and Deem Script Expression.

Date and Timestamp fields reach the expression as a QueryDate. An empty field is an empty QueryDate, never null: test it with field.isEmpty().

A QueryDate is a point in time plus a time zone. Reading the hour, day, week or a formatted text uses that zone. Which zone a value carries depends on where it came from:

SourceTime zone
Date or Timestamp fieldUTC
Text with an offset (2026-10-19T14:21:22+02:00)UTC
Text without an offset, YMD8 number (20261019)server time zone
ZDateTime, anything from ZToolsserver time zone

So in Norway a Date field read straight away shows UTC time: one hour behind in winter, two in summer. Near midnight the date and the week can also be wrong.

Convert before you read. The conversion keeps the point in time and only changes the zone.

field.zoneCet().toHHMM() // Europe/Oslo
field.zoneCet().getYMD8()
field.zone("Europe/Stockholm").getHour()
field.zoneUtc().getYMD8() // explicit UTC
  • Use a named zone (Europe/Oslo), never a fixed offset (+01:00). A fixed offset is one hour wrong in summer.
  • zoneSystem() uses the server time zone, so it is only right when that zone is set (below).
  • Writing a QueryDate back to a Date or Timestamp field keeps the point in time. Only convert when you read parts of the date.

Everything in ZTools uses the server time zone: reading (getHour, toHHMM, toYMD8, getWeek, …), formatDate(..), and the dates it returns from setDate(..), setTime(..), asDate(..), addDays(..) and the other add/with functions. M3 numbers (YMD8 date, HHMMSS time) are read as server time. So ZTools.setDate(orderTime).toHHMM() gives server time, even though orderTime arrives as UTC. formatDate returns null for an empty date.

So ZTools is only right when the server time zone is set. field.zoneCet() is right everywhere.

An order is registered on 16 July 2026 at 00:30 Norwegian time (summer, UTC+2). The Date field orderTime holds that point in time, which is 15 July 22:30 UTC.

ExpressionServer in UTCServer in Europe/Oslo
orderTime.toHHMM()22:30 ✗22:30 ✗
orderTime.getYMD8()20260715 ✗20260715 ✗
ZTools.toHHMM(orderTime)22:30 ✗00:30 ✓
ZTools.toYMD8(orderTime)20260715 ✗20260716 ✓
ZTools.setDate(orderTime).toHHMM()22:30 ✗00:30 ✓
ZTools.formatDate(orderTime, "HH:mm")22:30 ✗00:30 ✓
orderTime.zoneSystem().toHHMM()22:30 ✗00:30 ✓
ZDateTime.toHHMM() (run at the same moment)22:30 ✗00:30 ✓
orderTime.zoneCet().toHHMM()00:30 ✓00:30 ✓
orderTime.zoneCet().getYMD8()20260716 ✓20260716 ✓
  • Only zoneCet(), or zone(..) with a named zone, is right on both servers.
  • In winter (UTC+1) the error is one hour. Only times between 00:00 and 01:00 move to the day before.

The server time zone is the Java default zone of the Integrator process. It follows the operating system unless it is set, so the same pipeline can give different results on two servers. Set it on every machine that runs pipelines, and keep the memory setting:

Terminal window
SETX /m HOP_OPTIONS "-Xmx2048m -Duser.timezone=Europe/Oslo"
Terminal window
export HOP_OPTIONS="-Xmx2048m -Duser.timezone=Europe/Oslo"

The new value applies when Integrator is started again. -Duser.country=NO sets the language and number format, not the time zone. Check it with an expression: java.util.TimeZone.getDefault().getID().

  • Server: -Duser.timezone=<zone> in HOP_OPTIONS.
  • Expression: convert with zoneCet() or zone(..) before toHHMM, getHour, getYMD8, getWeek or format(..).
  • Empty dates: field.isEmpty(), not field == null.
  • Old expressions that add one or two hours by hand: replace the hours with zoneCet().