A Quick Introduction to Date Comparison in JavaScript
Here’s a first: a CodeMom story from the workplace! I had a story to work on this past week that involved doing some date comparison to determine whether or not a user’s registration had expired. My solution was to make an API call to bring back a user’s existing registration data and compare the expiration timestamp on their registration to the current date and time in order to decide whether or not they could be added to a group outside of their current placement. The app I’m working on is written in TypeScript, a typed wrapper around the JavaScript programming language.
Now, I’ve been writing JavaScript, off and on, for… a pretty long time. I started making web sites in junior high school–way back in the late 90’s–and JavaScript was first released in 1995. I’d say the first time I wrote JavaScript myself was probably in 2004 or 2005, although I didn’t get serious about it until around 2015 (y’know, with that whole ‘full time software developer job’ thing).
Would y’all like to know how many times I had to change, rebuild, and redeploy my code for this one simple date check?
Five. Five times! It was embarrassing.
Here’s the thing: JavaScript is kind of a funky language. It’s dynamically-typed, executed at runtime, and has no concept of integers. JavaScript lets you get away with a lot, which is why it’s one of my favorite languages–but sometimes, its loosey-goosey nature can actually be a drawback. That’s what I ran into with the date comparison.
Creating Date Objects
JavaScript has a ‘Date’ object; it stores date and time information in milliseconds since the Unix epoch. Creating a Date object in JavaScript is pretty easy. Just new up a Date object, and you’ll receive the current date and time:
let today = new Date();
You can also pass a string into the Date constructor, to have a Date object made from something like a timestamp:
let expiration = new Date('2024-08-05 16:43:03:221');
Now–remember how I said that Date objects store time in milliseconds since epoch? You may think that means that Date objects are all coordinated from a central moment, but Date objects are actually time-zone aware, and will adjust for the current system timezone. That means that the today variable above, when created in my browser, reflects the date and time in US Eastern Time (UTC -05:00) or Eastern Daylight Time (UTC -04:00), depending on the time of year.
console.log(today); // Tue Aug 06 2024 10:24:47 GMT-0400 (Eastern Daylight Time)
On my second attempt at comparing the expiration timestamp from the API call and the current date and time, I ended up being off by four hours, because the timestamp being returned from the API was a string representing a timestamp in UTC–and the conversion to a Date object placed it in EDT instead. (My first attempt? I typo’d the comparison operator when cleaning up my code to push. That was my own dang fault, and had nothing to do with JavaScript.)
console.log(expiration); //Mon Aug 05 2024 16:43:03 GMT-0400 (Eastern Daylight Time)
You can see how the above timestamp is the same as the one passed into the expiration variable, but now it’s incorrectly assumed that the time is in EDT. Strikes one and two for me.
Converting Dates to LocaleStrings
At this point, I was off for a long weekend, having booked a speaking engagement at a conference in another state. So my team’s junior developer fixed the four-hour bug by converting the Dates to LocaleStrings, and passing in the format and timeZone option to convert the current Date object to UTC. If you’re not familiar with the .toLocaleString()
function, it returns a Date object as a string, using your system’s locale settings.
new Date(returnedTimestamp).toLocaleString(); // '8/5/2024, 12:43:03 PM'
today.toLocaleString(); // '8/6/2024, 10:24:47 AM'
today.toLocaleString('en-US', {timeZone: 'UTC'}); // '8/6/2024, 2:24:47 PM'
This solved the bug, but another senior developer pointed out that converting from strings to Dates and back to strings is not efficient–which left me to solve the bug in a more economical way when I came back to work. Strike three belongs to my junior dev.
Numerical Value of Date Objects
Date objects in JavaScript are backed by a numerical value, which represents the number of milliseconds since Unix epoch. To get that value, simply call the .valueOf()
function on the Date object:
today.valueOf(); // 1722954287707
expiration.valueOf(); //1722890583221
You can put those values into https://www.epochconverter.com/, and see the timestamp in both UTC and your current time zone.
I thought using the numerical backing value of the timestamps might get around the time zone issue, but no–I was still four hours off. This is because no matter what you’re doing for comparison in a client, if the value on the server is being recorded in UTC, you’re still going to be incorrect by however many hours your current time zone’s UTC offset is.
In other words, I’m a genius (and also, wrong). Strike four for me.
The Benefit of Paying Attention (also, ISO Standards)
One of the hangups involved in working on this story is that my request for access to the database where the user registration is stored is still pending, so I was relying on logging data to the console in order to see what I was working with. If I were smarter, I would’ve logged the string being returned from the API immediately, instead of converting it to a Date object before logging it.
let returnedTimestamp = '2024-08-05 16:43:03:221Z';
I’m far from an expert on timestamps, but I’ve been around long to recognize that particular format–it’s very close to ISO 8601, an internationally-recognized standard for presenting date and time information. No matter where you are in the word, a date and time in ISO 8601 will be expressed as year, month, day, hour, minute, seconds, and milliseconds. ‘Z’ is used to designate that the timestamp is expressed in UTC, with zero offset for time zone.
Luckily, there is a handy function you can call on a Date object to provide an ISO 8601 string built from that Date:
today.toISOString(); // '2024-08-06T14:24:47.000Z'
The .toISOString()
function always returns a ISO 8601 timestamp, converted to UTC… which, coincidentally, meant that it was almost an exact match for the timestamp returned from the database. The only difference was the missing “T” demarcation between the date and the timestamp–the string returned from the database didn’t have it.
However, knowing what I know about the way JavaScript handles string equality, I could still write this code and have it do the comparison without having to modify the string returned from the API:
if(returnedTimestamp > today.toISOString()) {
// the registration has expired
}
Why? Hello, Unicode!
How can I successfully compare two dates if they’re presented as string data?
JavaScript does string comparison in lexicographic order; each character in the reference string is compared against its same index value in the comparison string. Comparison is based on each character’s Unicode index, or code point–a special value denoting the encoding of each character. Here is a table with digits 0-9, as well as some other characters’ Unicode code points:
Unicode Character | Value | Character Name | Code Point |
0 | 0 | Digit Zero | U+0030 |
1 | 1 | Digit One | U+0031 |
2 | 2 | Digit Two | U+0032 |
3 | 3 | Digit tdree | U+0033 |
4 | 4 | Digit Four | U+0034 |
5 | 5 | Digit Five | U+0035 |
6 | 6 | Digit Six | U+0036 |
7 | 7 | Digit Seven | U+0037 |
8 | 8 | Digit Eight | U+0038 |
9 | 9 | Digit Nine | U+0039 |
۲ | 2 | Extended Arabic-Indic Digit Two | U+06F2 |
߄ | 4 | NKO Digit Four | U+07C4 |
Ⅺ | 11 | Roman Numeral Eleven | U+216A |
A | Latin Capital Letter A | U+0041 | |
a | Latin Small Letter A | U+0061 | |
B | Latin Capital Letter B | U+0042 | |
b | Latin Small Letter B | U+0062 |
If you’re really interested, you can learn about every Unicode character (all 149,878 and counting!) by visiting https://www.unicode.org/charts/.
As you can see, the code points for digits one through nine are in numeric order. So, comparing two years, such as 2023 and 2024, as strings, might go something like this:
let x = ‘2023’ > ‘2024;
Comparison:
U+0032 > U+0032 // false, they are equal
U+0030 > U+0030 // false, they are equal
U+0032 > U+0032 // false, they are equal
U+0033 > U+0034 // false, 0034 is a greater index than 0033
console.log(x); // false
Outcome: FALSE; 2023 is NOT greater than 2024.
We can apply that same comparison logic to the two string timestamps, and the missing ‘T’ is no problem – a space (U+0020
) is ‘less than’ a Latin Capital Letter T (U+0054)
when comparing the Unicode code points, so it doesn’t throw off the comparison. If, at any point, the value in the timestamp string returned from the API was greater than the same index in the current timestamp string, it would mean the user’s registration had expired.
In Conclusion…
Handling dates and time in JavaScript can be trickier than it initially appears, especially when dealing with time zones. My experience with checking the registration expiration was a humbling reminder of how seemingly simple tasks can trip up the development process–and remember, I’ve been doing this for awhile!
However, working through this problem and documenting it in this article helped me review the underlying principles of date handling, the impact of time zone offset, and the benefits of standardized formatting. Working with date and time can be complex, and this post is proof of what happens when you don’t pay close attention–you waste a lot of time! (There’s a pun in there, too, somewhere…) While I just had a few embarrassing interactions with my team (it’s done! Oh no. It’s done! It’s not….) I was able to reinforce my understanding of JavaScript’s Date objects and come to a solution, so I won’t get tripped up next time.
Remember, embracing these challenges helps us grow as programmers, and allows us to become better equipped to tackle future challenges. Find the time (there’s the pun!) to practice working with Date objects, and you won’t have to explain yourself to your team as many times as I did. Happy coding!