Handling Dates in Javascript the Wrong Way.
Don’t be a hero, do not write custom code to handle dates.
It all started with a simple feature, we wanted to send a weekly report email to our customers. The object of the email is [Week 48 — Report] where 48 is the week number as displayed in a calendar. A developer wrote the following code.
const getNumberOfWeek = (currentDate: Date) => {
const msInOneDay = 86400000;
const firstDayOfYear = new Date(currentDate.getFullYear(), 0, 1);
const pastDaysOfYear =
(currentDate.valueOf() - firstDayOfYear.valueOf()) / msInOneDay;
return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
};
const getEmailObject => {
return "[Week " + getNumberOfWeek(new Date()) + "-Report]"
}
I asked why the developer didn’t use a library like date-fns
or luxon
. He said he didn’t want “to add unnecessary dependency to the code”. But here is the problem, the code is wrong.
The Hidden Problem(s)
Let’s try to understand what this code does. It defines the 1st of January as the first day of the year. Compute pastDaysOfYear
the number of days elapsed between the current date and the firstDayOfYear
. Then divide by 7 to have the week number.
On paper, it looks easy, but as we saw in Part 1. and Part 2 handling time is hard, and there are many problems with this code:
❌ Timezone issueconst firstDayOfYear = new Date(date.getFullYear(), 0, 1)
doesn’t return the first of January. If you are running in a timezone like GMT+2, this code returns the 31 of December 2023 at 23:00. The code is only correct if the system runs on UTC.
❌ Date vs DateTime issue
The code assumes that it works with a Date object 2024-01-01
without hours. But Javascript only supports DateTime. If the currentDate
has a time different than midnight, it introduces extra hours that are counted in the timestamp and Math.ceil()
will return the wrong value.
❌ Ignore Leap Year
By fixing the size of the day in Milliseconds, the code ignores any leap seconds.
❌ Ignore Locale
By using getDay()
we assume that the week starts on Monday. This is not true in the U.S.
// using https://moment.github.io/luxon/
DateTime.local(2021, 12, 29, {locale: 'fr-FR'}).weekNumber // returns 52
DateTime.local(2021, 12, 29, {locale: 'fr-FR'}).localWeekNumber // returns 52
DateTime.local(2021, 12, 29, {locale: 'en-US'}).weekNumber // returns 52
DateTime.local(2021, 12, 29, {locale: 'en-US'}).localWeekNumber // returns 1
❌ Doesn’t respect the ISO 8601 norm
The norm defines the first week of a year as follows:
- If 1 January falls on a Monday, Tuesday, Wednesday or Thursday, then the week of 1 January is Week 1. It means that most of the time Week 1 includes the last day(s) of the previous year. (unless 1 January is a Monday).
- If 1 January falls on a Friday, Saturday, or Sunday, then 1 January is considered to be part of the last week of the previous year. Week 1 will begin on the first Monday after 1 January.
I wanted to understand how often this custom code returns a wrong result. So I wrote a script to compare the results of his custom code with a library like date-fns
import { getWeek } from 'date-fns';
import { getNumberOfWeek } from './getWeekOfYear';
const testGetNumberOfWeek = () => {
const errors = [];
const currentYear = new Date().getFullYear();
for (let year = currentYear - 3; year <= currentYear; year++) {
for (let month = 0; month < 12; month++) {
for (let day = 1; day <= 28; day++) {
const dateToTest = new Date(year, month, day, 8, 0, 0);
const weekFromCustom = getNumberOfWeek(dateToTest);
const weekFromDateFns = getWeek(dateToTest);
if (weekFromCustom !== weekFromDateFns) {
errors.push({
date: dateToTest,
weekFromCustom,
weekFromDateFns,
});
}
}
}
}
console.error('Errors:', errors);
};
This script returns 195 inconsistencies in a time window of 4 years. (13% of the wrong answers)
- All Saturdays are in the wrong week (because of the DateTime issue). You could fix that by setting all dates at midnight.
- The code fails to predict the first week of the year. For instance,
Tue Dec 28 2021
returns week53
by the custom code but it should be week1
according to ISO. (A leap year can have 53 weeks, but 2021 is not a leap year.)
The hard truth about Javascript Dates.
Let’s do a test.
What does the following code return?
new Date("5/1/2024"); ???
1rst of May or 5th of January? The answer is neither, it will return 2024-04-30T22:00:00.000Z
on the 30th of April 2024.
What does this code return?
new Date(2024, 2, 22); ???
It’s not February but the 22 of March, because the month starts on 0 in javascript.
Last attempts?
new Date(2024, 1, 31); ???
There are never 31 days in February, yet javascript will not return an error, there is no notion of invalid dates. Javascript will return the closest valid day. 2024-03-01T23:00:00.000Z
Javascript has many issues:
- It doesn’t support Dates but only Datetimes (all date objects are unix timestamps).
- It doesn’t support time zones other than the user’s local time and UTC.
- The parser’s behaviour is inconsistent from one platform to another.
- The Date object is mutable.
- The behaviour of daylight saving time is unpredictable.
- No support for non-Gregorian calendars.
- No date arithmetic like add or subtract time.
Brendan Eich recounts that in 1995, he only had 10 days to write the JavaScript language and integrate it into Netscape. Managing dates was complex and given the deadlines, it was decided to just copy the implementation of Java and its java.Util.Date
object. The problem is that the Java library was poorly implemented, to the extent that nearly all its methods were declared obsolete in 1997 and replaced in Java version 1.1.
Unlike Java, 20 years later, JavaScript still has the same faulty implementation. The reasons why these problems cannot be resolved are linked to two concepts with which TC39 (the body that maintains ECMA) must deal extensively — web compatibility
and web reality
.
TC39 imposes changes that are backwards compatible in order not to break the existing code on the internet.
Conclusion
I hope I convinced you. Never write custom code to handle dates. You may think it’s easy but it’s not. Those 6 lines of code have 5 major bugs. Leverage libraries like Luxon or date-fns. (I don’t recommend moment.js
because objects are mutable).
If a developer doesn’t want to add dependency to the code, challenge why? Is it for the size of the bundle? This is mostly likely irrelevant, for a web application. But you can always check the size on bundlephobia
If it’s for security, make sure the package is updated regularly and actively supported. If that’s for a license reason, exclude licenses like GPLv3.