5…4…3…2…1…Happy New Year!
It’s midnight, January 1st, 2021. The ceremonial ball has dropped in Times Square and 2020 has just come to a close. As the festivities begin to wind down, your phone buzzes. It’s a text from your friend in Los Angeles.
“My wife just delivered our baby! It’s a boy!”
What is the baby’s birthday?
It’s pretty obvious to me that the hypothetical baby above will celebrate his birthday on December 31st, not January 1st. At the moment of his birth, it was still 2020 on the west coast, and it would be for another three hours. Even if that baby should someday move across the globe; for example, to London, where (at the moment of his birth) it had already been 2021 for four hours, his date of birth will still be December 31st, 2020.
Javascript, it seems, would disagree. I won’t go into depth on the many failings of its implementation but the fundamental issue is that it treats dates as time. Each JS Date object is defined as some number of milliseconds since the UNIX epoch, Jan. 1st 1970 UTC (frustratingly, UNIX time is measured in seconds since the same moment). This value can be somewhat straightforwardly translated into local time in any timezone, but therein lies the very problem; there are many cases where a date or time value should be relative, not absolute, and JS has no support for this whatsoever.
There are quite a few libraries out there to make the Date API a little nicer to work with when it comes to adding/subtracting values for instance, but getting the time in an alternate timezone is still very fiddly, and none of these libraries seem to have any interest in a simple “Time” object, or an alternate “Date” object that represents a calendar date vs a universal moment in time. I’m also not about to write my own library from scratch; I’m not completely insane.
This post is mainly about dates, not times, but this kind of approach is very applicable to both cases; imagine if you set an alarm on your phone for 7:30am, but when you moved to the next timezone over, your alarm would go off at 8:30am instead because it was still thinking in terms of the moment you originally set it.
Moyase is fundamentally about tracking work done on a daily basis. Originally, I would clamp all dates to midnight, theoretically giving me a consistent value to store and index on. Unfortunately I realized partway through development that all it would take to break this system and give me orphaned data would be to log some work, drive a few hours west, then log some more — “midnight today” would be one hour different between those two checkins, and my work history would be corrupt.
So here’s how I’ve “solved” this problem:
- All persistent objects that need to deal with dates store them as a simple string in the format “YYYY-MM-DD”. In the code, I refer to strings in this format as an object type called YMD.
- When I need to get a YMD, I start with a date in the current timezone, then convert it to an ISO 8601 string and take the first 10 characters. UPDATE: Even this approach still bit me in the end! Check out this followup post.
- The reverse is just as simple; I split the YMD string and create a new Date from its components (the fact that the Date constructor takes a 0-11 index for month value must be named and shamed here)
- That’s it.
The libraries I use to display and choose dates are all based on local time, but that’s fine — just like in the example of the baby’s birthday, I don’t actually care about the date anywhere else on earth besides where I am right now. If I define a goal as spanning the entire month of September, I don’t expect the range to shift twelve hours when I fly to Japan. The key thing that allows me to approach the problem this way is that, despite the fact that Moyase is built with web technologies, it’s not an internet app. All data is stored locally; there’s no multi-user support whatsoever. Solving this problem in a way that takes that wrinkle into consideration would be much more difficult (and is actually something we spent many hours pondering at my last job designing a financial compliance platform).
While writing this article I came across Temporal, which is an attempt to provide a brand new API that would solve literally every problem I faced here. I'm very interested to see progress made here, but at the time of writing it is literally not supported on any browser and the proposal has been in progress since 2017. Hopefully someday we'll all be able to use Temporal and this post will look hilariously outdated. Until then, this approach works for my purposes.