Bringing Coda up to date: 25-hour days and other things I learned when migrating from moment date library

Escapades in JavaScript date/time handling.

Hung Doan
Coda Blog
10 min readMay 16, 2022

--

Date is a common concept — we use date and time every day when talking with each other. However, I realized I knew next to nothing about its technical aspects until I worked on improving support for international dates on Coda. We had a mysterious bug: there were reports from a few users that our Date operations, on some rare occasions, produce Date results that are off by a few minutes. Below is an example where a Coda doc is set to Asia/Kuala_Lumpur timezone and a user finds that a date they entered is off by 9 minutes when referenced in another column.

The same object, but somehow off by 9 minutes.

This is quite bizarre. How could the same value show up differently in another place? I even double-checked that the underlying value was the same. It turned out it was because of a bug with the moment, the date library we’ve been using at Coda.

moment was everyone’s date library of choice, but it has fallen behind and is no longer actively maintained. As we invest in improving support for international users, we wanted to build on a stronger foundation. I kicked off work to replace the library, but little did I know what I would get myself into. Read on to hear about my escapades in javascript date/time handling and what I found the best library to be.

How hard it would be to create a date in a given timezone?

Coda supports various Date formulas. Makers can create dates from components, add and subtract date, and add custom date inputs that should all work in the user-chosen timezone. In order to support makers around the world, we had to find a library that supports a wide set of date time operations for every timezone.

The first test I did was to create a date with different date components (year, month, day) and time components (hour, minute, second) in a specific timezone. It seemed like a simple task until I understood how timezones work. Let’s take a step back to understand the fundamentals of date, time and timezone and why it’s a challenge to handle.

Date, timezone, and offset

If I ask you which date and time it is currently, I’m sure you can answer it without blinking, something like “It’s March 25, 2022, at 3pm.” If I relay the same information to someone on the other side of the Earth, would they agree? No! They would say that it’s March 26, 2022, at 5am. It’s a different time of day and even a different date for them. Although we’re at the same moment in time, the date components are different! To communicate this difference, we usually denote the date with a timezone, and we can convert the date components from one timezone to another based on their timezone offsets. The offset is relative to UTC, a standard time recognized around the world. I’m in California, USA, so my current timezone is PDT (Pacific Daylight Time), and it’s -7h from UTC time. Someone in Vietnam is in Indochina Time, which is +7h from UTC time.

Javascript comes with a standard Date object, but it comes with a severe limitation: no support for custom timezone (it implicitly uses the user’s local timezone). We would have to go through a few hoops in order to create a date a specific timezone with JS Date. Specifically, we have to find out the timezone offset of the date in the desired timezone. And here is why it’s not easy: timezone offset of the same location may change over time. A famous example is when Samoa switched time zones and effectively redrew the International Date Line. There is also a recurring version of this that’s familiar to people in the US and many countries: Daylight Savings Time. Every year, there will be about 5 months where California (America/Los_Angeles) timezone has -8 hours offset, and 7 months where the offset is -7 hours. To make things even more confusing, the day that it happens changes every year!

Fun fact: Standard time zones are introduced fairly recently in the history of humankind! Before 1883, every city in the U.S. had its own time, and the time in each city could vary by a few minutes. It created so much problem with train scheduling that the Railroad companies started using standard time zones and the rest is history.

It’s complex to construct a date in the right timezone, and not every Javascript Date library out there supports it.

How hard would it be to add one day to a date?

Another building block of date manipulation on Coda doc is adding or subtracting from a date. Adding one day to a date is a trivial task for humans to do. One day after March 25, 2022 should be March 26, 2022, and one month after this date would be April 25, 2022, right? What would be so hard about it? Let’s explore this problem through the lens of a computer system.

Unix timestamp

When given a date, the computer doesn’t store 6–7 different numbers representing the different date components. It’s smarter! It stores a single number, a timestamp, representing a specific moment in time. This number represents the number of millisecond from an epoch moment in time (a common one is 1/1/1970 00:00:00 in UTC). Every moment has a unique timestamp, no matter which timezone it is. Based on the timezone offset, this number can be converted to the time at a given timezone and derive the date components.

It’s simple and effective at reducing the complexity of storing time. But not when you want to do some math with it. A naive approach to date math would be adding the number of milliseconds to the timestamp. It quickly fails when you realize that:

  • A year doesn’t always have 365 days
  • A month doesn’t always have 30 days
  • And a day doesn’t necessarily have 24 hours.

Wait, what? A day doesn’t always have 24 hours? That’s right. Remember that DST may change a timezone offset forward or backward by 1 hour? Let’s look at California DST changes this year:

