Handling dates in JavaScript is notoriously difficult. You have to account for local timezones, daylight savings implications, the grand “epoch” of 1970, and milliseconds conversions. Does your head hurt too?
In a recent code challenge for Advent of JavaScript 2023, I was trying to figure out how many days and weeks from the current Date a particular holiday party was. My initial calculations were frustratingly inaccurate, but I uncovered a few tricks that I hope will help you too.
## Expected Payload
The Secret Santa app code examples for Advent of JavaScript 2023 are set up using RedwoodJS. As such, form data is managed behind the scenes with React Hook Form and dates are sent to the server in ISO date format:
```json
// Expected date payload from React Hook Form
2023-12-23T00:00:00.000Z
```
Hold up! Don’t run away. Let’s break this string down:
**An important note:** my application specifically uses the `<DateField>` component from React Hook Form which selects a date at midnight UTC +00:00. If we used the `<DatetimeLocalField />` component from React Hook Form instead (which uses `type="datetime-local"` for its input element), it would send a date at a specific time selected by the user adjusted to their local timezone.
## Quirks of Local Time
By far the easiest way figure out the time between two dates is to compare them both in terms of the same timezone. This, however, is where things start to take a left turn.
Take `Date.now()` or `new Date()`, for example. These methods return dates adjusted to your computer’s timezone. For me that is EST, but it may be different for other users.
The main issue here is that if we are comparing the current date adjusted for a user’s timezone and a date not adjusted for their timezone (i.e. the date of the holiday event stored in the application), our results will likely be inaccurate.
For instance: Saturday, 08:00 PM EST (UTC -05:00) is actually Sunday, 01:00 AM GMT (UTC +00:00). So if you’re trying to calculate the days until an event, you are likely to be over or under by a day, depending on your local timezone.
It would, therefore, be best for the application to convert stored UTC +00:00 dates to the local time first. But how do you do that?
## Swing and a Miss
JavaScript comes with some powerful `Date` APIs, but they are unfortunately unpredictable at times and can be challenging to use.
Say, for instance, that a user chose an event date of December 23, 2023. React Hook Form will send the ISO string `2023-12-23T00:00:00.000Z`, since we are using its `<DateField>` component (see “Expected Payload” section above). This is ******mostly****** correct in that the year, month, and date are what the user expects. The timestamp, however, may not match the user’s timezone like we need it to in order to properly perform Date calculations.
We could try to take the ISO string and transform it into a date object, which automatically converts it to our timezone:
```jsx
// Expected date payload from React Hook Form
const payload = '2023-12-23T00:00:00.000Z'
const payloadAsDate = new Date(payload)
console.log(payloadAsDate) // Returns 'Fri Dec 22 2023 19:00:00 GMT-0500 (Eastern Standard Time)'
```
Uh oh, did you see what just happened? Our `payloadAsDate` object returns the 22nd of December, not the 23rd! While technically correct due to timezone conversions, this is not what the user expects to see as their event date.
What can we do to convert the stored date to midnight local time on the correct day?
Thankfully, ISO date strings are standardized and we can expect the same pattern every time. What if we could pull out the year, month, and day from our payload and ignore the timezone? Let’s give it a shot with String methods:
```jsx
const payload = '2023-12-23T00:00:00.000Z'
const payloadSplit = payload.split('-') // Returns an array of substrings, one substring before each '-' character
const payloadYear = payloadSplit[0]
const payloadIndexedMonth = payloadSplit[1] - 1
const payloadDate = payloadSplit[2].split('T')[0]
// Use the payload's year, month, and day to create a new Date object, set to midnight at local time
const payloadToLocalDate = new Date(
payloadYear,
payloadIndexedMonth,
payloadDate,
0,
0,
0,
0
)
console.log(payloadToLocalDate) // Returns 'Sat Dec 23 2023 00:00:00 GMT-0500 (Eastern Standard Time)'
```
Huzzah! Our current date from `new Date()` and our payload date are in the same timezone. Now, how do we compare the two dates programmatically?
First up, we need to convert our current date to midnight to make for more sensible comparisons. We will take an approach similar to what we did when converting the payload to local time at midnight. We are only doing this since our users are not concerned with an event time; they simply want to see how many days and weeks remain until their upcoming party.
```jsx
const today = new Date()
const thisYear = today.getFullYear()
const thisDate = today.getDate()
const thisMonth = today.getMonth()
const todayAtMidnight = new Date(thisYear, thisMonth, thisDate, 0, 0, 0, 0)
```
Next, lets convert both of these dates to milliseconds to perform mathematical operations that we can’t do with Strings. For our example, let’s assume that the current date is December 22, 2023 and that the event date is December 23, 2023:
```jsx
const todayMs = todayAtMidnight.getTime() // Returns 1703221200000 ms
const payloadMs = payloadToLocalDate.getTime() // Returns 1703307600000 ms
```
By taking the difference between the two, we can determine how much time in milliseconds it is until our event. To then get the number of days remaining, we can divide the milliseconds remaining by the total number milliseconds in a day:
```jsx
const secToMs = 1000
const minToMs = 60 * secToMs
const hourToMs = 60 * minToMs
const dayToMs = 24 * hourToMs
const msUntil = payloadMs - todayMs // Returns 86400000 ms
const totalDaysUntil = msUntil / dayToMs // Returns 1
```
Now let’s go one step further. Our users want to see the time remaining in terms of weeks and days. The number of weeks makes more sense as an Integer and not a Float (e.g. a number with decimals). So let’s divide our `totalDaysUntil` by 7 (the total number of days in a week) and round down.
```jsx
const weeksUntil = Math.floor(totalDaysUntil / 7)
```
If `totalDaysUntil / 7` is a float, the numbers after the decimal demonstrate that a certain fraction of a week remains in addition to the total weeks before the decimal. Floats are yucky and, thankfully, JavaScript gives us the ability to find the remainder of days as an Integer using its remainder operator.
```jsx
const weeksUntil = Math.floor(totalDaysUntil / 7)
const daysRemaining = totalDaysUntil % 7
```
So, imagine that `totalDaysUntil` is 9. If we divide those 9 days by the total days in a week of 7, the quotient represents the number of weeks (1) and the remainder (2) represents the number of days. Give your brain a high five!
By this point you may be wondering why you’d approach a problem like this with Vanilla JS. After all, it is a lot work to do it this way.
One reason I like this exploration is that it forces me to stare the date monster square in the eyes 👾. Dates are really cumbersome in JavaScript and I am always curious how things work under the hood.
The question remains, though, about other approaches to handling and comparing dates in JavaScript. Here are a few ones that come to mind:
Obviously, there are quite a few ways to approach dates in JavaScript. Whichever option you choose, I encourage you to understand what’s happening behind the scenes so that you can get a better sense of how dates are handled in JavaScript.
Are you ready to build something brilliant? We're ready to help.