July 12, 2020

Time is Complicated: java.time

Time is complicated, really complicated. We have historically grown calendar systems, historical units, timezones, limelight savings times, etc. On top of that, we have precise atomic clocks and astronomical time definitions drifting apart and need periodical corrections, like with leap seconds.

Agreeing On Time
Figure 1. Agreeing On Time

For a long time, Java only had horrible time APIs with the java.util.Date and java.util.Calendar. You wanted to include a better API, like the wonderful Joda Time. With Java 8 the new java.time API was introduced, which is heavily inspired by Joda Time. Let’s take a small tour and take a look at the concepts.

Absolute Time vs Human Time

There are two different concepts of time. The first is an absolute timeline, like a clock ticking forward in a straight timeline. We can imagine a shared atomic clock used globally. Ignoring the speed of light and relativity for now. This is often how we as programmers imagine time to work.

Then there is human time, which we use in everyday life. That time isn’t a nice clock, but our archaic time and calendar systems. It’s a social, historical, and political system, not a strict timeline.

Absolute Time

First, start with absolute time. It is represented by an Instant in the java.time library. It truly represents an instant in time. So you can compare instants which one came before another, can sort time etc. It is a nice timeline. Well, within limits, the system clock might jump, but Instant will do its best =).

An Instant in time:
// A instant in time
var aPointInTime = Instant.now();
Thread.sleep(10);
// Later on, time progressed
var later = Instant.now();

System.out.println("Time is passing, first "+aPointInTime+" and later " +later);
Output:
Time is passing, first 2020-06-14T15:24:01.415488Z and later 2020-06-14T15:24:01.425584Z

For durations, there is the Duration class. Its an absolute duration of time, like a timer or stopwatch:

Durations:
var halfHour = Duration.ofMinutes(30);
var aFewSeconds = Duration.ofSeconds(5);

var aPointInTime = Instant.now();
var halfHourLater = aPointInTime.plus(halfHour);
System.out.println(halfHourLater+" is half an hour later of " +halfHourLater);

var difference = Duration.between(aPointInTime, halfHourLater);
System.out.println("The difference between " + aPointInTime + " and " + halfHourLater + " is " + difference);
Output:
2020-06-14T20:30:24.825930Z is half an hour later of 2020-06-14T20:30:24.825930Z
The difference between 2020-06-14T20:00:24.825930Z and 2020-06-14T20:30:24.825930Z is PT30M

Human Time

On the complete opposite end, we have human time. If I say, "Let’s meet at 2 pm on 8th June", I mean that its 2:30 pm in the local time with the local calendar, not some absolute point in time. For that, there is the LocalTime, LocalDate, and LocalDateTime. By local, it doesn’t mean that those times are in your local time zone. A Local* class has no time zone and no mapping to an absolute point in time. It’s the information a human would give you.

An example: You set an Alarm to 7 am on 7th July on your phone. By that, you don’t mean an absolute point in time. You want the alarm to go off at 7 am on 7th July even when you traveled around the world. Again, the Local* classes represent how we humans deal with time and calendars.

Human Time and Calendar
var time2_30pm = LocalTime.of(14, 30);
var july6_2020 = LocalDate.of(2020, 7, 6);
var july6_2020_at_2_30pm = july6_2020.atTime(time2_30pm);
var july25_2020_at_noon = LocalDateTime.of(2020, 7, 25, 12, 0);

System.out.println("Time: " + time2_30pm + " and day " + july6_2020);
System.out.println("Date times: " + july6_2020_at_2_30pm + " " + july25_2020_at_noon);

// Tons of convenience method.
var startOfDay = july6_2020.atStartOfDay();
var daysSteps = july6_2020.datesUntil(LocalDate.of(2020, 7, 20));
var weeksSteps = july6_2020.datesUntil(LocalDate.of(2020, 7, 20), Period.ofDays(7));

