You Probably Don't Need Moment.js Anymore

Tags

Digital wall clock showing 11:11

It’s 2020. And, the truth is, you probably don’t need Moment.js.

I don’t often hear it said in simple terms—why one can, and should, look to alternatives for Moment.js. Let’s talk about why we can simply “use the platform” and also provide you with an arsenal of generic examples that will hopefully apply to your applications.

Support

First, let’s discuss some preliminary points to help us prepare for the rest of the article.

  • JavaScript payload size was the biggest driver for looking to alternative solutions. By cutting 30kb off our JS bundle, we can deliver and parse the payload faster so users can start interacting with our application sooner. If you also can remove moment-timezone, your savings will be even greater.
  • You do not need Moment.js if you support newer browsers and if you show dates or datetimes only in your user’s timezone.
  • Things are not as easy as they seem, especially if you plan on manually parsing the output from the native APIs. For example, ko (Korean) Meridien time (AM/PM) shows up at the start of the time result, whereas in English, the Meridien time shows up at the end.
  • When testing out your solutions, it is always good to test languages like Korean and Arabic to make sure you logic will work.
  • If your server provides ISO 8601 dates, you are in luck. However, if you are working with dates like 12/1/2019, displaying localized dates and avoiding pitfalls of Javascript’s Date constructor becomes much harder.

Primitives

Here are some primitives we will be looking into:

  • Intl.DateTimeFormat
  • Date

    • toLocaleDateString
    • toLocaleTimeString
    • getHours
    • getMinutes
    • etc…
  • Intl.NumberFormat
  • Intl.RelativeTimeFormat

Examples

Ordinal Time with toLocaleDateString

This Date method might be your most useful API to reach for and the simplest to display a localized Date string.

Suppose you want to show an ordinal time like February 19, 2016 at 1AM for users in the East Coast, but February 18th, 2019 at 10PM for users in the West Coast. It could even be this blog post’s published at time!

Let’s just see what it looks like at it’s most basic level.

new Date('2019-02-19T06:00:00Z').toLocaleDateString(
  'en-gb'
); // 18/02/2019

What if we want to format the date to a human readable format?

new Date('2019-02-19T06:00:00Z').toLocaleDateString(
  'en-gb',
  {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }
); // 18 February 2019

Note, I am in Pacific Standard Time (GMT-8) and notice the date is shown as the day before. The system your software runs on will display the resulting date in the user’s localized time.

However, what if we receive a naive date (no timezone information) assuming our server context stores the data as UTC? This would print out the locale specific date. We can assign timeZone to utc in order to normalize the date to Greenwich Time.

new Date('2019-02-19T23:00:00.000000').toLocaleDateString(
  'en-gb',
  {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    timeZone: 'utc'
  }
); // 20 February, 2019

With this normalized date, we can adjust based on the user’s timezone as necessary.

let currentTimeZoneOffset = deserializedDate.getTimezoneOffset() * 60_000;
new Date(deserializedDate - currentTimeZoneOffset);

Intl.DateTimeFormat

This is one of your most powerful tools when migrating to native equivalents.

Here are some examples that display localized times and dates.

Intl.DateTimeFormat(navigator.language, { weekday: 'long', month: 'short', day: 'numeric' }).format(new Date()) // Friday, Dec 27
Intl.DateTimeFormat('en', { hour: 'numeric' }).format(new Date()) // 2 PM
Intl.DateTimeFormat('ko', { hour: 'numeric' }).format(new Date()) // 오후 2시
Intl.DateTimeFormat('en', { hour: "numeric", minute: "numeric", hour12: true }).format(new Date()) // 2:00 PM

Often I used Moment to also help me display the timezone name. Well, this is easy if you need to display it in English only.

let tzName = Intl.DateTimeFormat().resolvedOptions().timeZone;
let date = Intl.DateTimeFormat('en', { hour: 'numeric' }).format(new Date())
`${date} ${tzName}` // 2 PM America/Los_Angelas

If you need the abbreviated timezone name, you can pull some regex out of the box alongside toLocaleDateString and timeZoneName.

