dates, performance

How to format dates without Moment.js

By Dawntraoz Last updated on Sunday, 31 January 2021

How to format dates without Moment.js Image

In this post I want to tell you how I got rid of the moment library when I found new alternatives with smaller, even non-existent, size.

The moment of Moment.js has passed 😥

Five years ago, even before I knew the frameworks I work with today, Moment.js was the library that saved us from headaches trying to handle Date and its methods in its own way.

We are not going to deny, that when you start using a library and it solves your problem, you end up loving it and adding it to all the projects where you need to do some maths between dates or change your time zone. So you can imagine how many old projects use it.

If you would like to know more about why it is necessary to upgrade to a new library, I invite you to read Why you shouldn't use Moment.js...

But now, everything has changed and we have a lot of alternatives with which we can improve our performance. In my case, since in my website I only have a difference between dates and a few formatting, using only Date and Intl.DateTimeFormat I get what I need easily.

Intl.DateTimeFormat is an object that enable language-sensitive date and time formatting.

But for those projects that need to deal with more complex functionalities, here are a few libraries for different use cases that may be useful for you.

What alternatives do we have

Depending on how we use dates, there will be libraries that do better than others. Let's look at a few use cases and the top-rated libraries available.

Older browsers

Let's start with support for older versions of IE explorer, although that will no longer be necessary from August 2021.

Microsoft officially announced that it will stop support for IE11 and we will have to do the same, for our own health 😂

As Date.parse only handles ISO 8601 properly since ES5, to support IE9 we need libraries that use their own regex solution:

  • Luxon - A powerful, modern, and friendly wrapper for Javascript dates and times (from the creators of moment).

    It also has a guide to help moment users to understand the new way of working: https://moment.github.io/luxon/docs/manual/moment.html

  • JS-Joda - An immutable date and time library for JavaScript. It provides a simple, domain-driven and clean API based on the ISO calendar system.

    It comes with built-in parsers for ISO 8601 and supports ECMAScript 5 browsers down to IE9.

Internet Explorer is gone

Parsing

When it comes to EPOCH Time Parsing, most libraries do it properly, but that doesn't mean that there're some libraries that manage to do it faster:

An epoch is a date and time from which a computer measures system time. The Unix epoch is the number of seconds that have elapsed since January 1, 1970.

  • Date fns - Provides the most comprehensive, simple and consistent toolset for manipulating JavaScript dates in a browser & Node.js. Date-fns is fast so users will have the best user experience.

Format

If we are concerned about formatting dates, Intl.DateTimeFormat or the methods provided by Date should be enough, but in case we need a library for other functionalities and we want to remain consistent throughout the project:

  • Day.js - Fast 2kB alternative, is a minimalist JavaScript library that parses, validates, manipulates, and displays dates and times for modern browsers with a largely Moment.js-compatible API.

    If you use Moment.js, you already know how to use Day.js.

  • Format.js is a modular collection of JavaScript libraries for internationalization that are focused on formatting numbers, dates, and strings for displaying to people.

    A set of core libraries built on the JavaScript Intl built-ins and industry-wide i18n standards, plus a set of integrations for common template and component libraries.

Date time maths

But when our project requires to do maths with dates, we need a more powerful library to facilitate the operations and make it easier, without losing performance:

  • JS-Joda is still the one that gives the best performance results. But taking into account that it is not modular, we must be sure about whether it is necessary. Although it must be said that its roadmap includes a plan for reducing its size.

    Support for the domain models LocalDate, LocalDateTime, ZonedDateTime, Instant, Duration and Period. Also, supports IANA timezone, adding the plugin @js-joda/timezone you'll have access to the IANA timezone database with all the timezones available.

Package size

If, on the other hand, size is the most important factor, and Date and Intl are not enough, a modular library with tree-shaking or a light-weight library may be the best choice:

  • Day.js - 2kB, less JavaScript to download, parse and execute, leaving more time for your code.
  • Date fns - In this library, you can pick what you need and stop bloating your project with useless functionality. It works well with modern module bundlers such as webpack, Browserify, or Rollup and supports tree-shaking.

Immutable

One thing that is important to know is that we left behind a library that mutated the objects we created, which could only lead to errors. From now on we will work with immutable objects!!

An immutable object is an object whose state cannot be modified after it is created. This is in contrast to a mutable object, which can be modified after it is created.

How I got rid of moment in my old projects

As I told you before, in my website I only had a couple of uses, so it was very easy to change. But in another project, I had a few problems with daylight savings and timezones.

In order not to forget, I'll explain the problems and the solutions I found without using any library:

Formatting

  • Format I wanted: Jan 2021 (Short month and year)

    // How it comes from my headlessCMS Storyblok
    const finishDate = '2021-01-31'
    
    // -- Before
    moment(finishDate).format('MMM YYYY')
    
    // -- After
    new Date(finishDate).toLocaleDateString('en-GB', {
      month: 'short',
      year: 'numeric',
    })
    // or (equivalent with Intl)
    new Intl.DateTimeFormat('en-GB', {
      month: 'short',
      year: 'numeric',
    }).format(new Date(finishDate))
    
  • Format I wanted: Sunday, 31 January 2021 (Name of the day, day number, long month and year)

    // How it comes from my headlessCMS Storyblok
    const finishDate = '2021-01-31'
    
    // -- Before
    moment(finishDate).format('dddd, D MMMM YYYY');
    
    // -- After
    new Date(finishDate).toLocaleDateString('en-GB', {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric',
    })
    // or (equivalent with Intl)
    new Intl.DateTimeFormat('en-GB', {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric',
    }).format(new Date(finishDate))
    

