Esteban Herrera Family man, Java and JavaScript developer. Swift and VR/AR hobbyist. Like books, movies, and still trying many things. eherrera.net

More alternatives to Moment.js

11 min read 3231

More alternatives to momentjs

There’s no doubt Moment.js is one of the most popular libraries in the JavaScript ecosystem, but now that it’s considered a legacy project in maintenance mode and its use is discouraged, you may be looking for some alternatives.

At first, looking for an alternative library may not seem like an easy task because there’s a lot of things you can do with Moment.js. For example:

However, most projects don’t need all this functionality. While some projects may use Moment.js to format dates and times in a particular way (relative dates and times are popular), for other projects it may be more important to check if a date is before, after, or between other dates or displaying dates according to the locale of the user.

So probably, many projects can satisfy their requirements by using a combination of native JavaScript objects (such as Date and Intl) and optionally, one lightweight library for specific purposes.

Last year, I wrote an article reviewing alternatives to Moment.js in the context of internationalization. The libraries reviewed in that article (luxon, date-fns, day.js) are still good alternatives to Moment.js, but in this article, I’ll review the functionality of three more libraries:

Also, I’ll revisit the Intl.RelativeTimeFormat object, which reached Stage 4 at the beginning of 2020 and is now supported by more browsers.

Let’s get started.

js-joda

If you have worked with Java, you won’t have a hard time learning how to use this library because it’s a port of the Date/Time API that was introduced in Java 8 (which in turn, is based on the library Joda-Time, hence the name).

js-joda is organized into a set of immutable core classes. The main ones are:

  • LocalDate, which represents a date without time information. For example, 2020-12-01
  • LocalTime, which represents time. For example, 10:00:01.999999999
  • LocalDateTime, which represents a date with time information. For example, 2020-12-01T10:00:01.999999999

These classes don’t provide time zone information. For this, you have to use ZonedDateTime, which stores the date, time, and time zone information, and ZoneId or ZoneOffset to work with particular time zones (you’ll also need to import the package @js-joda/timezone).

We made a custom demo for .
No really. Click here to check it out.

The library also provides other classes (such as ChronoField) and types to represent portions of a date or time (such as Month). In particular, there’s a set of classes that represent amounts and points in time:

  • Duration, which represents an amount of time from nanoseconds to days. For example, 2 hours
  • Period, which represents a date-based amount of time using days, months, and years. For example, 2 years and 5 days
  • Instant, which represents a single point in time measured from 1970-01-01T00:00:00Z (including time zone information). For example, 2020-12-01T10:00:47.202Z

Most of these classes have a common interface so they can implement the same methods in different ways. For example, Temporal provides a contract with methods for manipulating date-time fields (such as days or seconds) that are implemented by classes such as Instant, LocalTime, and ZonedDateTime, among others.

This way, for parsing or creating date-time objects, all the classes have variations of the methods of, from, parse, and now. Here are some examples:

// Creates a LocalDate object from a year, month, and dayOfMonth value
const ld1 = LocalDate.of(2020, Month.DECEMBER, 1);
// Creates a LocalTime object from an ISO 8601 string
const lt1 = LocalTime.parse("10:01:00.123456789");
// Creates a LocalDateTime object from the current datetime in UTC time
const ldt1 = LocalDateTime.now(ZoneOffset.UTC);
// Creates a ZonedDateTime ojbect from from a local date, time, and a time zone
const zdt1 = ZonedDateTime.of(ld1, lt1, ZoneId.of("Europe/Paris"));
// Creates an instant from the ZonedDateTime object
const i1 = Instant.from(zdt1);
// Creates a Period object from a text string such as PnYnMnD
const p1 = Period.parse("P1Y10M");
// Creates a Duration object from a number of standard hours (positive or negative)
const d1 = Duration.ofHours(-48);

The with* methods can be used as setters (creating a new instance since everything is immutable):

// Returns a copy of the LocalDate with the value 2020/12/3
const ld2 = ld1.withDayOfMonth(3);
// Returns a copy of the Instant object with the specified seconds but without changing the nanoseconds part
const i2 = i1.withFieldValue(ChronoField.INSTANT_SECONDS, 923232434);

Or you can use the at* methods to combine two instances of different types:

