Time

Time for non-Time Lords – Part 3

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 we discussed the various APIs available to use to simplify date and time handling, so let’s dive in and see how they work.

I’m going to be covering how to use Jake Wharton’s JSR 310 back-port for Android as this will be the best option for many developers. For API 26 and later JSR 310 is provided, but it will be a few years before many of us will be able to take advantage of this, so the back-port is the best option. I’ll use the name JSR 310 throughout as it represents the standard upon which both Jake’s optimised back-port and the Java 8 time support are based. We’ll look at the minor differences between the two implementations as we go.

First we need to include it in our project:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.1"
    defaultConfig {
        applicationId "com.stylingandroid.time"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:26.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    implementation 'com.jakewharton.threetenabp:threetenabp:1.0.5'
    testImplementation 'junit:junit:4.12'
}

One of the optimisations that Jake does over the standard JSR 310 Java back-port is how the time zone data is loaded, and this requires it to be initialised in the Application object:

class TimeApplication: Application() {
    override fun onCreate() {
        super.onCreate()

        AndroidThreeTen.init(this)
    }
}

For the JSR 310 implementation built in to SDK 26 and later you can omit these two steps.

Now we can use them. The actual APIs in the back-port and those built in to SDK 26 and later are identical except for the package. I don’t usually include the imports in the code that is included in the body of my posts, so the sample code should be interchangeable between the two. However the source repo is built using the back-port so uses the relevant package, and is compatible back to API 15 – that’s the minSdkVersion for Jake’s back-port.

Let’s start with the real basics. Unlike java.util.Date which stores both the time and date in a single object, JSR 310 uses separate objects. Let’s start by looking at how time is handled. We’ll use LocalTime which represents a basic time without any concept of time zones:

LocalTime.now().also { now ->
    println("Now: $now")
    now.plusHours(23).also { laterTime ->
        println("Later time: $laterTime")
        println("Duration between $now and $laterTime: ${Duration.between(now, laterTime)}")
        println("Duration between $laterTime and $now: ${Duration.between(laterTime, now)}")
    }
}

If we run this we get the following:

Now: 11:47:50.146
Later time: 10:47:50.146
Duration between 11:47:50.146 and 10:47:50.146: PT-1H
Duration between 10:47:50.146 and 11:47:50.146: PT1H

There are a couple of things worth noting here. Firstly all of the output values are in ISO 8601 formats. The duration has a PT prefix indicates that this is purely a time value as any date components (i.e. numbers of days months or years) would be included between the P and the T. The value following the T is the time duration – in this case either 1 hour or -1 hour.

The second thing worth noting is that although we added 23 hours to the current time, the laterTime is actually an hour earlier than the current time. This is because there is no concept of advancing days when we are working with time-only values. Understanding this is a key part of knowing whether a time-only value is applicable to your use-case. If you need to correctly handle time-spans which may straddle midnight, then a simple time may not be the correct choice.

This example shows some of the basic functionality that we’ll often use with time and date values – we often need to calculate an offset from a given time, and we can use the plus[Nanos|Mills|Seconds|Minutes|Hours]() and minus[Nanos|Mills|Seconds|Minutes|Hours]() methods to achieve this. Also we can use a Duration object to represent a length of time from two values.

Next we have standalone dates, let’s look at using LocalDate:

LocalDate.now().also { today ->
    println("Today: $today")
    today.plusDays(1).also { tomorrow ->
        println("Tomorrow: $tomorrow")
        println("Period between $today and $tomorrow: ${Period.between(today, tomorrow)}")
        println("Period between $tomorrow and $today: ${Period.between(tomorrow, today)}")
    }
}

One thing worth noting is that whereas for time we used Duration to calculate the difference, for dates we need to use Period instead. As well as having different object to represent time and date values, there are also different objects to handle time and date durations. While it might seem logical that the difference between two dates can be represented as a value in seconds, LocalDate will not resolve down to a second value, and if you attempt to use one as an argument to Duration.between() it will throw an exception.

Similarly to LocalTime, we can perform date arithmetic using the plus[Days|Weeks|Months|Years]() and minus[Days|Weeks|Months|Years]() methods.

This produces the following:

Today: 2017-08-06
Tomorrow: 2017-08-07
Period between 2017-08-06 and 2017-08-07: P1D
Period between 2017-08-07 and 2017-08-06: P-1D

Once again the values are output in ISO 8601 formats, and in this case the Period is output as a date – the value immediately following the P. In this case there is no time component whatsoever.

In many cases we actually need to specify both date and time components to accurately identify a specific point in time, and we have a DateTime representation. Let’s look at LocalDateTime:

LocalDateTime.now().also { now ->
    println("Now: $now")
    now.plusHours(23).also { laterTime ->
        println("Later time: $laterTime")
        println("Duration between $now and $laterTime: ${Duration.between(now, laterTime)}")
        println("Duration between $laterTime and $now: ${Duration.between(laterTime, now)}")
    }
    now.plusDays(1).also { laterTime ->
        println("Later time: $laterTime")
        println("Duration between $now and $laterTime: ${Duration.between(now, laterTime)}")
        println("Duration between $laterTime and $now: ${Duration.between(laterTime, now)}")
    }
}

LocalDateTime is actually a composite of LocalTime and LocalDate, so is a superset of both APIs – so we can happily add either hours or days to the value. Also, we can no longer use Period to calculate date periods because Period.between() will only accept date-only values. So we need to use the finer-grained Duration instead.

That covers the real basics and in the next article we’ll begin to dive a little deeper.

The source code for this article is available here.

© 2017, Mark Allison. All rights reserved.

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.

2 Comments

  1. Hi Mark.
    Thanks for the article. I have the feeling the ThreeTenABP library is well underrepresented in blog posts.
    I noticed two typos which you might want to fix: (1) “JRS” instead “JSR” and (2) “so so is a superset” instead of “so it is a superset”. Further, I am – as a lazy reader – missing the output of the LocalDateTime example.
    Best, Tobias

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.