How to Work with Date in Plain Javascript — No Libraries Needed

Javascript Date object is not very well-loved in the community. It is something Javascript adopted from Java but both grew in different directions. Its API can be confusing and lacks a lot of methods and information that you often need when working with date and time in your projects.

Javascript and Java Date

The Javascript Date object came from Java. Java introduced Calendar in recent versions and deprecated a lot of Date methods but you still have, what I consider to be, only the good parts about the Java Date object. Javascript Date object feels forgotten sometimes but it includes many things super useful. It simply allows you to access Date and time information and that's it.

Java Date object has methods like compareTo, equals, before, and after which are useful when you are dealing with date and time. Its new Calendar object allows you to access more details you often want when dealing with dates in general.

Date object Pros and Cons

I have nothing against the Date object perhaps because I am used to creating utility functions for what I need and almost always used an external library when dealing with Date and time. I recently shared a video on how to create a date picker using vanilla Javascript and goes to show, working with Date can be complex and requires a lot of work and manipulation.

Here are some cons to that:

  • It lacks Date comparison methods;
  • It lacks an easy way to calculate the week in the year;
  • It has an elaborated/complex way to localize the date;
  • It lacks a useful or friendly way to format Date. We got the Intl format method though;
  • It is zero-based which is not how we think about dates;
  • It forces you to make weird calculations to get simple information like how many days in a month, is a leap year, week number, etc;
  • lacks validation. Passing the wrong info to create a Date will not throw an error sometimes resulting in a date with “Invalid Date” as a value;
  • Lacks a way to get timezone details. Use Intl Api for this instead;
  • It is mutable;

Here are some pros:

  • It has UTC support;
  • Supports ISO time;
  • Easy to extend and create utilities around it;
  • Intl API is used to format Date using the toLocaleString method;
  • It is evolving;

Some weird details worth mentioning are that the Date object is in UTC and when you call methods to access date information, it uses local time. To get UTC date information you must use UTC methods. So, instead of calling getDay you call the getUTCDay method instead.

Date libraries proposed solutions

We have many libraries attempting to solve this Date issue which is awesome. Here are examples of a few worth mentioning:

  • Moment.js (Deprecated) — super popular library and one of the best until its deprecation. The main issues are its size, mutable nature and not modern enough. You should not use this library at all!
  • Luxon — inspired by MomentJs, luxon is pretty much a wrapper around native Intl API and it is an improvement over MomentJs. It is immutable, chainable, and easy to use. Its main limitation is that it will not work properly in an environment where Intl API is not supported.
  • Datefns — this library takes a different approach by providing you with a library of functions you can use to work with the native Date object. It is lightweight and modular(tree shakable) and at this moment, its limitation is the lack of support for UTC and timezone which if you need, you can use Luxon, DayJs, or Intl API.
  • Days — It is very similar to MomentJs but in a light package. It is immutable and you can add extra capabilities through plugins when you need them. If you are familiar with MomentJs it will feel very familiar.

The way all these libraries chose to deal with Date is to either create a wrapper around the Date and Intl object or to introduce a library of functions that allows you to perform calculations and extract extra information.

How to “Fix” Date?

The ideal way would be if the Date object gained some extra methods like Array has, but Javascript gave us Internationalization API which is awesome but not enough. The Date object can benefit a lot from some general utility methods but some people would agree that it is not enough due to the fact that its limitations and problems are not only about the lack of some methods.

Our best solution is the upcoming Temporal API which is still in early stages as a Proposal . It is a much better native way to handle date in general and we are all looking forward to it. Until then, we are stuck with native Date object and libraries built around it.

I really love the Datefns approach which is very functional and avoids extending or wrapping the Date object. It is like a lodash for Dates. It is a simple library of functions you call with your Date and optional options and you get something back.

An intrusive approach would be to extend the Date object to override some of the methods and introduce new ones. A slightly better alternative and Object-Oriented approach would be creating an immutable class around the Date object with your own functionality. Something like this:

class DateTime {
#locale = 'default'; // language
#Date = null;

constructor(...args) {
const date = new Date(...args);

// Simple validation
if(date.toString() === 'Invalid Date') {
throw new Error(`Invalid Date: "${args}"`);
}

this.#Date = date;
}
}

The DateTime class is a simple wrapper class that handles the Date internally for you. I'll continue with this class to show you more techniques in handling Dates in vanilla Javascript.

It simply takes arguments to create a new date with and does a simple validation to ensure the Date is not silently created as an invalid Date.