// Combines a LocalDate and a LocalTime object to create a new LocalDateTime
const ldt2 = ld1.atTime(lt1);
// Combines a LocalDateTime object and a time zone to create a ZonedDateTime object
const zdt2 = ldt1.atZone(ZoneId.of("+04:00"));

In a similar way, the methods to query and check for conditions start with is:

console.log(ld1.isLeapYear());
console.log(ldt1.isBefore(ldt2));

And there are plus and minus method to manipulate the date/time information of the objects:

// Add 10 minutes: 10:11:00.123456789
const lt2 = lt1.plusMinutes(10);
// Add a duration
const ldt3 = ldt1.plus(d1);
// Subtract one hour: 09:01:00.123456789
const lt3 = lt1.minus(1, ChronoUnit.HOURS);
// Substract a period
const zdt3 = zdt1.minusAmount(p1);

Regarding date formatting, js-joda doesn’t have as many options as moment.js, but it does have a set of pattern strings (the same patterns used in Java) for custom formats. Here are some examples:

// 10:00
console.log(lt2.format(DateTimeFormatter.ofPattern("h:m")));
// 2019, 1
console.log(zdt3.format(DateTimeFormatter.ofPattern("y, Q")));

However, if your pattern contains text, you’ll have to use a locale.

To do so, import the package @js-joda/timezone along with @js-joda/locale to import all locales, or an individual locale package (@js-joda/locale_de, for example):

import "@js-joda/timezone";
import { Locale } from "@js-joda/locale_de";

// ...

// Formatting text with the DE locate: Okt. 1 2:52 PM
console.log(
  ldt3.format(
    DateTimeFormatter.ofPattern("MMM d h:m a").withLocale(Locale.GERMAN)
  )
);

You can try all the examples in this sandbox.

Sugar

Sugar is a library that provides many utility functions to work with arrays, numbers, objects, and dates, among other types.

The library contains many modules and polyfills, with a total size of around 38k gzip. However, if you don’t plan to use the complete set of functionality, you have three options:

Sugar has three modes of use:

  • Default, which uses the Sugar global object, organized into namespaces that correspond to the modules that extend the native classes. The methods are called directly on the object. For example, Sugar.Date.create("2020-01-01")
  • Chainable, which uses the Sugar namespaces as constructors to build an instance. For example,new Sugar.Date("2020-01-01").isLeapYear().raw
  • Extended, which maps the methods directly on the native types:
Sugar.Date.extend();
console.log(new Date().isLeapYear());

Here I’ll use the default mode.

To create an instance use the create() method passing a variety of formats, many of them unconventional:

const d1 = Sugar.Date.create("last month");
const d2 = Sugar.Date.create("in 2 hours");
const d3 = Sugar.Date.create("20th of May");
const d4 = Sugar.Date.create("13 Jan 2014 11:00:00 CST");
const d5 = Sugar.Date.create("the 2nd Friday of October 2009");
const d6 = Sugar.Date.create("6-2017"); // months are zero-based, this is actually May, 2017
const d7 = Sugar.Date.create("5 minutes ago");

You can see a lot of examples of how to create dates in the unit tests of the library.

Once you have created a Sugar instance, you can manipulate the date using the set method, which also accepts a boolean parameter to reset the units more specific than those passed (just remember that months are zero-based):

// Sets month to January
Sugar.Date.set(d1, { month: 0 });
// Sets day to 10 and reset hours to midnight
Sugar.Date.set(d2, { day: 10 }, true);

In Sugar, not all methods are immutable. Here you can find the list of methods that mutate the date object.

There are also methods to shift the date forward and backwards (advance() and rewind() respectively), add single units of time add*() methods) and moving the date to the beginning or end of a unit of time (beginningOf*() and endOf*() respectively):

// Shifts the date forward one month
Sugar.Date.advance(d3, { months: 1 }); // Keys can be singular too: month
// Shifts the date backwards 2 hours
Sugar.Date.rewind(d4, { hours: 2 });
// Adds 1 month
Sugar.Date.addMonths(d5, 1);
// Sets the date to Jan 1st at midgnight of the same year
Sugar.Date.beginningOfYear(d6);
// Sets the time to 11:59:59
Sugar.Date.endOfDay(d7);

