Talks

Which Time Is It?

Which Time Is It?

by Joël Quenneville

In the RubyConf 2023 talk titled 'Which Time Is It?', Joël Quenneville delves into the complexities of time representation within programming, particularly in Ruby. He emphasizes that 'time' is not a singular concept but encompasses various distinct notions that programmers must understand to avoid bugs and enhance code clarity. The talk outlines key differences between moments and durations in time representation:

  • Moments vs. Durations: Moments are specific points in time, such as 'November 14, 2023, at 10:30 AM PST,' while durations represent lengths of time, like '45 minutes.' Understanding this distinction is crucial while designing code that interacts with time.

  • Ruby Time Classes: Ruby provides classes like Time, DateTime, and Date that represent moments, but it lacks a built-in representation for durations, which often leads developers to use numerics instead. Quenneville explains how each of these classes has different resolutions—Time has nanosecond precision while Date only goes down to one day.

  • Mathematical Operations with Time: Quenneville explores the repercussions of attempting to perform arithmetic operations on time values. He illustrates the difference in behavior when using the plus and minus operators for time manipulation, pointing out that adding two Time instances yields nonsensical results, whereas subtracting them gives the duration between the two points.

  • Time of Day Concept: He introduces 'time of day' as a separate concept, unlinked from specific dates but still behaving like moments on a circular timeline. This idea proves important in various applications, especially when programming interfaces with third-party systems like PostgreSQL that manage time differently.

  • Real-World Applications: The speaker shares a project involving synchronization of time-series data with video playback, discussing challenges faced due to discrepancies in timestamp formats. This underscores the significance of precise time definitions in practical applications.

Concluding his presentation, Quenneville shares essential takeaways:
- Know which type of time you're working with, whether moments, durations, or a time of day.
- Utilize appropriate mathematical operations relevant to these types.
- Be mindful of the resolution needed for accurate calculations, as misunderstanding these concepts can lead to logical errors in code.

By the end, attendees are equipped with a nuanced understanding of how to model time effectively within their code, preventing bugs and facilitating more accurate and meaningful operations. The talk resonates with the Ruby community's need for clarity in time management within applications.