But, It still does not check for other things, for example, if I create a date for 2021 1(February) 29 — which does not exist because 2021 is not a leap year — it will change that to March 1st, 2021. To catch things like that it is necessary a more robust validation, error, and warning system around it.

Now we need getters to work as an alias for some common Date methods. With these getters, we can fix the month and day of the week to start from 1 instead of zero.

  get year() {
return this.#Date.getFullYear();
}

get month() {
return this.#Date.getMonth() + 1;
}

get dayOfTheWeek() {
return this.#Date.getDay() + 1;
}

get date() { // alias to dayOfTheMonth
return this.dayOfTheMonth;
}

get dayOfTheMonth() {
return this.#Date.getDate();
}

get hour() {
return this.#Date.getHours();
}

get minutes() {
return this.#Date.getMinutes();
}

get seconds() {
return this.#Date.getSeconds();
}

get milliseconds() {
return this.#Date.getMilliseconds();
}
get timestamp() {
return this.#Date.getTime();
}

Another piece of information we need is the week of the year we are currently at. This method simply gets the first day of the year, subtract the current time from it to then divide it by days in milliseconds in order to get the number of days since the beginning of the year. Then, we divide all that by seven.

get week() {
const firstDayOfTheYear = new Date(this.year, 0, 1);

// one day = 86400000ms
const pastDaysOfYear =
(this.timestamp - firstDayOfTheYear) / 86400000;
return Math.ceil(
(pastDaysOfYear + firstDayOfTheYear.getDay() + 1) / 7
);
}

The Javascript Date has 2 very handy static members which we should also include here. They are the good parts of the Date object and we will need them for later calculations.

static get now() {
return new DateTime(Date.now());
}
static parse(...args) {
return Date.parse(...args);
}

With that, we need a way to update the Date. The challenge here is to keep this Date object immutable. What we can do is, instead of changing the current Date we can return a new DateTime instance using the timestamp returned by the various set methods.

set() {
const date = new Date(this.timestamp);
return {
date: value => new DateTime(date.setDate(value)),
month: value => new DateTime(date.setMonth(value)),
year: value => new DateTime(date.setFullYear(value)),
hour: value => new DateTime(date.setHours(value)),
minutes: value => new DateTime(date.setMinutes(value)),
seconds: value => new DateTime(date.setSeconds(value)),
milliseconds: value => new DateTime(date.setMilliseconds(value))
};
}

Let's add some static methods and getters for general important information. First, a static method to calculate if the given year is a leap year. Then another static method that given the year, it returns a frozen array of 12 items, each representing the days in that particular month.

static isLeapYear(year) {
year = year ?? (new Date()).getFullYear();
if(`${year}`.length !== 4 || !isNaN(year) ) {
return false;
}
return year % 100 === 0
? year % 400 === 0
: year % 4 === 0;
}

static monthSizes(year) {
return Object.freeze(
[31, (DateTime.isLeapYear(year) ? 29 : 28),
31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
);
}

Since we have a way to get the number of days in each month now, it would be nice to get the days of the week as well. It can come in handy when you want to display the days of the week in a calendar for example.

static daysOfTheWeek(locale = 'default', format = 'long') {
const weekDays = Array.from({length: 7});
const date = new Date();
for(let i=1; i<=7; i++) {
weekDays[date.getDay()] =
date.toLocaleString(locale, { weekday: format});
date.setDate(date.getDate() + 1);
}
Object.freeze(weekDays); return weekDays;
}
// Usage Example:
DateTime.daysOfTheWeek
// returns ['Monday', 'Tuesday', 'Wednesday', ...];

Notice that it takes an optional locale/language and format to use and this will make sure you get an array of days of the week in a human-readable format, but I'll talk about language more in the next section.

Language/Locale support

The daysOfTheWeek static method above makes use of the toLocaleString method which takes 2 options, the locale/language to use and an options object which is the same object expected by Intl.DateFormat method. This is where Date objects meet Intl API.

We used the weekday option here and whatever option you specify, is the one that will be used in the returned string. In this case, toLocaleString simply returns a string containing the week information in the format specified which is “long” by default.

To continue with localization I need to go back to the private locale property included in the beginning when creating the DateTime class. Because it is private, we need a way to retrieve and update it as well. We can use a getter and a setter for it.

get locale() {
return this.#locale;
}
set locale(newLocale) {
this.#locale = newLocale;
}

The first thing we can do with the locale property now is to provide human-readable and translated options for months and weekdays. With the toLocaleString method, we can specify only the format information we want and it will return a string in the currently selected language.

get monthShort() {
return this.#Date.toLocaleString(this.locale, {
month: 'short'
});
}
get monthLong() {
return this.#Date.toLocaleString(this.locale, {
month: 'long'
});
}
get dayOfTheWeekLong() {
return this.#Date.toLocaleString(this.locale, {
weekday: 'long'
});
}
get dayOfTheWeekShort() {
return this.#Date.toLocaleString(this.locale, {
weekday: 'short'
});
}
// Usage Example:
const date = new DateTime();
date.monthShort; // Feb
date.monthLong; // February
date.dayOfTheWeekLong; // Tuesday
date.dayOfTheWeekShort; // Tue