Methods beginning with is allow us to compare or test dates:

// Is the year of d1 2020?
Sugar.Date.is(d1, '2020');
// Is d1 before January 2nd, 2020?
Sugar.Date.isBefore(d1, 'January 2nd, 2020');
// Is d1 after December 2018?
Sugar.Date.isAfter(d1, 'December 2018');
// Is d1 Friday?
Sugar.Date.isFriday(d1);
// Is d1 a date in the future?
Sugar.Date.isFuture(d1);
// Is d1 a weekday?
Sugar.Date.isWeekday(d1);

Sugar also provides a way to get time differences in many units with the methods *Since, *Ago, *Until, and *FromNow:

// How many months have passed since d4?
Sugar.Date.monthsSince(d4);
// How many years have passed between d4 and d5?
Sugar.Date.yearsSince(d4, d5);
// How many days ago was d4)
Sugar.Date.daysAgo(d4);
// How many hours aga from d5 was d4?
Sugar.Date.hoursAgo(d5, d4);
// How many weeks from d4 until now?
Sugar.Date.weeksUntil(d4);
// How many seconds until d5 from d4?
Sugar.Date.secondsUntil(d5, d4);
// How many ms from now to d4)
Sugar.Date.millisecondsFromNow(d4);

But what I like most about Sugar is all the options it provides for formatting. The format method supports two types of tokens, LDML, and strftime.

LDML, which are formats that are both short and easy to remember (search for a list of tokens here):

Sugar.Date.format(d3, "{Weekday}, {hours}:{mm}:{ss}{TT}"); // e.g. Saturday, 12:00:00AM

And strftime, which is used in other programming languages, such as Python (search for a list of tokens here):

Sugar.Date.format(d3, "{Weekday}, {hours}:{mm}:{ss}{TT}"); // e.g. Saturday, 12:00:00AM

In addition, there are four predefined format patterns (short, medium, long, and full):

Sugar.Date.short(d6); // 01/01/2017
Sugar.Date.medium(d6); // January 1, 2017
Sugar.Date.long(d6); // January 1, 2017 12:00 AM
Sugar.Date.full(d6); // Sunday, January 1, 2017 12:00 AM

And two methods for relative time (relative and relativeTo) that automatically choose the most appropriate unit:

Sugar.Date.relative(d2); // 2 weeks ago
Sugar.Date.relativeTo(d2, d5); // 10 years

English is the default locale included automatically. At the time of this writing, Sugar supports 17 locales, which are included in the official build or added separately via the downloads page.

A locale can be set globally with the setLocale method, or passed as an argument to locale dependent methods such as isLastWeek (so it can know the beginning of the week), create(), or relative():

import "sugar-date/locales";

// Sets the locale to italian globaly
// Sugar.Date.setLocale("it");

// Parse the string with the spanish locale
const d8 = Sugar.Date.create("hace 5 dias", "es");

// Uses the default locale, english (or the one set with setLocale)
// It doesn't use the locale used to create the instance
Sugar.Date.full(d8);

// Uses the french locale
Sugar.Date.full(d8, "fr");

You can try all the examples in this sandbox.

Spacetime

Spacetime is a library to parse, manipulate, compare, and format dates with a special focus on time zones and daylight saving time (DST) to avoid errors when manipulating times in time zones with different DST rules.

You have to be aware of some considerations, however, spacetime has an API very similar to Moment.js (with some nice additions), with the difference that all its methods are immutable.

For example, you can create a Spacetime instance using many input formats and some helper methods:

/ ISO Format
const s1 = spacetime("2020-12-01");
// As long date
const s2 = spacetime("Dec 02 2020 17:50");
// As epoch in ms
const s3 = spacetime(1606975200000);
// As an array (months are zero-based)
const s4 = spacetime([2020, 0, 1, 20, 0]);
// As an object (months are zero-based)
const s5 = spacetime({year:2020, month:0, date:1});
// Current time
const s6 = spacetime.now();
// Today at midgnight
const s7 = spacetime.today();
// Tomorrow at midnight
const s8 = spacetime.tomorrow();

Getters and setters are handled in the same way that Moment.js is, with a few helpful additions such as season(), hourFloat(), and progress():

