DoorDash’s platform relies on accurate times affecting real-world events, such as if an order is ready when a Dasher, our term for a delivery person, comes to pick it up. Our assignment algorithm, which makes many of these events possible, considers hundreds of variables, including location, distance, available Dashers, and food preparation time. After considering every variable, we apply a score to each Dasher and delivery pair, and optimize the region to make the most optimal assignments. 
The dispatch team iterates on this algorithm weekly, as we’re constantly searching for quicker and more efficient ways of making the most optimal assignment. Because we iterate so quickly on our codebase, code readability is an important quality that helps us balance speed with accuracy. If the code is difficult to understand at a glance, it increases our cognitive load and slows down our development time. Code with poor readability also makes it easier for bugs to slip into our algorithm.
Like many startups, when we first developed the algorithm, we chose to represent time with primitive types. This methodology worked for a while, but as we’ve grown in size and complexity, it became obvious that we needed a more reliable way to represent time due to bugs that stem from this practice. Some bug examples include unit conversion errors from milliseconds to seconds, or errors with adding five minutes to an estimation when we only meant to add five seconds. 
After considering the challenge, we refactored our codebase to use a time library instead of primitives to increase code readability. Refactoring our codebase in this manner was a tall order. Timestamps and durations are the basis for all of our estimations, so naturally they are used extensively throughout our codebase. Because our code directly impacts our customers’ livelihoods and meals, we had to make sure our refactorization would not introduce any unintended changes. Rather than refactoring our code all at once, we broke the problem into smaller chunks and opted to do the migration slowly over multiple changes.
Una nota rápida sobre las bibliotecas de tiempo
In JVM-based languages, the java.time library (also known as JSR-310) is practically the only choice. Another commonly used library, Joda-Time, was actually the precursor to java.time, and was developed by the same person who would go on to lead the java.time project. Given that Joda-Time is no longer under active development, and that the Joda-Time website recommends users switch to java.time, using java.time for our project was an obvious choice. 
Un ejemplo básico de codificación del tiempo
Uno de los componentes básicos del código son las funciones y los parámetros que les pasamos. Tomemos esta función como un ejemplo extremadamente simple:
fun getEstimateInSeconds(timestampInSeconds: Long): Long {
    val now = System.currentTimeMillis()
    return if (timestampInSeconds < (now / 1000)) {
        timestampInSeconds + 60 * 5
    } else {
        timestampInSeconds - 60 * 60 * 3
    }
}
The above code merely takes a timestamp and compares it against the current time, modifying the time to get a result. Functionally, it gets the job done. However, it’s hard to read because of its use of primitive types and literals to represent time. 
De forma similar a como iteramos sobre nuestro algoritmo, iteraremos sobre la función anterior para mejorar su legibilidad utilizando java.time.
Ajuste de los parámetros de las funciones
Observe que la función anterior toma Long
como parámetro, y el parámetro timestampInSeconds
enfatiza la unidad en el nombre del parámetro. Sin embargo, no hay ninguna garantía en tiempo de compilación o de ejecución de que lo que se pasa a esta función es una marca de tiempo con segundos como unidad. Por ejemplo, no hay nada que impida a un usuario desprevenido introducir un valor incorrecto, como el siguiente:
val myTimestampInMinutes = 1000
val myIncorrectResult = getEstimateInSeconds(myTimestampInMinutes)
The above code will compile and run without issue, despite there being an obvious bug. One way the java.time library helps us is by innately capturing time units in its objects. We can improve this function by using java.time’s Instant
para representar marcas de tiempo.
fun getEstimate(timestamp: Instant): Long {
    val now = Instant.now()
    return if (timestamp.getEpochSecond() < now.getEpochSecond()) {
        timestamp.getEpochSecond() + 60 * 5
    } else {
        timestamp.getEpochSecond() - 60 * 60 * 3
    }
}
Now that we’re passing a java.time object into this function, the function doesn’t need to make any assumptions about the time unit used for the input. We can retrieve the time unit we need with getEpochSecond()
o toEpochMilli()
. Además, los Instantes proporcionan una función conveniente para obtener un objeto con la hora actual con Instant.now()
.
Sumar o restar tiempo
When working with timestamps, it’s common practice to add constant values of time. If the timestamp is in seconds, adding five minutes is frequently represented as 60 * 5
. Esta práctica no sólo es propensa a errores, sino que también se vuelve ilegible cuando empezamos a tratar con lapsos de tiempo más largos, como las horas. Podemos mejorar aún más la representación de los intervalos de tiempo introduciendo la función Duration
object. 
fun getEstimate(timestamp: Instant): Instant {
    val now = Instant.now()
    return if (timestamp.getEpochSecond() < now.getEpochSecond()) {
        timestamp.plus(Duration.ofMinutes(5))
    } else {
        timestamp.minus(Duration.ofHours(3))
    }
}
En Duration
representa una cantidad de tiempo basada en el tiempo, como cinco minutos, escrita como ofMinutes(5)
in the above code snippet. Now that we’re dealing with the Duration
object, we don’t need to convert the timestamp back into the primitive as we can just use the built in plus
y minus
y devuelve el resultado Instant
object. 
Comparación de marcas de tiempo
Hay una mejora más que podemos hacer porque todavía estamos convirtiendo en primitivas con el getEpochSecond
y comparando el resultado con un operador de comparación. El sitio Instant
ofrece una forma cómoda y legible de comparar un objeto Instant
object with another: 
fun getEstimate(timestamp: Instant): Instant {
    val now = Instant.now()
    return if (timestamp.isBefore(now)) {
        timestamp.plus(Duration.ofMinutes(5))
    } else {
        timestamp.minus(Duration.ofHours(3))
    }
}
Comparar marcas de tiempo es lo mismo que determinar qué marca de tiempo es anterior o posterior a la otra. Podemos utilizar la función isBefore()
o isAfter()
functions to make this comparison without having to convert back into primitive types. 
Conclusión
Empezamos con este código:
fun getEstimateInSeconds(timestampInSeconds: Long): Long {
    val now = System.currentTimeMillis()
    return if (timestampInSeconds < (now / 1000)) {
        timestampInSeconds + 60 * 5
    } else {
        timestampInSeconds - 60 * 60 * 3
    }
}
Y terminó con esto:
fun getEstimate(timestamp: Instant): Instant = {
    val now = Instant.now()
    return if (timestamp.isBefore(now)) {
        timestamp.plus(Duration.ofMinutes(5))
    } else {
        timestamp.minus(Duration.ofHours(3))
    }
}
While these functions do the exact same thing, the latter is more easily read. For a code maintainer, the purpose of this function is more easily understood, leading to less time spent trying to read the code. When quick iteration is vital to an engineering team’s efforts, code readability is a small but worthwhile investment. Migrating from primitive types to a time library for time coding is one way to improve code readability.
Fotografía del encabezado por Fabrizio Verrecchia en Unsplash.