To see the full list of options you can specify with the toLocaleString method, you should read about the Intl.DateFormat method. It even allows you to specify the calendar (e.g greek, Chinese, catholic) you want as well as timezone, numbering system, and time format. It is super powerful and the options are extensive but I'll share more as we go.

UTC Support

The Date object has UTC methods equivalent to those used to get and set hours, months, etc. They are expressed as getUTCHours and setUTCMonth. However, we will not build a whole set of methods and properties to address those separately. There is a shortcut and ill show you.

When you create Date with the Date object, it actually creates Date in UTC but when you are working with it, it returns local date and time. The date has a UTC method that takes almost the same arguments as the Date constructor which we can use to force the Date to stay in UTC mode all the time.

The code below exposes a utc getter that returns a new DateTime instance but uses the UTC method and the current Date details to instantiate it. This will give us access to all properties and methods of DateTime but in UTC mode.

get utc() {
return new DateTime(
Date.UTC(
this.year, this.month, this.date, this.hours,
this.minutes, this.seconds, this.milliseconds
)
);
}
// Usage Example:
const date = new DateTime();
date.monthLong; // February
date.utc.monthLong; // March
// date sampled at the end of feb

Format Date

Being able to format date is probably the most desired feature when working with Date and we can rely on Intl API and the foundation we have to set some cool and simple formatting methods that handle both, custom and relative time formatting.

The simple and best formatting option is to rely on the Intl API to format according to the language of the user using the DateTime. It requires the user to know about the options but it is the recommended way if the localization of time is a concern. Its limitation is that it handles the format and delimiter which is often something developers want to have control over.

localeFormat(options = {}) {
return this.#Date.toLocaleString(this.locale, options);
}
date.localeFormat({
year: 'numeric',
month: 'numeric',
day: 'numeric'
});
// "2/23/2021"

The more traditional and expected formatted method is to provide the string representing the desired format and for it to return the date in that specific format. This example does not contain a full list of options since the DateTime class still excludes a lot of information like era, timezone, etc.

Warning: formatting is an extensive task that needs to take into consideration many Date details, language, and user experience. This is a simplified technique that is good for limited formatting options.

get amPm() {
return this.#Date
.toLocaleString('en', {hour: '2-digit'}).split(' ')[1];
}
format(formatStr) {
return formatStr
.replace(/\bYYYY\b/, this.year)
.replace(/\bYYY\b/, this.yearShort)
.replace(/\bWW\b/, this.week.toString().padStart(2, '0'))
.replace(/\bW\b/, this.week)
.replace(/\bDDDD\b/, this.dayOfTheWeekLong)
.replace(/\bDDD\b/, this.dayOfTheWeekShort)
.replace(/\bDD\b/, this.date.toString().padStart(2, '0'))
.replace(/\bD\b/, this.date)
.replace(/\bMMMM\b/, this.monthLong)
.replace(/\bMMM\b/, this.monthShort)
.replace(/\bMM\b/, this.month.toString().padStart(2, '0'))
.replace(/\bM\b/, this.month)
.replace(/\bHH\b/, this.hours.toString().padStart(2, '0'))
.replace(/\bH\b/, this.hours)
.replace(/\bhh\b/, this.amPm === 'AM'
? `${this.hours}`.padStart(2, '0')
: `${this.hours % 12}`.padStart(2, '0'))
.replace(/\bh\b/, this.amPm === 'AM'
? this.hours
: this.hours % 12)
.replace(/\bmm\b/, this.minutes.toString().padStart(2, '0'))
.replace(/\bm\b/, this.minutes)
.replace(/\bss\b/, this.seconds.toString().padStart(2, '0'))
.replace(/\bs\b/, this.seconds)
.replace(/\bS\b/, this.milliseconds)
.replace(/\ba\b/, this.#Date
.toLocaleString(this.locale, {hour: '2-digit'})
.replace(
this.#Date.toLocaleString(this.locale,
{hour: '2-digit', hour12: false}),
''
).trim())
}
// Usage Example:date.format('MMMM, DD(DDD) YYYY, HH:mm[S] a');
// February, 23(Tue) 2021, 04:44[546] PM