// Sets a new date based on s1 with 400 milliseconds
const s9 = s1.millisecond(400);
// Get milliseconds: 400
console.log("s9.milliseconds(): " + s9.millisecond());
// Get month (zero-based): 11
console.log("s9.month(): " + s9.month());
// Get day of year: 336
console.log("s9.dayOfYear(): " + s9.dayOfYear());
// Get day of year: winter
console.log("s9.season(): " + s9.season());
// Set the hour + minute in decimal form
const s10 = s2.hourFloat(16.5);
// Get the time: 4:30pm
console.log("s10.time(): " + s10.time());
// How far the moment lands between the start and end of the day/week/month/year (percentage-based)
console.log("s3.progress('year'): " + s3.progress('year'));

The same happens with query or comparison methods:

// s3: 2020-12-03
// s4: 2020-01-01
console.log("s3.isAfter(s4): " + s3.isAfter(s4));
console.log("s3.isBefore(s4): " + s3.isBefore(s4));
console.log("s3.isEqual(s4): " + s3.isEqual(s4));
console.log("s3.leapYear(): " + s3.leapYear());
// Detect if two date/times are the same day, week, or year, etc
console.log("s3.isSame(s4, 'year'): " + s3.isSame(s4, "year"));
// Given a date amd a unit, count how many of them you'd need to make the dates equal
console.log("s3.diff(s4, 'day'): " + s3.diff(s4, "day"));
// Is daylight-savings-time activated right now, for this timezone?
console.log("s3.inDST(): " + s3.inDST());
// Does this timezone ever use daylight-savings?
console.log("s3.hasDST(): " + s3.hasDST());
// The current, DST-aware time-difference from UTC, in hours
console.log("s3.offset(): " + s3.offset());
// Checks if the current time is between 10pm and 8am
console.log("s3.isAsleep(): " + s3.isAsleep());

As well as with manipulation methods:

// s5: 2020-01-10 1:31pm
// Move to the first millisecond of the day, week, month, year, etc.
const s11 = s5.startOf('month'); // 2020-01-01 12:00am
// Move to the last millisecond of the day, week, month, year, etc.
const s12 = s5.endOf('week'); // 2020-01-12 11:59pm
// Increment the date/time by a number and unit
const s13 = s5.add(1, 'season') // 2020-05-10 1:31pm
// Decrease the date/time by a number and unit
const s14 = s5. subtract(2, 'years') // 2018-01-10 1:31pm
// Move forward/backward to the closest unit
const s15 = s5.nearest('hour'); // 2020-01-10 2:00pm
// Go to the beginning of the next unit
const s16 = s5.next('quarter'); // 2020-01-04 12:00am
// Go to the beginning of the previous unit
const s17 = s5.last('month'); // 2019-12-01 12:00am

To format dates and times, Spacetime has some predefined formats:

s11.format('numeric-uk') // 01/01/2020
s12.format('iso-utc') // 2020-01-13T05:59:59.999Z
s13.format('mm/dd') // 05/10
s14.format('nice') // Jan 10th, 1:31pm  
s15.format('quarter') // Q1     
// They can be combined using this syntax
s16.format('{day} {date-ordinal}, {month-short} {year}')); // Sunday 1st, Dec 2019

But you can also use more standard date format patterns:

s17.unixFmt('yyyy/MM/dd h a')); // 2019/12/01 12 AM

Or the since() function for relative times. For example, when you execute:

spacetime('September 1 2020').since('September 30 2020')

It will return the following object:

{
   "diff": {
      "years": 0,
      "months": 0,
      "days": -29,
      "hours": 0,
      "minutes": 0,
      "seconds": 0
   },
   "rounded": "in 29 days",
   "qualified": "in 29 days",
   "precise": "in 29 days"
}

About time zones, when you create an instance, you can pass an additional parameter to specify the time zone (the use of IANA names is recommended):

const s18 = spacetime(1601521200000, "Europe/Paris");
const s19 = spacetime([2020, 0, 1, 20, 0], "Lima"); // America/Lima
const s20 = spacetime.now("-4h");

But once you have an instance, you can also easily change it to another time zone with goto() (once again, the use of IANA names is recommended):

