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

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.

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.

  • 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;
  • 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;

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:

  • 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.

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.

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;
}
}
  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();
}
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
);
}
static get now() {
return new DateTime(Date.now());
}
static parse(...args) {
return Date.parse(...args);
}
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))
};
}
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]
);
}
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', ...];

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.

get locale() {
return this.#locale;
}
set locale(newLocale) {
this.#locale = newLocale;
}
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

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.

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.

localeFormat(options = {}) {
return this.#Date.toLocaleString(this.locale, options);
}
date.localeFormat({
year: 'numeric',
month: 'numeric',
day: 'numeric'
});
// "2/23/2021"
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
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
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

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.

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.

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.

Blog & YouTube Channel for Web, UI & Software Development - beforesemicolon.comyoutube.com/c/BeforeSemicolon — Writer: Elson Correia

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