Another type of format is the relative time format which the Intl API already provides a way to accomplish. One thing we need first is a way to know the difference between the time now and the time in a DateTime instance.

We use milliseconds to calculate the difference in time between the provided Date and our Date. If the Date is in the past the number will be positive, otherwise negative.

static get weekInMilliseconds() {
return 604800000;
}
static get dayInMilliseconds() {
return 86400000;
}
static get hourInMilliseconds() {
return 3600000;
}
static get minuteInMilliseconds() {
return 60000;
}
static get quarterInMilliseconds() {
return 7884000000;
}
static get monthInMilliseconds() {
return 2629800000;
}
differenceFrom(date = null) {
const {timestamp, month, year, hours} =
date instanceof DateTime ? date : new DateTime(date);
return {
days: Math.round((this.timestamp - timestamp) / DateTime.dayInMilliseconds),
years: Math.round((this.timestamp - timestamp) / (DateTime.dayInMilliseconds * 365)),
months: Math.round((this.timestamp - timestamp) / DateTime.monthInMilliseconds),
weeks: Math.round((this.timestamp - timestamp) / DateTime.weekInMilliseconds),
quarters: Math.round((this.timestamp - timestamp) / DateTime.quarterInMilliseconds),
hours: Math.round((this.timestamp - timestamp) / DateTime.hourInMilliseconds),
minutes: Math.round((this.timestamp - timestamp) / DateTime.minuteInMilliseconds),
seconds: Math.round((this.timestamp - timestamp) / 1000)
}
}
// Usage Example:
const diff = date.differenceFrom(DateTime.now);
diff.days; // 25
diff.months; // 1
diff.years; // 0

With the differenceFrom method, we can now implement our relative format method. It uses the RelativeTimeFormat method from Intl API which you should check for more details.

In order to get the relative time information, we used the options localeMatcher of “best fit” which will give us the best-suited information for the time we need. The numeric of “auto” will let it decide when to use numbers versus day description, for example, “yesterday” or “1 day ago”. Finally, we use the “long” style so it spells the words fully.

get relativeFormat() {
const rtf = new Intl.RelativeTimeFormat(this.locale, {
localeMatcher: "best fit",
numeric: "auto",
style: "long"
});
const diff = DateTime.now.differenceFrom(this);
return {
days: rtf.format(diff.days, "day"),
months: rtf.format(diff.months, "month"),
weeks: rtf.format(diff.weeks, "week"),
quarters: rtf.format(diff.quarters, "quarter"),
years: rtf.format(diff.years, "year"),
hours: rtf.format(diff.hours, "hours"),
minutes: rtf.format(diff.minutes, "minute"),
seconds: rtf.format(diff.seconds, "second")
}
}
// Usage Example:
const date = new DateTime(2021, 1, 24);
const relativeTime = date.relativeFormat;relativeTime.days // yesterday
relativeTime.minutes // 1,063 minutes ago
relativeTime.hours // 18 hours ago

Comparison methods

At some point, we will need to know the difference between dates and times. Is it before or after? Is it in a particular range or quarter, etc? Comparisons can get quite vast in options so let's take care of the fundamentals which we can add more options on top later.

is() {
const thisDate = this;

const getTimestamp = date =>
date instanceof DateTime ? date.timestamp : date.getTime();
return {
before: (date) => thisDate.timestamp < getTimestamp(date),
after: (date) => thisDate.timestamp > getTimestamp(date),
equalTo: (date) => thisDate.timestamp === getTimestamp(date),
inRange: (date1, date2) =>
thisDate.timestamp > getTimestamp(date1) &&
thisDate.timestamp <= getTimestamp(date2),
weekend: () => [7, 1].includes(this.dayOfTheWeek),
weekdays: () => [2, 3, 4, 5, 6].includes(this.dayOfTheWeek),
am: () => this.amPm === 'AM',
pm: () => this.amPm === 'PM',
future: () => thisDate.timestamp > Date.now(),
past: () => thisDate.timestamp < Date.now(),
leapYear: () => DateTime.isLeapYear(thisDate.year),
monday: () => this.dayOfTheWeek === 2,
tuesday: () => this.dayOfTheWeek === 3,
wednesday: () => this.dayOfTheWeek === 4,
thursday: () => this.dayOfTheWeek === 5,
friday: () => this.dayOfTheWeek === 6,
saturday: () => this.dayOfTheWeek === 7,
sunday: () => this.dayOfTheWeek === 1,
firstQuarter: () => [1, 2, 3].includes(this.month),
secondQuarter: () => [4, 5, 6].includes(this.month),
thirdQuarter: () => [7, 8, 9].includes(this.month),
forthQuarter: () => [10, 11, 12].includes(this.month),
}
}
// Usage Example:
const date = new DateTime(2021, 1, 25);
date.is().before(Date.now()) // true
date.is().monday() // false