System.out.println("Start of day: " + startOfDay);
System.out.println("Day steps:");
daysSteps.forEach(System.out::println);
System.out.println("Week steps:");
weeksSteps.forEach(System.out::println);
Output:
Time: 14:30 and day 2020-07-06
Date times: 2020-07-06T14:30 2020-07-25T12:00
Start of day: 2020-07-06T00:00
Day steps:
2020-07-06
2020-07-07
...
2020-07-19
Week steps:
2020-07-06
2020-07-13

Bridging the Worlds: ZonedDateTime

Ok, we have now Instant and LocalDateTime etc. So, how do we bring these worlds together? How do we convert an instant to human time? Or translate a LocalDateTime to a concrete instant in time? Here is where the ZoneDateTime comes in. A timezone contains the rules on how a particular instant in time is mapped to the local human time and vice versa.

Ancient Wisdom Of Timezone Rules
Figure 2. Ancient Wisdom Of Timezone Rules
Translate Instants to Human Times:
var usPacific = ZoneId.of("US/Pacific");
var chTime = ZoneId.of("Europe/Zurich");
var now = Instant.now();

var asZurichTime = now.atZone(usPacific);
var asPacificTime = now.atZone(chTime);

System.out.println("The instant " + now + " is "+asZurichTime + " or for humans zurich it is " + asZurichTime.toLocalDateTime());
System.out.println("The instant " + now + " is "+asPacificTime + " or for humans zurich it is " + asPacificTime.toLocalDateTime());
Output:
The instant 2020-06-14T11:28:28.363685Z is 2020-06-14T04:28:28.363685-07:00[US/Pacific] or for humans zurich it is 2020-06-14T04:28:28.363685
The instant 2020-06-14T11:28:28.363685Z is 2020-06-14T13:28:28.363685+02:00[Europe/Zurich] or for humans zurich it is 2020-06-14T13:28:28.363685
Translate Human Date to Instant:
var usPacific = ZoneId.of("US/Pacific");
var zurich = ZoneId.of("Europe/Zurich");
var oct24_10_30 = LocalDateTime.of(2020, 10, 24, 10, 30);
var oct25_10_30 = LocalDateTime.of(2020, 10, 25, 10, 30);


var oct24_10_30_in_zurich = oct24_10_30.atZone(zurich);
var oct24_10_30_in_seattle = oct24_10_30.atZone(usPacific);
var oct25_10_30_in_zurich = oct25_10_30.atZone(zurich);
var oct25_10_30_in_seattle = oct25_10_30.atZone(usPacific);
System.out.println(oct24_10_30 + " in Zurich translates to " + oct24_10_30_in_zurich.toInstant());
System.out.println(oct24_10_30 + " in Seattle translates to " + oct24_10_30_in_seattle.toInstant());
System.out.println(oct25_10_30 + " in Zurich translates to " + oct25_10_30_in_zurich.toInstant());
System.out.println(oct25_10_30 + " in Seattle translates to " + oct25_10_30_in_seattle.toInstant());
Output:
2020-10-24T10:30 in Zurich translates to 2020-10-24T08:30:00Z
2020-10-24T10:30 in Seattle translates to 2020-10-24T17:30:00Z
2020-10-25T10:30 in Zurich translates to 2020-10-25T09:30:00Z
2020-10-25T10:30 in Seattle translates to 2020-10-25T17:30:00Z

Noticed how the two local dates translate to the same time for Seattle, but changes for Zurich. The reason is that the Zurich timezone had its daylight saving time switch in between the dates.

The daylight saving times also mean that certain local date-times do not exist, like March 29th, 2020 2.30 am in Zurich, because at 2 am jumped to 3 am.

Not Existing Time:
var usPacific = ZoneId.of("US/Pacific");
var chTime = ZoneId.of("Europe/Zurich");
var march29_2_30 = LocalDateTime.of(2020, 3, 29, 2, 30);
var march29_2_30_zurich = march29_2_30.atZone(chTime);
var march29_2_30_seattle = march29_2_30.atZone(usPacific);