const s21 = s18.goto("Australia/Sydney"); // Oct 1 2020, 1:00pm
const s22 = s18.goto("GMT-5"); //-5 is actually +5

Also, you can get an array containing all the time zones within a range of hours using your local time as a reference:

spacetime.whereIts('12:00pm', '2:00pm') // ["asia/seoul", "asia/tokyo", "pacific/palau", "australia/adelaide", ...]
spacetime.whereIts('10am') // Within an hour, from 10am to 11am

And get the metadata of the time zone of an instance:

s21.timezone();
/* Returns:
{
   "name": "Australia/Sydney",
   "hasDst": true,
   "default_offset": 10,
   "hemisphere": "South",
   "current": {
      "offset": 10,
      "isDST": false
   },
   "change": {
      "start": "04/05:03",
      "back": "10/04:02"
   }
}
*/

You can try all the examples in this sandbox.

Intl.RelativeTimeFormat

One of the most helpful features of Moment.js is the ability to display dates as relative time:

const start = moment('2020-12-01');
const end   = moment('2020-12-04');
start.to(end); // "in 3 days"
start.to(end, true); // "3 days"

Not all libraries provide this functionality, but now that Intl.RelativeTimeFormat is fully supported by most browsers, this is not a problem anymore.

Intl.RelativeTimeFormat is a standard built-in object that allows you to format numbers as relative times in a localized way.

The constructor optionally takes two arguments, a BCP 47 language tag (or an array of such strings) and an object with properties to configure the locale matching algorithm, the format, and the length of the output message:

const rtf = new Intl.RelativeTimeFormat("es", {
    localeMatcher: "best fit",
    numeric: "auto",
    style: "short"
});

Once you have an instance of this object, you can use the format() method passing the numeric value to use in the message as the first argument, and the unit (like days or hours, in either singular or plural forms) as the second argument:

rtf.format(-4, 'second'); // hace 4 s
rtf.format(-1, 'week'); // la semana pasada
rtf.format(3, 'quarter'); // dentro de 3 trim.
rtf.format(2, 'year'); // dentro de 2 a

Or formatToParts() to get an array with the parts of the message separated:

rtf.formatToParts(-4, 'second');
/* Returns:
[
   {
      "type": "literal",
      "value": "hace "
   },
   {
      "type": "integer",
      "value": "4",
      "unit": "second"
   },
   {
      "type": "literal",
      "value": " s"
   }
]
**/

And if you’re wondering how to get the numeric part, in other words, the elapsed time between two dates, check out this StackOverflow answer that shares the following function:

// in miliseconds
var units = {
  year  : 24 * 60 * 60 * 1000 * 365,
  month : 24 * 60 * 60 * 1000 * 365/12,
  day   : 24 * 60 * 60 * 1000,
  hour  : 60 * 60 * 1000,
  minute: 60 * 1000,
  second: 1000
};

var rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

var getRelativeTime = (d1, d2 = new Date()) => {
  var elapsed = d1 - d2;
  // "Math.abs" accounts for both "past" & "future" scenarios
  for (var u in units)
    if (Math.abs(elapsed) > units[u] || u == 'second')
      return rtf.format(Math.round(elapsed/units[u]), u);
}

You can try all the examples in this sandbox.

Conclusion

There’s no doubt there are a lot of alternatives to Moment.js. In this article, we have reviewed three libraries that provide similar functionality and a few useful additions in some cases.

In my opinion, each of those libraries is better at different use cases:

  • js-joda works great as a general-purpose library, in particular, if you have a Java background
  • Sugar is particularly good for formatting dates, and since it provides many methods for types other than dates, it can be useful for other parts of your application
  • Spacetime is particularly good for when you have to support multiple time zones due to the way it calculates remote times and the methods for changing between time zones.

Also, now the JavaScript Internationalization API is more widely supported by browsers, consider the combination of a lightweight library and native features or even if you need an external library at all.

Happy coding!

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    Are you adding new JS libraries to improve performance or build new features? What if they’re doing the opposite?

    There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.

    LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.

    https://logrocket.com/signup/

    LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

    Build confidently — .

    Esteban Herrera Family man, Java and JavaScript developer. Swift and VR/AR hobbyist. Like books, movies, and still trying many things. eherrera.net

    Leave a Reply