As you can see, our foundation allowed for a very simple comparison and Date checkers as well as some simple comparisons. One thing you need to get a hold of is to think of a time in milliseconds instead of the human way of counting days and hours.

Simple math methods

We already have the differenceFrom method which is a nice mathematical method. It allows us to have an idea of how our time differs from another in a specific part (hours, minutes, seconds, etc). What we need now is a way to increment or decrement time.

What the add and subtract methods do is perform the operation at the level(hour, minutes, days, etc) you want but return a new DateTime instance instead of modifying the current Date which is the ideal behavior you want.

The Date set methods return the newly updated time in milliseconds which we can use to instantiate and return a new DateTime instance.

add(value) {
const date = new Date(this.timestamp);
return {
get days() {
return new DateTime(date.setDate(date.getDate() + value));
},
get months() {
return new DateTime(date.setMonth(date.getMonth() + value));
},
get years() {
return new DateTime(date.setFullYear(date.getFullYear() + value));
},
get hours() {
return new DateTime(date.setHours(date.getHours() + value));
},
get minutes() {
return new DateTime(date.setMinutes(date.getMinutes() + value));
},
get seconds() {
return new DateTime(date.setSeconds(date.getSeconds() + value));
},
get milliseconds() {
return new DateTime(date.setMilliseconds(date.getMilliseconds() + value));
},
}
}
// Usage Example:
date.add(12).days
date.add(1).years.add(30).days
subtract(value) {
const date = new Date(this.timestamp);
return {
get days() {
return new DateTime(date.setDate(date.getDate() - value));
},
get months() {
return new DateTime(date.setMonth(date.getMonth() - value));
},
get years() {
return new DateTime(date.setFullYear(date.getFullYear() - value));
},
get hours() {
return new DateTime(date.setHours(date.getHours() - value));
},
get minutes() {
return new DateTime(date.setMinutes(date.getMinutes() - value));
},
get seconds() {
return new DateTime(date.setSeconds(date.getSeconds() - value));
},
get milliseconds() {
return new DateTime(date.setMilliseconds(date.getMilliseconds() - value));
},
}
}
// Usage Example:date.subtract(1).years
date.subtract(6).months.subtract(8).hours

Final Details

There are so many details we can add to this to make it an even better Date object. We could add a “calendar” option similar to a locale that allows us to change the calendar from Chinese, catholic, etc. The same goes for timezone. The format method still lacks a lot of formatting options and we still haven’t included information like the era, quarters, season, etc.

What I am trying to say is that there is a lot that goes into having a custom Date handler which is the reason why you should use a tested and well-supported modern library. The value of this article is in showing you that Date requires a more careful analysis and thought and it is definitely worth your learning time.

Here a final powerful method:

clone() {
return new DateTime(this.timestamp);
}

Conclusion

Dealing with a Date can be intense and complex as you could see. There are so many options to consider and you should deal with Date in a way it takes into consideration how your website users track and deal with time.

This article barely scratches the surface and you can see the DateTime class code on GitHub for a much closer look. Hopefully, it was enough to make you feel more comfortable and aware of how small these simple manipulations are. Small stuff adds up but this was fun to do.

Libraries allow you to produce code faster and are tested by the community behind it, but if you are in a learning process — which we all are — explore the native Date object as much as possible so you understand it more. Add Intl API to the list as well and good luck!

I put most of these ideas in practice in my OOP Web Component date picker video which you can check if you want to see it in action.

Youtube Channel: Before Semicolon
Website: beforesemicolon.com

Blog & YouTube Channel for Web, UI & Software Development - beforesemicolon.comyoutube.com/c/BeforeSemicolon

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store