Teneo Developers

Times, Dates and Time Zones in Solutions

On this page we will take a look at how to get and use times and time zones in a few different scenarios - along with some of the issues to be aware of. The intention is not to go into a deep investigation of all Time Zone problems and how to resolve them as this is a very complicated field and there is lots of discussion elsewhere around how to deal with it.

The aim instead is to be a basic look at some key points for using time and time zones in Teneo Solutions and to a starting point for further investigation.

what time is it flow

Audience

This information is important for any solution developer - but the key audience is anyone dealing with Times / Time Zones within scripts in a solution as the scripts should encapsulate any logic which is needed.

The Golden Rule

Always store times in UTC - then apply a timezone when relevant to display to the user.

Only in very rare cases when you know for a fact that the data is all in the same time zone (including observation of daylight savings time) can you ignore this golden rule. Even then it is recommended to use UTC and convert to a users particular time zone to display (or, as we will see, use Groovy data formats with a known time zone).

Key Points when working with Time

The following are important concepts to consider when working with Time:

  1. UTC (Universal Coordinated time - equivalent to, but not identical to GMT, Greenwich Mean Time) is the baseline for all time zones and all time calculations.
  2. Time Zone is a set of rules for how to calculate the local observed time (wall time) in a particular geographical area.
  3. DST (Daylight Saving Time) is a particular range of dates within a year when a particular Time Zone has a different "UTC-offset" applied.

Incremental vs. Observed Time

Observed Time is the time that you would see if you look at a clock on the wall. For this reason it is sometimes called "wall time".

Observed time is:

  • Immediately understandable to a particular person at a particular place on a particular day
  • Simple: "11:15", "quarter to 3"
  • Calculated as UTC + "current UTC offset"

Because of DST, the current UTC offset is not fixed - it will be different on different dates. Making comparisons between different times (even in the same time zone) is not always straightforward; you can see a walk-through of this complication further down on this page.

Incremental Time is the best time to use for computation.

  • It is UTC!
  • Comparisons are simple as there is no DST or Time Zone offsets at all
  • Doesn't immediately mean anything to the vast majority of users as it doesn't intuitively relate to the "wall time"
  • Needs converting to Local (Wall) time before displaying to the user

"Current" time

Current time is a difficult concept for all the reasons above, partly because it does not define to whom/where the time is "current". For example, at any one moment in time:

  • Current observed (Wall) time for a user in London will be different to current observed time for a user in São Paulo.
  • Current incremental (UTC) time for both users is the same.

So again - always work in UTC, or ensure groovy knows the Time Zone.

Groovy and Time Zones

Groovy (via Java 8) has access to a range of classes for working with time. Using these classes largely removes the need to explicitly convert to UTC to perform calculations - but you do need to ensure you are telling groovy which time zone it should apply to each time object you are working with.

  • Instant is an instantaneous point on the global time-line (UTC), and is unrelated to time zone.
    • Use Instant.now() to get the current time in UTC - this can then be converted to the Wall time zone if needed (see Use Case 1 below)
  • LocalDate, LocalTime and LocalDateTime have no concept of time zone.
    • These have useful comparisons methods: isAfter / isBefore - but you must ensure that you are comparing LocalTime from the same time zone.
    • Can be created via ZonedDateTime.toLocalTime() / ZonedDateTime.toLocalDateTime() / ZonedDateTime.toLocalDate() to then allow comparisons
  • ZonedDateTime should be the life-blood of using Time and Date in a solution.
    • Created with a Time, Date and TimeZone this class is then aware of relative offsets and Daylight Saving rules etc.

Use Case 1: "What is the time?"

In most cases a Teneo Solution is running on a server. It is not running on the end user's machine. This means that asking Groovy for the time now will return the time wherever the server is (see Server Time).

Getting the current time for a user then simply needs to follow the Golden Rule: First get the time in UTC and then convert it to the user's Wall time.

Calculate

groovy

1// Instant.now() returns the current time in UTC 
2def nowUTC = Instant.now()
3
4// Get zone id from here: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
5def tzLDN = ZoneId.of( "Europe/London" )
6
7// Convert the UTC time to a known time zone "ZonedDateTime"
8ZonedDateTime nowLDN = nowUTC.atZone(tzLDN)
9println "in London now it is: " + nowLDN
10

To do this you need to know the time zone of the user. This would need to come from the end user application - it is not automatically available from the system - here we have hard coded it to "Europe/London".

what time is it flow

The conversation will look something like this in Tryout: current time tryout

Use Case 2: Chat Handover - Opening Hours