00:00:18.119 Good morning, everybody! We, the Ruby committee, will be introducing the speakers before each talk today and tomorrow. So, I'd like to introduce Joël Quenneville. Joël has been writing Ruby for over a decade and is the co-host of the podcast 'The Bike Shed,' where he discusses the technical and social issues surrounding our craft. Please give a warm RubyConf welcome to Joël!
00:00:31.199 Hello, everyone! Welcome. The current time is just a little after 10:30, and that's the easy answer to the question, 'What time is it?' But this morning, I want to ask a slightly different question: 'Which time is it?' Now, time has a variety of meanings in English. We use the term 'time' to refer to multiple different quantities that are similar and related, but also distinct. Choosing the right one for our code helps us design better and avoid bugs. A classic example of this is the difference between the code `1.day` and `1.day.ago`. If you have a method and you want to pass an argument—an instance of time—you need to choose between `1.day` or `1.day.ago`. Not only are these two different times, but they're also two fundamentally different kinds of time.
00:01:21.560 So, what's the difference between `1.day` and `1.day.ago`? Well, we're looking at two different kinds of time here. The first is what I'm going to call a 'moment.' This is a point in time, a value like 'November 14th, 2023, at 10:30 AM PST.' That's the moment when this talk started. But there's also a different kind of time, which I will call a 'duration'—an amount of time, something like '45 minutes.' That's the length of this talk slot.
00:01:47.880 Let's try to visualize a bit of the difference here, as it's not always intuitive. Imagine we have a timeline with tick marks that extends infinitely into the future and the past. Moments would be points on that timeline; they are discrete points. Durations, on the other hand, are distances. We have points and distances, and some of you might be getting really excited about vectors and one-dimensional vectors, but that's not what this talk is about. Although, if you're excited about these topics, come talk to me afterward; I would love to have that conversation.
00:02:35.440 So, this is RubyConf, and Ruby provides us several classes for working with time. The first is the `DateTime` class, which is used to represent moments—points in time—but I don't recommend that you use it, because as of Ruby 3, it's been deprecated. That's okay, though, because there are other classes available to us, including the perhaps confusingly named `Time`. `Time` also represents a moment, a point in time, and represents values like 'November 14th, 2023, at 10:30:00.123456789 AM Pacific Standard Time.' So why all those decimal points? Well, it's because time has nanosecond resolution.
00:03:22.560 Those two sound like fancy concepts, but really, all that means is that on our timeline, the tick marks are only one nanosecond apart. When we create two instances of `Time`, the closest they can be to each other is one nanosecond. Contrast this with something like the `Date` class. `Date` also represents a moment, a point in time, but it typically represents moments like 'November 14th, 2023,' without the additional details. This is a resolution of one day, so in this case, two instances of `Date` can be at closest one day apart.
00:04:03.600 Now, a fun thing about resolution: if you use `Date` or `Time` instances in a Ruby range, it will generate an array of values that are spaced out based on the resolution of the type you chose. You may not have known this, but if we take a starting date of November 12th and a starting date of November 11th, and put them in a range, the output would be November 12th, 13th, 14th, and 15th. This can be really convenient because if we used `Time` instead, we would have received a million instances representing all the nanoseconds between those two points. This can be very nice because you can play around with that and even create your own classes that represent different resolutions. One class I've created in the past represents 'months,' so there are still moments that are points in time, but the resolution is one month. This has allowed me to simplify my reporting by leaning into this syntax.
00:05:48.640 Now, this is not a talk about enumeration; I gave that talk last year at RubyConf Mini. If you're excited about what you can do with enumeration and ranges, look it up. It's on YouTube, called 'Teaching Ruby to Count.' One thing I want you to notice about all these classes we've been examining in the core library is that they all represent the same kind of time. We've looked at `DateTime`, `Time`, and `Date`, and they all represent moments. We haven't seen a single thing that represents duration yet; what's going on here? Well, core Ruby tends to represent moments using `Date` and `Time` classes and uses numerics for things like duration—numerics are values like integers and floats. This distinction becomes important when we start trying to do math.
00:07:07.200 Part of why it's really useful to have values that represent time in our code is that we want not just to represent them—we don’t want to have a string that we can display to people—but we want to perform math on these time values. We want to ask, 'How big is the distance between two points?' If we want another event far in the future, we need to ask, 'What time will that be?' We want to determine which of two events occurs before the other. We can do all this with math, but unlike regular arithmetic, where you might be used to saying we have four standard operators—addition, subtraction, multiplication, division—and any two numbers can be combined using those operators, time works a bit differently because not all math operators make sense.
00:08:54.440 We are going to dive a bit into the time documentation and look at some of the signatures for the operators. If you start reading that, you'll notice that they're a bit interesting. Starting with the `Time` plus operator, the signature states that you take a `Time` object and add it to a numeric value—this can be a float or an integer—and you get back a new `Time` value. The first thing to notice is that we can't add two `Time` instances together. That would have been my first assumption. When looking at the `Time` library's plus operator, you might think to do `Time A + Time B` and get back a new time, but that's not the case. What's actually happening is we take a `Time`, which is a moment, and add a numeric that represents a duration to it.
00:09:94.480 Now, why can't we add those two together? What is actually happening here with the plus operator? Whenever I encounter an operator and I don't understand the rules around it, I like to step back and ask the question: what question is this operator actually asking? What does 'plus' mean in the real world regarding a piece of time? In the case of addition, it asks, 'What point is 45 minutes after point A?' So on our visual timeline, we have point A, we have some distance, and we're essentially asking, 'What point is that distance shifted over to the right?' We're not adding two points together; we take a point and a distance—that's a moment and a duration—to get back a new point. If we think about adding two points visually, we start to realize that's nonsensical.
00:11:00.600 Now, under the hood, times are just numbers; we'll see a bit about that later. But it is possible to take a `Time` and convert it to an integer using the `to_i` method. So, if you really wanted to add two points in time, technically you could do `Point A + Point B.to_i`, and that would work, but you would get a totally nonsensical answer to whatever question you were asking. Don't do that. Sometimes, thinking a little about what the operators mean can help you get a better sense of what your program is trying to do or, even more so, what it shouldn't do. If you find yourself in a position where you're trying to add two points in time, maybe you need to step back and ask yourself not just what the operator is doing, but what your program is actually trying to accomplish. When you do this, you might realize you aren't trying to add two points. You're likely trying to do something else, such as adjusting an appointment for next week.
00:13:18.640 We've seen the plus operator, but the minus operator gets a little weirder. If I look through the documentation, there are two signatures: you can take a `Time` and subtract a numeric from it to get a new `Time`, or you can take a `Time` and subtract another `Time` from it to get a float. The first thing that jumps out to me is the two signatures. This is Ruby, right? We can overload operators and perform different actions depending on the argument type passed in. What are these two signatures actually doing with our time? As always, I like to ask, 'What question is this operator asking?'
00:14:12.119 In the case of that first signature, it's basically asking the inverse of what the plus operator does. The plus operator asks what point is 45 minutes into the future, while the minus operator is asking what point is 45 minutes in the past. We can see this visually: we have point A, a distance, and we're asking, 'What is that point shifted back into the past?' Just like with addition, it makes sense to think about that as having a point in time and a duration. But what about that second signature? This one is a bit peculiar. Remember that I said it's nonsensical to add two points together? Why can we subtract two moments from each other? You might expect there to be some sort of symmetry here, but there isn’t. Again, let's ask ourselves what this operator is doing. When you're subtracting two points in time, what you're actually asking is the duration between points A and B.
00:15:54.480 On our timeline, we have point A and point B, and the question is, how big is that distance between them? So, between these two operators, we have three distinct inquiries regarding the data: we have a starting point, an ending point, and a distance between the two of them. These operators allow us to say that depending on any two pieces of this data, if you want to find out the third, you can use the relevant math operator to get that answer. Some of you might be having flashbacks to high school algebra class; you're not wrong. We’re actually dealing with the same equation and just rearranging it to solve for a different value.
00:17:04.960 When adding, we say to find point 2, we can add point 1 and a duration. The first subtraction is solving for the origin point, where we can subtract point 2 to get the duration. Finally, if we're trying to find the duration between two points, we can subtract point 1 from point 2. The initial version of this talk, while I was putting it together, I thought about calling this 'An Algebra of Time.' I wanted to explore all the different ways that the math operators work and delve into some of the surprising edge cases where some combinations make sense and others do not. However, I figured not many people would want to attend that talk, so instead, I focused more on the idea of different kinds of time and building a better intuition around them.
00:18:56.560 One of the intuitions I want to build is why certain operations make sense while others do not. As we’ve seen, it’s the same operation, rearranged three different ways. Just by rearranging this equation, we can see the two different ways that subtraction works and the one way that addition works. I didn’t need to check the `Time` documentation; this equation illustrates everything for me. It also shows that while you can subtract one point from another, you can't add two points together.
00:20:14.080 Now, this is not the only set of math operations you can perform on a point in time; however, these are the ones I wanted to dig into for this talk. One interesting aspect is how the two types interact with each other. You're not just working with durations or points in time/moments with other moments, but while these two types are separate, they do interact with one another. We've seen different types of time already: moments, durations, and now we're discussing another kind entirely. This is a situation where you might want to model time without a date.
00:21:22.720 We talked about resolution, and how `Date` is like a `Time` class instance without the time aspect—it's just a date. But what if you need the opposite? What if you want a time without the associated date? You've seen a couple of different time classes: moments and durations. Now, the third type of time we’re discussing is 'time of day.' Time of day represents values like the generic idea of '5:30 PM.' This is time decoupled from a specific day. Time of day behaves like moments—they're points in time, but they’re on a circular timeline that loops every 24 hours.
00:22:62.080 How does time of day interact with the other types we've seen? Well, our three questions we examined earlier regarding points in time on a linear timeline still apply. We can perform addition, asking, 'What is a point that is 45 minutes in the future?' We can perform subtraction, asking, 'What is a point that is 45 minutes in the past?' Keep in mind, though, that this is a circular timeline, so your answer might loop around. Finally, we can again ask about the distance between points A and B.
00:23:36.160 Another thing we can do is take an instance of time—a moment that has precision, such as hours, minutes, and seconds—and convert that into a time of day. We can take a full timestamp and extract just the time of day for future calculations. Conversely, we can take a time of day, which is a generic concept like '5:30,' combine that with a date, and obtain a time. However, core Ruby does not provide us with a time of day class; instead, we have to rely on third-party solutions. Ruby has a great ecosystem of gems that provide us with various tools for working with different types of time.
00:24:43.600 One of the classic gems is ActiveSupport. If you've worked with Rails, you've used this. ActiveSupport provides us with the syntax for things like `1.day` or `1.day.ago`, as we saw earlier in this talk. So, what are `1.day` and `1.day.ago`? The method overloads on the integer class, but what it actually returns is an instance of ActiveSupport::Duration. So, `1.day` represents a duration—an amount of time—whereas `1.day.ago` represents a moment, a point in time. Not only do they refer to different times, but they are fundamentally different entities that are not interchangeable. Knowing which one to use is crucial when you're thinking about your code.
00:26:52.400 This gives us a very nice, human-readable syntax—some sugar for performing operations that seem intuitive. You take `Time.now` (that's a moment) and subtract some duration—like the number of seconds in a day—to get `1.day.ago`. But what about time of day? This is a gem that I really like: `jackctod`, which provides helpers for some of the math you might want to do. It facilitates conversion to and from other time types. If you're using this in a Rails app, it also offers helpers to register itself as a compatible type to align with PostgreSQL's time of day column type, as PostgreSQL supports a time of day column type alongside the other types we typically see used for time.
00:29:04.320 Now, I noticed a bug that I encountered recently. If you don't use a gem like this and you have a time of day column type in PostgreSQL, Rails by default tries to convert it into a `Time` instance in Ruby. This leads to converting two different types of time: transforming a time of day into a moment, which can result in some weird behavior, as all time of day instances get assigned today's date. When you start performing math or trying to compare them, you might get unexpected results. I was even chasing down a bug that stemmed from this issue, where the solution was to convert the column into instances of this time of day gem.
00:31:07.240 However, you can't always incorporate third-party code; sometimes you have to implement it yourself. So, how would you design your own time representation? I would advise against using human-readable integers like '130' to represent the time '10:30 AM.' While this might feel intuitive, it can lead to some frustrating issues. For example, it may allow you to store invalid times, like '3176.' I don't know when '3176' is—it’s nonsensical and invalid. Maybe we can implement validations around it, but the core problem is that it confounds mathematical operations. Remember, one reason time is valuable as its own type is that we want to perform math on it.
00:32:15.840 If we want to calculate the number of minutes between 10:25 AM and 9:25 AM, math will tell us it’s 100 minutes, but for all of you who passed first grade, that's not correct—the answer is actually 60 minutes. The problem is that clocks roll over at 60 while our decimal system rolls over at 100. This discrepancy leads to many problems. Another thing I recommend against is storing multi-part values like hour and minute. We try to represent human thought on time, which is often divided into hours, minutes, and seconds, but this breaks the underlying value down into multiple resolutions. In actuality, a point in time is a single value, not multiple.
00:34:27.840 So, what should you do instead? The classic solution is to store an epoch and a counter. An epoch signifies the zero point of your time, while a counter maintains a straightforward number—how many of whatever resolution you've chosen since that zero point. Let's look at a few real-world examples. A common one is Unix time, which is the number of milliseconds since January 1, 1970. January 1, 1970 serves as the epoch, and the counter holds milliseconds, providing the resolution since that zero point. Another example is PostgreSQL's time of day column, which stores the number of microseconds since midnight; once again, we have an epoch (midnight) and a counter with a resolution measured in microseconds. Finally, we have Ruby's `Date`, which has an interesting epoch: the year 4,713 BCE, with a counter storing days since that starting point.
00:36:40.400 If you are building your own, I suggest having a class that wraps one big counter, such as this: we could define the `time_of_day` class and wrap it around a single number, passing in the number of microseconds since midnight, storing that. Since time of day is circular, we can ensure we don't exceed the maximum number of microseconds in a day by utilizing modular math. Otherwise, this is just a Ruby class acting as a wrapper around a number. This is a pattern I appreciate: when there are numbers that require some special operations, encapsulate them in a class to abstract all that complexity cleanly. Integers are effective, but Ruby is designed for object-oriented programming.
00:38:53.560 Because passing in microseconds since midnight may not be intuitive for humans, we can create a custom constructor that allows us to pass in hours, minutes, and seconds in the way people commonly understand. Under the hood, this constructor can handle the math to convert everything into microseconds before passing the value into the main constructor. Additionally, we may want to implement a few mathematical operators: for example, using the `plus` operator, we pass in a duration. Remember, when we perform addition on time of day or moments, we are not adding another moment but a duration to advance into the future. We can achieve this by adding the new duration to the underlying counter and returning that value in a new instance—there's no need to transfer values from one column to another or manage any modular math, which is handled by the constructor.
00:39:28.520 This single counter approach truly shines when you’re trying to implement any kind of time-related logic. I’ve done similar implementations for moments, with a resolution of months, and they integrate seamlessly with Ruby's range functionality, making them very compatible with the Ruby ecosystem. I want to talk briefly about a project that enhanced my understanding of the nuances of working with time.
00:40:20.680 This project involved working with a researcher conducting studies. Participants would take part in mini therapy sessions, wearing multiple sensors and being filmed. She collected time-series data from various metrics like heart rate and skin sensitivity, along with video footage. She wanted to identify interesting portions of her data to conduct further quantitative analysis. The project I built consisted of a series of time-series graphs synchronized with video playback. Each stream had a scrubber, enabling viewers to scroll the video while moving a scrubber across time-series data or vice versa.
00:41:43.960 This functionality allowed you to say, 'Oh, there's a spike in heart rate; I wonder what happened there.' By moving the scrubber, you could see that someone walked into the room, causing the participant's heart rate to spike. Everything in this setup was time-based, but the time series data used Unix timestamps, which represent milliseconds since January 1, 1970. However, synchronizing this with video footage was a bit more challenging, as timestamps from the video were in seconds since the start of the video. One potential issue was the difference in precision across the two: milliseconds versus seconds, which sometimes led to serious mismatches.
00:43:01.240 Moreover, trying to align video timestamps, which are relative to the beginning of the video, with timestamps anchored to January 1, 1970, was tricky. For instance, when we wanted to highlight an interesting portion of data, we needed to export the specific time series associated with that moment, which required a start point, an end point, and a duration between the two.
00:44:34.560 This is where proper temporal calculations were essential! I ran into instances where I was working with the wrong units during my time calculations, leading me to export more data than intended or sometimes failing to synchronize correctly across different graphs. So, when working with time, I like to keep a few questions in mind. First of all, what kind of time do I need? Am I dealing with a moment, a duration, or a time of day? Knowing what type of time I’m working with allows me to better model the problem and select the right operations.
00:45:36.319 Then I consider what operators are applicable: what kind of operation should I be using, and does it even make sense? If you're trying to add two points in time, maybe it’s time to re-evaluate what your program's intention is. If you come across a mathematical operation where the library you’re using doesn’t support the operation you’re trying to perform or the types don’t align correctly, it may help to step back and reflect on the purpose of the operator being used. Also, think about what your program's objective is.
00:47:06.160 Finally, considering resolution is useful: what degree of precision do you need? Are you looking for something that accounts for nanoseconds, or do you need daily, monthly, or yearly resolution? Keeping these factors in mind can greatly aid in both the conceptualization of the problem you're aiming to solve and the accuracy of your implementation.
00:48:32.440 Thank you for joining me today to explore some ideas about time! My name is Joël Quenneville. I'm a Principal Developer at BBot, and I’m also a co-host on the podcast 'The Bike Shed,' which you can find at bikeshed.fm. You can find me on social media platforms as @JoëlQuenneville.
00:48:48.000 Thank you very much!