Time is hard. Any developer who tells you differently is either lying; thinks they understand it when they really don’t; or is a bona fide Time Lord. In this series we’ll explore some of the issues that time can pose, and take a look at some tools that we can use to help make time handling a little easier.



Previously in this series we’ve looked at a few aspects of the JSR 310 date and time APIs, but one thing that we haven’t yet looked at is a thing which can cause many problems for developers: Timezones.

Before we dive in it is worth pointing out that all of the examples we’ve looked appear to be completely agnostic of the timezone, but actually Local[Date|Time|DateTime] classes are not timezone agnostic at all – they just appear to be because they are automatically created in the local timezone for the device. If we are always working with date and time values in the same timezone then we can safely ignore the timezone because it is a constant. If we want to use a truly timezone agnostic representation then we need to use Instant (which represents a single point on a timeline) instead.

So let’s see how LocalDateTime behaves if we create two instances in different time zones from the same Instant:

MainActivity.kt Instant.now().also { now -> LocalDateTime.ofInstant(now, ZoneId.systemDefault()).also { here -> LocalDateTime.ofInstant(now, ZoneId.of("Europe/Paris")).also { paris -> println("Here: $here") println("Paris: $paris") println("Duration: ${Duration.between(here, paris)}") } } } 1 2 3 4 5 6 7 8 9 Instant . now ( ) . also { now - > LocalDateTime . ofInstant ( now , ZoneId . systemDefault ( ) ) . also { here - > LocalDateTime . ofInstant ( now , ZoneId . of ( "Europe/Paris" ) ) . also { paris - > println ( "Here: $here" ) println ( "Paris: $paris" ) println ( "Duration: ${Duration.between(here, paris)}" ) } } }

I am using the TZDB ID for Paris and we’ll look at this a little deeper a little later on.

This gives us the following output:

I: Here: 2017-08-13T12:21:43.343 I: Paris: 2017-08-13T13:21:43.343 I: Duration: PT1H 1 2 3 I : Here : 2017 - 08 - 13T12 : 21 : 43.343 I : Paris : 2017 - 08 - 13T13 : 21 : 43.343 I : Duration : PT1H

The times are an hour apart, and the duration is shown as one hour difference between the two even though they are created from the same Instant. This is because the difference in time zones is not taken in to account in the Duration calculation.

However, if we do the same thing using ZonedDateTime (which includes the timezone information), then this alters how the duration is calculated:

MainActivity.kt Instant.now().also { now -> ZonedDateTime.ofInstant(now, ZoneId.systemDefault()).also { here -> ZonedDateTime.ofInstant(now, ZoneId.of("Europe/Paris")).also { paris -> println("Here: $here") println("Paris: $paris") println("Duration: ${Duration.between(here, paris)}") } } } 1 2 3 4 5 6 7 8 9 Instant . now ( ) . also { now - > ZonedDateTime . ofInstant ( now , ZoneId . systemDefault ( ) ) . also { here - > ZonedDateTime . ofInstant ( now , ZoneId . of ( "Europe/Paris" ) ) . also { paris - > println ( "Here: $here" ) println ( "Paris: $paris" ) println ( "Duration: ${Duration.between(here, paris)}" ) } } }

This displays the same times, but with the timezone information included, and correctly calculates the Duration as 0 because it takes the time zone in to account for the calculation:

I: Here: 2017-08-13T12:21:43.354+01:00[Europe/London] I: Paris: 2017-08-13T13:21:43.354+02:00[Europe/Paris] I: Duration: PT0S 1 2 3 I : Here : 2017 - 08 - 13T12 : 21 : 43.354 + 01 : 00 [ Europe / London ] I : Paris : 2017 - 08 - 13T13 : 21 : 43.354 + 02 : 00 [ Europe / Paris ] I : Duration : PT0S

So these examples all use the TZDB ID format to reference a specific time zone – in this case Paris, France. But we can also see from the output that it is identifying my time zone using the TZDB ID for London, England. Using the TXDB IDs is the easiest way of addressing different time zones.

We can actually use the static ZoneId.getAvailableZoneIds() method to obtain a list of supported timezones, and we could check whether a specific one is actually supported before we attempt to obtain an instance of it.

One word of caution when using this: It is a large list, and here’s how we can prove that:

MainActivity.kt println("Available timezones: ${ZoneId.getAvailableZoneIds().size}") 1 println ( "Available timezones: ${ZoneId.getAvailableZoneIds().size}" )

If we run this we get the following output:

I: Available timezones: 586 1 I : Available timezones : 586

If you are using this list then you need to be aware of this – performing any non-trivial operation for each entry in that list will be very expensive. I originally wrote some code for this article to iterate through and print detailed information for each time zone, but when I ran it I got runtime warnings that I was doing too much on the main thread. Rather than switch the code to a background thread, I have elected to remove it altogether because such sample code could be taken out of context without the above warnings – and I really don’t want to inadvertently propagate bad practise.

There is an awful lot more to the JSR 310 APIs that we haven’t covered, but we have covered the parts which I think will be of most use to the majority of developers. If you have any more specific requirements then check out the APIs – there’s lots of useful, but more specific functionality in there.

The source code for this article is available here.

© 2017, Mark Allison. All rights reserved.

Related

Copyright © 2017 Styling Android. All Rights Reserved.

Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.