When it comes to learn the options of Intl.DateTimeFormat, this cheatsheet has been a great help 😍

Difference (Period of time)

On my website, I've represented the period of time I have been in each company. For that, I've calculated the difference between the entrance and leaving date:

// -- Before
const start = moment(this.start_date, 'YYYYMMDD')
const finish = moment(this.finish_date, 'YYYYMMDD')
const diff = moment.duration(finish.diff(start))

let formatDiff = ''
const year = diff.years()
const month = diff.months()

if (year > 0 && month > 0) {
  formatDiff += `${year} years & ${month} months` 
} else if (year > 0) {
  formatDiff += `${year} years`
} else if (month > 0) {
  formatDiff += `${month} months`
}
console.log(formatDiff)

// -- After
const start = new Date(this.start_date)
const finish = new Date(this.finish_date)
const diff = new Date(finish.getTime() - start.getTime())

let formatDiff = ''
const year = diff.getFullYear() - new Date(0).getFullYear() // 1970
const month = diff.getMonth()

if (year > 0 && month > 0) {
  formatDiff += `${year} years & ${month} months` 
} else if (year > 0) {
  formatDiff += `${year} years`
} else if (month > 0) {
  formatDiff += `${month} months`
}
console.log(formatDiff)

Once I got what I wanted, I set out to measure the times, to see if the change was also worthwhile in terms of performance. To my surprise, the change was much more than noticeable, going from ~0.10ms in moment to ~0.045ms using Date 😱

I measured it using performance.now() as follow:

const startTime = performance.now()

// -- Code calculating difference between dates

const endTime = performance.now()
console.log(endTime - startTime) // Result in milliseconds

If you've calculated time periods in a different way, I would love to see your solution 😍, you can send me a DM at @dawntraoz.

Working with Timezones

In backend, the common way to store a date in a database is in UTC format, so far so good.

But, when I created an instance of Date with the string coming from the DB, the result was set up to the browser's time zone.

The problem was related to the string coming from the DB, it didn't have any of the formats detected as UTC by Date. Adding 'Z' at the end of the string solved the issue.

const dbDate = '2021-01-25 00:10:16.2451106';

// Problem: UTC 0, it's not the hour we set up
const incorrectUTC = new Date(dbDate);
console.log(incorrectUTC.toUTCString()); // "Sun, 24 Jan 2021 23:10:16 GMT"

// Solution: Z added
const correctUTC = new Date(`${dbDate}Z`);
console.log(correctUTC.toUTCString()); // "Mon, 25 Jan 2021 00:10:16 GMT"

// -- Date from my TimeZone (CET)
console.log(correctUTC.toString()); // "Mon Jan 25 2021 01:10:16 GMT+0100 (Central European Standard Time)"

// -- Date from a chosen TimeZone (America/New_York)
const americaDate = correctUTC.toLocaleString('en-GB', {
  weekday: 'short',
  month: 'short',
  year: 'numeric',
  day: 'numeric',
  hour: 'numeric',
  minute: 'numeric',
  second: 'numeric',
  timeZone: 'America/New_York',
  timeZoneName: 'short'
})
console.log(americaDate); // "Sun, 24 Jan 2021, 19:10:16 GMT-5"

While researching what was going on, I found in the documentation of Date.parse in MDN the root of the problem:

The first string, according to ES5, will imply UTC time, and the others are specifying UTC timezone via the ISO date specification ('Z' and '+00:00')

Date.parse("2019-01-01")
Date.parse("2019-01-01T00:00:00.000Z")
Date.parse("2019-01-01T00:00:00.000+00:00")

The following call, which does not specify a time zone will be set to 2019-01-01 at 00:00:00 in the local timezone of the system.

Date.parse("2019-01-01T00:00:00")

Timezones + Daylight saving rules

Another issue I came up while working with native Date.

Using an offset to calculate Timezone is a wrong approach, and you will always encounter problems while using it. Time zones and daylight saving rules may change on several occasions during a year, and it's difficult to keep up with changes.

To get the system's IANA timezone in JavaScript, you should use Intl TimeZone instead of Date offset:

// Not what we need - Doesn't take into account daylight saving rules
const offset = (new Date()).getTimezoneOffset() * 60000;

// Solution
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone)

But even though I have decided to use Date and Intl in my projects, I still have the itch to try out the library Spacetime.

Spacetime is a solution without Intl that calculate time in remote timezones, support daylight savings, leap years, and hemispheres. I have to take a look at it and put it into practice 👀 Share with me your thoughts if you already tried it!!

Bye Bye moment 👋

And, well, we've come to the end, we've to leave moment behind. But, thanks to this library, many others have made their way to make our community a better place.

It only remains for me to say, thanks moment for everything you solved for us in its day and goodbye!!