let [, tzName] = /.*\s(.+)/.exec((new Date()).toLocaleDateString(navigator.language, { timeZoneName: 'short' }));
let date = Intl.DateTimeFormat('en', { hour: 'numeric' }).format(new Date())
`${date} ${tzName}` // 2 PM PST

Things can get more nuanced. What if you need the hour or just the Meridien time? For example, say you have to insert a DOM element between the hour and Meridien time for styling purposes?

// Extract the Hour
Intl.DateTimeFormat('en', { hour: 'numeric' }).formatToParts(date)[0].value // 2

// Meridien Time
Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).formatToParts(date).find(p => p.type === "dayPeriod").value; // PM
Intl.DateTimeFormat('ko', { hour: 'numeric' }).formatToParts(date).find(p => p.type === "dayPeriod").value; // 오후

Duration

For duration we can take advantage of Intl.NumberFormat.

Intl.NumberFormat('es', { minimumIntegerDigits: 1 }).format(100); // 1000
Intl.NumberFormat('zh', { minimumIntegerDigits: 1 }).format(1000); // 1,000

What if you need a delimiter to put between hours and minutes?

const [delimiter] = new Date().toLocaleTimeString('en-us').match(/\b[:.]\b/);
delimiter // ':'

const [delimiter] = new Date().toLocaleTimeString('in').match(/\b[:.]\b/);
delimiter // '.'

With this information, we can combine our knowledge to output a localized duration of a movie.

const nonPaddedIntl = Intl.NumberFormat(locale, { minimumIntegerDigits: 1 });
const paddedIntl = Intl.NumberFormat(locale, { minimumIntegerDigits: 2 })

const [delimiter] = new Date().toLocaleTimeString('en-us').match(/\b[:.]\b/);
const duration = 49_500; // input in seconds
const hours = Math.floor(duration / 3600);
const minutes = Math.floor(duration / 60) % 60;
const seconds = duration % 60;
const indexToPad = hours ? 0 : 1;
const timeFormat =
  [hours, minutes, seconds]
  .map((val, i) => {
    return (val < 10 && i > indexToPad) ? paddedIntl.format(val) : nonPaddedIntl.format(val);
  })
  .filter((val, i) => {
    if (i === 0) {
        return !(val === '00' || val === '0');
    }

    return true;
  })
  .join(delimiter); // 4:32

Time Range

Your application may need to indicate a future time or date range of a specific event. I’ll give a brief overview on how we can accomplish this.

First, we have a wide range of concerns. For example, when should you display the times with AM/PM? If you are sometime between 12:01PM and 11:59PM, then likely you can show your time range as 1 - 2PM. However, if the time crosses the noon or midnight boundaries, you likely want to display the time as 11AM - 1PM. Moreover, you likely have to pad your numbers and consider arabic unicode digits.

Oofda. A lot to consider.

First, I would recommend building a list of internationalized keys. You probably need 20 or so keys.

"TimeRange.HourAligned.EndsAtNoon": "{hStart}AM – {hEnd}PM",
"TimeRange.HourAligned.AfterNoon": "{hStart} – {hEnd}PM",
"TimeRange.Unaligned.AfterNoon": "{hStart}:{mStart} – {hEnd}:{mEnd}PM"
...

To determine which keys we need, we can build the key based on comparisons of our start and end dates using Date.getHours and Date.getMinutes.

function getHours(date) {
  const hours = date.getHours(); // 0 - 23
  return hours === 0 ? 12 : hours % 12;
}

function localizationKey(startDate, endDate) {
  const startHours = startDate.getHours();
  const startMinutes = startDate.getMinutes();
  const endHours = endDate.getHours();
  const endMinutes = endDate.getMinutes();

  const keys = ['TimeRange'];

  // First element: hour alignment.
  if (startMinutes === 0 && endMinutes === 0) {
    keys.push('HourAligned');
  } else if (startMinutes === 0) {
    ...
  }

  if (endHours === 12 && endMinutes === 0) {
    keys.push('EndsAtNoon')
  } else if {
    ...
  }

  return keys.join('.');
}

