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 supports 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 find 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
How hard it would be to create a date in a given timezone?
. 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
, 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.
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!
, 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.
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 human 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.
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 the specific moment in time. This number represents the number of millisecond from an
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 millisecond 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 necessary 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 lost! 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
. When adding days, months, years, we should add directly to that component instead of adding the milliseconds. With that, we circles 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.
Format a date with timezone
All libraries leverage the native Intl API
Create date from date components & timezone
date-fns functions don’t support timezone;
date-fns-tz provides rudimentary supports like formatting dates in timezone.
Handle Daylight Saving Time and other timezone shifts
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, now 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.
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.
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 recompute 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 2 years, it might be read as a different date when the change is in-effect. Storing timestamp is
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 result. 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:
It offers a simple & easy to use API that fellow developers can use without having to think about timestamp and offset. Behind the scene, the expected date string & timezone are mapped to a validated timezone offset, based on source like
website, producing an exact expected date. When a test fail, 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 date are tested in our code:
Example of unit test changes.
The project has been a journey through the twists and turns of time! I’ve learned more about date, timezone and offset that 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 make 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 in your website or platform, pick luxon and never turn back!
Use 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.
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