System.out.println(march29_2_30 + " translates to " + march29_2_30_zurich + " in Zurich, actual local time " + march29_2_30_zurich.toLocalTime());
System.out.println(march29_2_30 + " translates to " + march29_2_30_seattle + " in Seattle, actual local time " + march29_2_30_seattle.toLocalTime());
Output:
2020-03-29T02:30 translates to 2020-03-29T03:30+02:00[Europe/Zurich] in Zurich, actual local time 03:30
2020-03-29T02:30 translates to 2020-03-29T02:30-07:00[US/Pacific] in Seattle, actual local time 02:30

Or the opposite, where a local date-time can mean two different times, because the local time repeated. Like on October 25th, 2.30 am repeats twice in Zurich. So for these cases, you have to tell the Java time libraries which time you mean.

Overlapping Time:
var chTime = ZoneId.of("Europe/Zurich");
var oct25_2_30 = LocalDateTime.of(2020, 10, 25, 2, 30);
var oct25_2_30_zurich = oct25_2_30.atZone(chTime);

var earlyInterpretation = oct25_2_30_zurich.withEarlierOffsetAtOverlap().toInstant();
var lateInterpretation = oct25_2_30_zurich.withEarlierOffsetAtOverlap().toInstant();
System.out.println(oct25_2_30 + " could mean " + earlyInterpretation + " or " + lateInterpretation + " in Zurich");
Output:
2020-10-25T02:30 could mean 2020-10-25T00:30:00Z or 2020-10-25T00:30:00Z in Zurich
What 2.30 am
Figure 3. What 2.30 am?

Side note: Timezones can be wild. A government might change the offset for a timezone. This happens all the time, just take a look at the timezone newsletter. So, that database has to be kept up to date. For example, if you checked JDK release notes, it often says that the timezone database was updated. There’s also an overview page which timezone database version was added to what Java release.

Periods of Time

Remember that we had the Duration class for an absolute amount of time. There is the equivalent for human time scales. For example, when we say, let’s meet in 7 days at the same time, we mean the same local time no matter if there was a daylight saving time change. We do not mean 7days x 24hours later. Similar with a year. If we say, same date next year we do not mean 365days x 24hours. That is the difference between a Duration and a Period. The Period is a fuzzy human time scale and needs to be aware of calendars and timezones. A Duration is a fixed amount of time.

Adding Durations vs Periods
var chTime = ZoneId.of("Europe/Zurich");
var oct24_12_30 = LocalDateTime.of(2020, 10, 24, 12, 30);
var feb12_12_30 = LocalDateTime.of(2020, 2, 12, 12, 30);
var oct24_12_30_zurich = oct24_12_30.atZone(chTime);

var durationDays = Duration.ofDays(7);
var durationYear = Duration.ofDays(365);
var periodDays = Period.ofDays(7);
var periodYear = Period.ofYears(1);

System.out.println(oct24_12_30 + " plus a duration of 7 days gives " + oct24_12_30_zurich.plus(durationDays).toLocalDateTime() +  " in Zurich");
System.out.println(feb12_12_30 + " plus a duration of 1 year gives " + feb12_12_30.plus(durationYear));
System.out.println(oct24_12_30 + " plus a period of 7 days gives " + oct24_12_30_zurich.plus(periodDays).toLocalDateTime() +  " in Zurich");
System.out.println(feb12_12_30 + " plus a period of 1 year gives " + feb12_12_30.plus(periodYear));
Output:
2020-10-24T12:30 plus a duration of 7 days gives 2020-10-31T11:30 in Zurich
2020-02-12T12:30 plus a duration of 1 year gives 2021-02-11T12:30 in Zurich
2020-10-24T12:30 plus a period of 7 days gives 2020-10-31T12:30 in Zurich
2020-02-12T12:30 plus a period of 1 year gives 2021-02-12T12:30 in Zurich

Wrap Up

This was a small peak into the java.time API. It tries to help you reason about time and do the right thing. It handles a lot of scenarios. If you need more, you can use Joda time, which as the inspiration for the java.time.

I also recommend these two talks. The code examples are in C#/F# but the concepts are the important part.

It’s about time - Christin Gorman:

Dates and times aren’t that hard - honestly! - Jon Skeet:

Tags: Time-Handling Java Development C/C++