This use case entails working out whether a call center is open now. Here we need to compare the current time to the "open hours". To do this we need to know "now" and "open hours" in UTC (or in a known time zone).

Say we have a call center in São Paulo which is open between 09:03 and 18:00 every day and we want to be able to hand over a user if the center is open and display opening hours if it is not - regardless of where the end user is connecting from.

Calculate

groovy

1// Get current incremental time
2// Instant.now() returns the current time in UTC
3def nowUTC = Instant.now()
4println "now (UTC): " + nowUTC
5
6// This is the Time Zone of the chat centre
7def chatZone = ZoneId.of("America/Sao_Paulo")
8
9// Tell Groovy that we're interested in now (UTC) in the same time zone as the chat center
10def nowCZ = ZonedDateTime.ofInstant(nowUTC, chatZone)
11
12// From here we can make all calculations in call centre "Local" (Wall) time
13// the call center opening hours are defined in Local time and "now" is already TimeZone aware.
14def hasOpened = nowCZ.toLocalTime().isAfter(LocalTime.of(9, 30))
15def hasClosed = nowCZ.toLocalTime().isBefore(LocalTime.of(18, 0))
16

We can create a Global Scripted Context to allow us to use this calculation simply within Flows; this Scripted Context calculates the call center open "states":

scripted context

This then gets used within a Flow:

using scripted context in flow

Use Case 3: Time comparisons - a walk-through

Consider finding the number of hours between 2 times: "How many hours are there between 1 today and 4 tomorrow?"

Now lets get rid of one of the potential confusions: AM / PM. We will just state this here as that particular confusion should already be well understood and is a communication issue - not a calculation issue! So to the question: "How many hours are there between 13:00 today and 04:00 tomorrow?"

The obvious answer (which in most cases will actually be correct) is (24 - 13) + 4 = 15.

Daylight savings

So what if the user is in London, and Daylight savings ends tonight?

This means we need to start being careful, so lets look at some details:

  • What is the time zone today and tomorrow?

The time zone does not change. Remember, time zone is a collection of rules for finding the local (wall, observed) time from UTC. One of those rules is how and when to apply Daylight Saving.

  • OK, so it is UK time, which GMT is UTC, right? So UTC offset is 0.

No! UTC offset changes with Daylight saving so in fact:

code

1Today at 13:00
2Time Zone = Europe/London
3GMT Offset = +1:00 (Daylight savings active)
4Tomorrow at 04:00
5Time Zone = Europe/London
6GMT Offset = +0:00 (Daylight savings ended)
7
  • Right! So we need to take into account DST today, but not tomorrow. Well that's easy; it's only adding 1!