// Next, we need to interpolate values into our localized key
const locKey = localizationKey(startDate, endDate);
const hourStart = getHours(startDate);
const hourEnd = getHours(endDate);
const minuteStart = startDate.getMinutes(); // 0 - 59
const mnuteEnd = endDate.getMinutes();

intl.t(locKey, { hourStart, hourEnd, minuteStart, minuteEnd }); // 1:45PM - 2:15PM

Human Readable Relative Dates

In this example, we will use date-fns to show you how easy outputing human readable relative dates are. Of course, you will need something like ember-intl (or some internationalization library) for this to work for users across the world. However, there is a standard API Intl.RelativeTimeFormat. We will start with a solution that supports both Edge and Safari out of the box.

import { isAfter, isSameDay, formatDate, parseISO, subDays } from 'date-fns';

function humanReadableDate(comparisonDate) {
    const today = new Date();
    const yesterday = subDays(today, 1);
    const aWeekAgo = subDays(today, 7);
    const twoWeeksAgo = subDays(today, 14);
    const threeWeeksAgo = subDays(today, 21);

    // Get the date in English locale to match English day of week keys
    const compare = parseISO(comparisonDate);

    let result = '';
    if (isSameDay(compare, today)) {
        result = intl.t('Updated.Today');
    } else if (isSameDay(compare, yesterday)) {
        result = intl.t('Updated.Yesterday');
    } else if (isAfter(compare, aWeekAgo)) {
        result = intl.t(`Updated.${formatDate(compare, 'EEEE')}`);
    } else if (isAfter(compare, twoWeeksAgo)) {
        result = intl.t('Updated.LastWeek');
    } else if (isAfter(compare, threeWeeksAgo)) {
        result = intl.t('Updated.TwoWeeksAgo');
    }

    return result;
}

humanReadableDate(new Date()) // 'Updated Today'

With Intl.RelativeTimeFormat, this becomes a tad easier depending on your use case. We will stick with our example above, so you still need internationalization.

import { isAfter, isSameDay, formatDate, parseISO, subDays } from 'date-fns';

function humanReadableDate(comparisonDate) {
    const today = new Date();
    const yesterday = subDays(today, 1);
    const aWeekAgo = subDays(today, 7);
    const twoWeeksAgo = subDays(today, 14);
    const threeWeeksAgo = subDays(today, 21);

    // Get the date in English locale to match English day of week keys
    const compare = parseISO(comparisonDate);

    const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
    let result = '';
    if (isSameDay(compare, today)) {
        result = rtf(0, 'day');
    } else if (isSameDay(compare, yesterday)) {
        result = rtf(-1, 'day');
    } else if (isAfter(compare, aWeekAgo)) {
        const diffInTime = compare.getTime() - aWeekAgo.getime();
        result = rtf(diffInTime / (1000 * 3600 * 24), 'day');
    } else if (isAfter(compare, twoWeeksAgo)) {
        result = rtf(-1, 'week');
    } else if (isAfter(compare, threeWeeksAgo)) {
        result = rtf(-2, 'week');
    }

    return `${intl.t('Updated')} ${result}`;
}

humanReadableDate(new Date()) // 'Updated Today'

As with a lot of APIs, you do have options for a polyfill.

Date Libraries

date-fns is one of the best lightweight libraries if you do a lot of formatting but don’t need timezones. luxon is also recommended by the Moment.js maintainers. It supports timezones as well!

Wrapping up

I hope my rambling serves as future documentation as you refactor and tackle new features involving Dates and Times. Most of the work needed will be more verbose and likely need a greater suite of tests. However, clarity and performance enhancements can bring your software piles of unspoken “thank yous” from your future team members and users. And, if you’re looking for a more granular resource, You-Dont-Need-Momentjs offers phenomenal insights on the world of dates and how it relates to Moment.js and the general ecosystem.

DockYard is a digital product agency offering custom software, mobile, and web application development consulting. We provide exceptional professional services in strategy, user experience, design, and full stack engineering using Ember.js, React.js, Ruby, and Elixir. With a nationwide staff, we’ve got consultants in key markets across the United States, including San Francisco, Los Angeles, Denver, Chicago, Austin, New York, and Boston.