On March 13, 2022, the Pacific Time (PT) timezone switches from being -8 hours from UTC to -7 hours from UTC. This day loses 1 hour because after 01:00 AM, it will be 03:00 AM! Worry not, on the DST end day, November 6, 2022, the day will have 25 hours to make up for the loss! To everyone who wishes for 25 hours a day: your wish is granted 1 day a year. Cheers!

To everyone who wishes for 25 hours a day: your wish is granted one day a year!

A better way to approach date math is differentiating calendar math and time math. When adding days, months, years, we should add directly to that component instead of adding milliseconds. With that, we circle right back to the first problem, which is creating a date from specific components.

Let’s take a moment to appreciate the challenges of handling timezones:

What is the best date library for timezone handling?

It took me awhile to understand the challenge at hand and, frankly, I don’t want to be dealing with this madness. Thankfully, there are kind souls out there who has solved these challenges and made their code open sourced. I looked at popular open-sourced date libraries out there and tested how they faced against the tasks of handling timezones.

luxon was the clear winner! Join me in celebrating the existence of luxon! If you don’t have to deal with complex timezone logic, that’s fantastic; the other libraries may work for you. But if you do, you know which library to go to. Before I understood the complexity of handling timezone logic, I tried to compare the libraries based on fancy features like tree shaking, bundle size and so on. But now, I’m content that there is even something that handles all the messiness of timezone as well as luxon. In fact, I learned a lot about timezone math from luxon‘s fantastic documentation.

Moment strikes back!

We found the ideal replacement for moment, and it was luxon. It was time to migrate our data and codebase off the old library to the new one.

Now I can explain to you what caused the bug we saw at the beginning of the post. moment‘s approach is to bundle the timezone offset data with their library (in a different bundle called moment-timezone). However, it doesn’t and is not practical to bundle all the available timezone offset data. This creates a difference with modern Javascript Date formatting API supported by most browsers nowadays, Intl, which has a complete offset data bundled with it. Since Coda used Intl API to format a date, the same timestamp in moment is presented as a different time in Intl, hence the bug we observed. We don’t have this problem with luxon because they use Intl API under-the-hood for timezone offset data.

This brings up a migration challenge: if we simply swap out moment with luxon, many of our dates would stay wrong. We have to rewrite the timestamp that was handled wrongly by recomputing the timestamp with luxon. In general, if you migrate from moment to any recent date libraries, you may have to do this too, because they all use Intl API under-the-hood now.

Now, this “timezone offset change” actually can happen in reality without changing the date library. In fact, the US Congress is considering making DST time permanent in the U.S. If a date is converted to timestamp, stored in a database and stashed away for two years, it might be read as a different date when the change is in-effect. Storing timestamp is not a silver bullet. Ideally, we should store a pair of timestamp and timezone offset to uniquely capture a moment in time at the time it’s created.

One more thing: date unit tests

One of the challenges I faced when migrating our code was date-related unit tests. There are unit tests for our date formulas and date utility functions. Since they are tested against multiple timezones, the result is usually timezone-dependent, and it’s a challenge to write the expected result. We ended up with various ways of writing imprecise expected results. Here are a few examples:

They are imprecise because depending on the offset data used (moment API, Intl API, JS Date API), the timezone offset for a given Date & timezone may be different, and the resulting date string may be different. They become a challenge when removing moment because there are a lot of confounding factors that relate to a test failing, such as a bug, converting function, or data specific to moment.

How might we achieve both simplicity of writing a unit test and the precision of the test? I spent a lot of time pondering this question, and this is where the fundamentals we learned above become useful: we need precise date components and timezone offset for the expected date. Here is a desired, standardized API that I created that offers a simple and easy-to-use API that fellow developers can use without having to think about timestamp and offset.

Behind the scenes, the expected date string & timezone are mapped to a validated timezone offset, based on sources like TimeAndDate website, producing an exact expected date. When a test fails, we would print out the differing component, making it simple to spot the difference between the actual date and the expected date. We can unify the way dates are tested in our code:

Example of unit test changes.

Takeaways

The project has been a journey through the twists and turns of time! I’ve learned more about date, timezone, and offset than I ever wanted, but I can be the go-to person in the team for date-related matters now. The learning also made it a rewarding journey! Coda now has made away with bizarre Date bugs and has a better foundation to build more date time support for our users.

Here are a few useful takeaways:

  • If you are serious about timezone support on your website or platform, pick luxon and never turn back!
  • Use a pair of timestamp and timezone offset to uniquely represent a moment in time. Timestamp alone is not enough, and timezones don’t have a fixed offset.
  • Lastly: Date & Time handling is complex, avoid it if you can!

I was naive about the complexity of this problem when I jumped in, but hopefully, by sharing the knowledge here you’ll come to the problem more prepared than I am!

And if you are interested in deeply challenging technical problems like this, come join us; the Coda team is hiring!

--

--

Hung Doan
Coda Blog

Software Engineer at Coda.io. Passionate about building tools that empower people!