Yep, for these dates, probably - it depends where you were planning to add the 1: (24 - 13) + 4 + 1 = 16 which is correct. Daylight Saving adds 1 hour to the wall time, so at the end of daylight saving time goes back by 1 hour (02:00 becomes 01:00 again - so there are actually 2 1 o'clocks on Sunday morning!)

However this is only valid for these two days - 1 day earlier / later or even same date 1 year earlier / later and the calculation would not be correct. Most likely the previous calculation would.

You can try it here:

  1. Select "London" for both Start and End Location then "Calculate Duration"
  2. Change the dates and do it again.
  • Fine, I get it; it's not as simple as it seems. I guess I'd better let someone else deal with it.

No, you can do it! Lets turns that calculation around though and see what we are actually doing:

(24 - 13) + 4 + 1 equates to: (HoursInADay - StartTime) + EndTime + OffsetToDST

What we are doing here is we are converting to a single UTC offset to do the calculation. So we could equally do it the other way around:

(24 - (13 - 1)) + 4 = 16 > (HoursInADay - (StartTime - OffsetToDST)) + EndTime

Or we could convert everything to 0 UTC offset - ie. UTC! (HoursInADay - (StartTime - StartTimeUTCOffset)) + (EndTime - EndTimeUTCOffset) > (HoursInADay - StartTimeUTC) + (EndTimeUTC)

In "Europe/London" time this is slightly pointless: (24 - (13 - 1)) + (4 - 0) = 16 However, it is correct. In "Europe/London" time the day after it seems even more pointless: (24 - (13 - 0)) + (4 - 0) = 15 but it is correct.

What if we were somewhere else? (Here we change the dates to still be on the DST boundary.)

Now we are in São Paulo and DST ends at midnight. São Paulo observes BRT (Brasilia Time): UTC - 03:00 and BRST (Brasilia Summer Time): UTC - 02:00 so to perform the same calculation for "today in São Paulo" we use:

(24 - (13 - BRSTOffset)) + (4 - BRTOffset) > (24 - (13 + 2)) + (4 + 3) = 16 which is again correct, with just the substitution of the UTC offset.

  • Ok - I get that it works, but I don't see why it is easier. You're still converting into something and you need to know all about the different time zones anyway.

2 things:

1. This works across locations

Applying exactly the same logic can allow us to compare local times in different locations

What is the number of hours between 13:00 27th July in London and 04:00 28th July in São Paulo? 27th July in TimeZone Europe/London > BST (British Summer Time) > UTC offset is +1:00 28th July in America/Sao_Paulo > BRST (Brasilia Time) > UTC offset is -3:00.

Plugging that in: (24 - (13 - BSTOffset)) + (4 - BRSTOffset) > (24 - (13 - 1)) + (4 + 3) = 19.

LDN SP 19

2. Groovy can do most of it for you!

All you need to remember is to ensure you tell groovy the time zone before calculating.

Time Comparisons in Groovy

Saturday 28th October 2023 13:00 Europe/London -> Sunday 29th October 2023 04:00 Europe/London.

groovy

1def LDNStart = ZonedDateTime.of(2023, 10, 28, 13, 0, 0, 0, ZoneId.of("Europe/London"))
2def LDNEnd = ZonedDateTime.of(2023, 10, 29, 4, 0, 0, 0, ZoneId.of("Europe/London"))
3println "LDNStart BST Ends difference: " + LDNStart.until(LDNEnd, java.time.temporal.ChronoUnit.HOURS)
4

The code above will print: LDNStart BST Ends difference: 16

So we have defined a ZonedDateTime because it is a format that is intrinsically TimeZone aware. We created it with a Date and Time (in Observable "Wall" time) and the TimeZone in which this is Observed. Then we simply compare the times using the in built methods.

Saturday 18th February 2023 13:00 -> Sunday 19th February 2023 04:00

groovy

1def SPStart = ZonedDateTime.of(2023, 10, 28, 13, 0, 0, 0, ZoneId.of("America/Sao_Paulo"))
2def SPEnd = ZonedDateTime.of(2023, 10, 29, 4, 0, 0, 0, ZoneId.of("America/Sao_Paulo"))
3println "SPStart BRST Ends difference: " + SPStart.until(SPEnd, java.time.temporal.ChronoUnit.HOURS)
4

The code above will print: SPStart BRST Ends difference: 16

13:00 27th July 2023 in London -> 04:00 28th July 2023 in São Paulo

groovy

1def XZoneStart = ZonedDateTime.of(2023, 7, 27, 13, 0, 0, 0, ZoneId.of("Europe/London"))
2def XZoneEnd = ZonedDateTime.of(2023, 7, 28, 4, 0, 0, 0, ZoneId.of("America/Sao_Paulo"))
3println "LDN start SP Ends difference: " + XZoneStart.until(XZoneEnd, java.time.temporal.ChronoUnit.HOURS)
4

The code above will print: LDN start SP Ends difference: 19

Server time

Calling LocalTime.now() will return the time in the Server Time Zone. This means that to use this time you will need to know (or be relying on) the time zone of the Server.

Using Instant.now() will return the UTC time, which is therefore independent of the setup of the Server.

Therefore usually Instant.now() is the more useful option.

The following is chunk of script that will get current time in various different time zones and output them:

groovy

1import java.time.*
2
3// Instant.now() returns the current time in UTC 
4def nowLocal = Instant.now()
5println "now (UTC): " + nowLocal
6
7// Get zone id from here: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
8def tzLDN = ZoneId.of( "Europe/London" )
9
10// Convert the UTC time to a known time zone "ZonedDateTime"
11ZonedDateTime nowLDN = nowLocal.atZone(tzLDN)
12println "London DateTime: " + nowLDN
13
14// Get the local (this means LOCAL TO THE SERVER) timezone
15def serverTimeZone = Calendar.getInstance().getTimeZone();
16println "Server TimeZone: " + serverTimeZone.ID
17
18// Get the local zone id (there might be a more elegant way to do this...)
19def tzServer = ZoneId.of(serverTimeZone.ID)
20
21// Convert the UTC time to server time
22ZonedDateTime nowServer = nowLocal.atZone(tzServer)
23println "Server DateTime: " + nowServer
24
25// This last is the same as calling LocalDateTime (or LocalTime if you only want the time)
26println "LocalDateTime (Server) Date and Time: " + LocalDateTime.now()
27println "LocalTime (Server) Time: " + LocalTime.now()
28
29// More here: https://stackoverflow.com/a/32834526
30