Published on

Taming Time in Kotlin

Authors

Most of the time to be able to do something useful in code you need to say get the current date or date and time. This is something that mutates and is hard to test unless you carefully plan it and can lead to "Cinderella bugs" where the code can fail seemingly randomly at very specific times. With Kotlin there is luckily a fairly easy way to tame this and also make it much much easier to test. The other benefit to this approach is you can adjust the clock on code if you ever need to.

A typical method may look like this:

fun doAlertOnExpiry(): Alert {
  if(LocalDateTime.now() < someCutOffDate) {
     val updatedAlert = alertRecord.copy(updated = LocalDateTime.now(), mustAlert=true)

     return alertRepository.save(updatedAlert)
  } else {
    val updatedAlert = alertRecord.copy(updated = LocalDateTime.now(), mustAlert=false)

    return alertRepository.save(updatedAlert)
  }

}

This code is fairly simple and contrived but illustrates a few things:

  1. The tests will be a mission as we cannot accurately test the time values being used
  2. This code is simple but for more complex code it can break easily due to time being out of our control and is harder to debug/fix due to 1

Luckily in Kotlin and other languages that support telescoping functions, there is an easy solution:

fun doAlertOnExpiry(now: LocalDateTime = LocalDateTime.now(), updatedDatetimeProvider: () -> LocalDateTime = { LocalDateTime.now() }): Alert {
  if(now < someCutOffDate) {
     val updatedAlert = alertRecord.copy(updated = updatedAtProvider() , mustAlert=true)

     return alertRepository.save(updatedAlert)
  } else {
    val updatedAlert = alertRecord.copy(updated = updatedAtProvider(), mustAlert=false)

    return alertRepository.save(updatedAlert)
  }

}

We have addressed the issues above it now allows us to:

  1. Easily test:
    1. In the test we can tell the test what value to use for now using for example: LocalDateTime.of(2022, Month.JUNE, 3, 11, 14,0,0,0)
    2. In the test we can easily control updated by just giving it whatever provider we wanted and even giving it a closure that can adjust the value returned based on the invocation number
  2. Easily control the value used for the current time
    1. now will always be the same when called in the function even when it telescopes i.e. it will be the exact same to the nano second (the old approach would have had differences there where it would change from LocalDateTime.now() first being called to when it is called again later)
    2. updated will change but if you wanted to you can easily give the function your own provider to force it to be the same updated value for example { LocalDateTime.of(2022, Month.JANUARY, 4, 12, 37, 1, 2, 0) }

An example simple closure to illustrate this is as follows:

fun timeClosureExample(): () -> LocalDateTime {
    var timesCalled: Int = 0
    return {

        if(timesCalled == 0){
            timesCalled++
            LocalDateTime.of(2021, Month.MAY, 4, 14, 54, 1, 3, 0)
        } else {
            timesCalled++

            LocalDateTime.of(2022, Month.DECEMBER, 7, 18, 32, 5, 7, 0)
        }
    }
}
val callback = timeClosureExample()
println(callback())
println(callback())
println(callback())

This outputs:

2021-05-04T14:54:01:03:00.000Z
2022-12-07T18:32:05:07:00.000Z
2022-12-07T18:32:05:07:00.000Z