OkKotlin

Type-safe time calculations using Duration

Time calculations have always been a sensitive topic in most programming languages. Kotlin is no exception.

If we have a function which accepts a time value to calculate a duration, it's challenging to enforce a proper unit that the caller must follow.

Starting from Kotlin 1.3.50, we can now make use of the Duration class to have worry-free time calculations in our programs. Let's see how that works.

Accepting time as a Long value is broken

The most common approach to store and work with time values is to have the time converted into milliseconds and stored into a Long variable.

While this works fairly when we are dealing with internal code, working on our own mostly, as soon as we expose some API which accepts a time for a teammate to use or general public, cracks appear.

Take this function as an example:

fun retryBackup(after: Long) {    
    // Retry data backup after a given millis of time
}

It's quite safe to say that this function is not type-safe. Although it accepts a Long parameter, that Long value can be anything.

Frustration of using Long for time

We can pass the value as milliseconds, or minutes, or days and the program would compile and run just fine. However, our calculations would be skewed.

The reason behind this is inside the function, we are expecting a time in milliseconds for our calculations. Therefore, if we need to retry the backup after 6 hours, we need to first convert 6 hours to its equivalent milliseconds and then pass the value.

This is not obvious to the caller when calling the function. The after parameter is just a Long . A Long value doesn't automatically equate to milliseconds. It can represent any unit of time.

Sure, we can improve the readability of our function by renaming the parameter to afterTimeInMillis but that is merely an indication to the caller, not safety.

We can still wrongly pass 6 as the value and the backup will be retried after 6 milliseconds instead of 6 hours.

Kotlin has a better way

Kotlin, in it's 1.3.50 release, introduced a new class called Duration. We will use this class to accept time in a type-safe way.

Quick definition:

The time to read this article can be represented by a duration

A Duration represents the amount of time between two instants. Like, 1 second, 2 hours, 5 minutes and so on.

Now, coming back to our previous function, retryBackup , to make it type-safe, we can swap out the Long parameter with a Duration and extract a milliseconds value from the supplied duration to work with.

Like this:

fun retryBackup(after: Duration) {    
    val millis = after.toLongMilliseconds()    
    // Retry data backup after a given millis of time
}

There's just a tiny hiccup though.

If we try to run this code, it won't compile. The compiler will complain that we need to mark our function with an ExperimentalTime annotation.

This because Duration isn't mature yet and to use it, we need to tell the compiler that we are okay with using an experimental feature.

Therefore, our final code will look like this:

@ExperimentalTime
fun retryBackup(after: Duration) {    
    val millis = after.toLongMilliseconds()    
    // Retry data backup after a given millis of time
}

Hang on a second, we still haven't talked about how this approach is type-safe. It is type-safe because we now need to specify a Duration with a proper time unit when calling the function, like this:

retryBackup(after = 6.hours)

Try passing a Long or an Int as a parameter. You will get a compile time error. This approach, apart from being type-safe, is much more readable too.

The standard library provides a bunch of handy extension properties on Int, Long, and Double values to create durations easily like we did before (6.hours).

Similarly, we can convert a Duration to it's equivalent milliseconds or nanoseconds by calling the methods toLongMilliseconds() and toLongNanoseconds() respectively.

If you have used Ruby on Rails, this sort of pretty syntax might seem familiar to you.

Calculating relative time

A duration is just a representation of an amount of time. It's not a timestamp.

In cases where we want to get a relative time like "1 day from now", we need to add the current time to our duration.

Since making this computation over and over again is a hassle, we can create a reusable extension property on the Duration class to ease our job.

The following extension property will compute a relative duration from the current time:

@ExperimentalTime
val Duration.fromNow
    get(): Duration {
        val currentDuration = System.currentTimeMillis().milliseconds
        return this + currentDuration
    }

One great thing about the Duration class is that all common operations like addition, subtraction, multiplication, etc are supported. Therefore, we can easily add the current elapsed duration with our offset and get a final computed duration.

Now, we can use this property to pass a relative timestamp instead of a duration value, like this:

runBackup(on = 1.days.fromNow)

Experimenting in production?

As we saw earlier, Kotlin's Duration APIs are at an experimental stage. So, the question is, will it be a good idea to use this in our production apps?

I would suggest not to use this API in a large project right now. Whatever code we discussed in this article might be invalidated in a later release.

Navigating the codebase and changing each usage will be a frustrating task.

For small hobby projects, it's perfectly fine to use these APIs because even if they change, your project might be fine with a couple of changes here and there.

Try it, maybe?

It feels nice to see how Kotlin is shaping up to be a language focused on developer happiness like Ruby. The duration API is another step in that direction.

Why don't you try out this new language feature?

Here's a sketch note on the topic

Sketch note Kotlin's Duration API