Category: Moment.js

Why Moment.js Isn’t Immutable Yet

This post reflects my opinions about immutability in Moment.js. I do not speak for our entire team.

About five months ago now, I joined the maintainers team of Moment.js. I actually didn’t have much OSS experience before joining the Moment team, so jumping into such a massively popular library has been quite the experience. I have learned a ton about JavaScript, dates, and even how standards bodies work. What I have learned the most about though, is what it means to be an OSS maintainer.

The first thing that I will tell you about any OSS maintainer is that we all want to make you, the user, happy. While we’re admittedly getting some benefit from a ‘personal brand’ standpoint, none of us on the Moment team have ever made a single dollar off of the library directly. We do it for the love of code, and for the love of our users.

Sometimes though, we end up in a position where we can’t deliver to our users in the way that we want to. One such example is changing Moment to have an immutable API.
This issue about making Moment an immutable library has been open since 2014, and in fact discussions of immutability of the library date back to 2012. There are hundreds of comments on the issue, and it gets a +1 type comment or thumbs up at least once a week. With the increase in popularity of React.js, the interest in this issue is going steadily up. We have a couple other big issues that also get +1’d a lot, but the interesting thing about immutability is that the people giving feedback on this issue are PASSIONATE about it. This is not a ‘nice to have’ for them. They believe it is an essential feature of the library that is missing.

Now, it is important to know that all of Moment’s maintainers agree that date and time types should be immutable. If we were sitting down and writing a new date and time library today, that is how we would make it. However, changing Moment to be an immutable API has some  very big logistical concerns for us.

In fact, changing Moment from mutable to immutable is almost like making a different library. If this change is done ‘in full’ and not as an add-on, it potentially creates a breaking change for practically every line of code that uses Moment.

But what does that really mean?

Moment.js has been around for five years, which is an eternity in the world of JavaScript libraries, and it isn’t really slowing down in usage. A quick analysis of some stats:

  • 4.5 million NPM downloads a month
  • 15,000 visits a day to Momentjs.com
  • Average of 75 stack overflow questions a month in the last year
  • 40 GitHub Issues for June 2016 (so far)
  • 21 Pull Requests for June 2016 (so far)

These stats are just for Moment.js core. Moment TimeZone and the docs add many more issues and pull requests.

We have no way of knowing how many people are using Moment. We can know the NPM download number, but I would be surprised if that represents more than half of our user base. Other package managers, CDNs, etc are adding many more.

What we do know is that because of those numbers our workload currently looks like this:

Iskren – Moment.js releases, pull request reviews, major code changes
Tim – All Moment TimeZone code and releases
Isaac – GitHub issue management, pull request reviews
Matt – Moment TimeZone GitHub issue management, Stack Overflow questions
Maggie – Documentation maintenance, Stack Overflow questions

All of us do a little bit of every category you see there, but this is generally how everything shakes out. The critical thing to notice though, is that very little of this time is writing actual code for the library – our fantastic community of contributors is really driving that. We’re just making sure it gets delivered to people.

The trick is, between the paying jobs, family, and stuff that just comes up in life, all of us are kept pretty busy.

We’re actually not concerned about the time it would take to write the code for a new major version of Moment that was immutable. If we agreed upon a time frame, a couple of us could probably set aside the rest of life’s concerns for a couple weeks and get this done.

What we are concerned about is having two forks to maintain. From the outside, it’s easy to say “use SemVer, end of life the other fork, and move on”. We have too many users for this. The bottom line is that we can make as many ‘end of life’ and ‘end of support’ announcements about 2.0 as we want, and we can set as many timelines as we please. It won’t change the fact that Moment 2.0 will be in hundreds of thousands of code bases, if not more, for years to come. One need only look to the prevalence of websites still using jQuery 1.* versions to know this is true.

What this means is that we will still be getting GitHub issues, bug reports, Stack Overflow questions and possibly even pull requests on the 2.0 fork for (potentially) years. And unlike jQuery , we don’t have a foundation backing us to help us have the time to answer these.

And as I said in the beginning, we want to make people happy. So we really don’t want to tell everyone ‘too bad, so sad, upgrade to the newest immutable version.’ Because we’re all coders at our day job. And we all know how difficult it can be to get ‘upgrade JavaScript library’ on a development schedule, especially when that library has made a change like moving to immutability, where potentially every line of your code that uses the library could be affected.

In the face of all that, with the mutability vs immutability issue, it all comes down to this one fact:

The mutable library does what people need it to do.

In writing this library, Tim (the author), Iskren (the current lead), and the rest of the community managed to improve millions of code bases. People use it every day. It solves their problems.

An immutable API would be better. But would it be so much better that we should tell all of the users we have out in the world that we can’t support their (working) code as it is today?

We haven’t ruled out immutability. Maybe it is worth it. It will remain in discussion, along with work-arounds like making this plugin an official plugin that ships with the library, which is frankly a more likely route. But this is why it hasn’t happened yet.

I would ask anybody who is rallying for immutability in Moment this question:

Do you want Moment to be immutable, or is Moment not the library you want?

Moment.JS Shows the Wrong Date!

I have been answering some Stack Overflow questions lately, and it seems like quite a lot of them all boil down to someone saying that Moment.JS is showing the wrong date.

Here are some of the most common reasons why Moment might not be showing what you expect, and the ways to get it to do what you want:

The console shows the wrong date when I dump a Moment object

I might run code that looks something like this:

MomentConsoleUTC

As you can see, I parsed January 1, but am seeing December 31 in the console.

When you dump a Moment object to the console, you see Object.prototype.toString() being called on the _d variable, which is the built-in JavaScript date object that Moment wraps. In most JavaScript implementations, that will cause that object to display in local time. I parsed a UTC time though, so I’m seeing the wrong result in the console. If I had used Moment Timezone, I would also be seeing a wrong value in the console.

This is easy to work around. Ignore the value of _d – it is not useful information. Instead, rely on .format() to get the correct result:

moment.utc('2016-01-01').format()
2016-01-01T00:00:00Z

.format() Shows a Date I Didn’t Parse

Most often, when .format() isn’t giving the expected result, it is because the date was parsed with an offset, and the wrong Moment constructor function was used. This is a little bit of a complicated statement, but let’s break it down.

First, the date format causing confusion is usually ISO 8601 format with offset. That format looks like this:

2016-05-14T08:33:14-03:00

This indicates that the date in question is May 14 2016 at 8:33 AM in local time, and that the local time of this date is offset three hours behind UTC.

You may also see an ISO8601 formatted date that looks like this:

2016-05-14T13:37:09Z

Note that the z in the date above indicates that it is in UTC, or has an offset of +00:00.

This is a very helpful format in that it reflects both local time, and global time in the same time stamp. It can get people into trouble when making moments though.

Moment offers four ways to construct a moment object, and all of them will use this offset information to achieve different results.

The moment constructor functions are as follows:

  • moment()
  • moment.utc()
  • moment.parseZone()
  • moment.tz() (this only works with the Moment TimeZone add on)

The moment() Function

When you use the default moment constructor, it will convert the date from the provided offset to the environment’s local time. Right now my local time is -05:00. As such, if I take my -03:00 date from above, and parse it with moment, I get the following:

moment('2016-05-14T08:33:14-03:00').format()
"2016-05-14T06:33:14-05:00"

As you can see, the time has been shifted from 08:33:14 to 06:33:14 to reflect the offset change.

It is important to note that the default moment constructor will use information from the browser to keep the moment in the user’s local time, even when offsets change due to DST. If I add six months to this moment, my local offset will be -06:00. This will be properly handled:

moment('2016-05-14T08:33:14-03:00').add(6, 'months').format()
2016-11-14T06:33:14-06:00

The moment.utc() Function

When you use the moment UTC constructor, it will convert the date from the offset provided to UTC. Using the same date string:

moment.utc('2016-05-14T08:33:14-03:00').format()
"2016-05-14T11:33:14Z"

As you can see, this time the date has been shifted forward three hours, from 08:33:14 to 11:33:14 to be in line with UTC.

The moment.parseZone() Function

The .parseZone() function will parse a moment with the offset fixed to the provided offset.

It is very important to remember that this function is mis-named. When you use .parseZone() you are parsing the moment to have a fixed offset. A fixed offset is NOT a time zone. Most time zones will have more than one offset at various points in the year due to DST. Also, most time zones have changed their offset over time. Any math performed with a moment in fixed offset mode may yield unexpected results. The use cases for a fixed offset moment are limited. See the Stack Overflow timezone tag for more information on time zones vs offsets.

Using my same time, I get the following result with .parseZone():

moment.parseZone('2016-05-14T08:33:14-03:00').format()
2016-05-14T08:33:14-03:00

As you can see, the time and offset are unchanged.

The moment.tz() Function

The moment.tz() function will parse a given time with offset and convert it to the time zone provided. It is useful when you need to interpret a time in a time zone other than UTC, or the user’s local time.

Note that this functionality does not work without the Moment TimeZone add on library being included in your project. The Moment TimeZone library uses the IANA time zones to provide time zone support. A list of IANA time zone identifiers and offsets can be found here.

If you only need to work in UTC, and the user’s local time, do NOT bring in the Moment TimeZone library. It only causes extra code to be sent to the browser. Of course if you are in Node, feel free to bring it in everywhere just in case.

Using the same date and time as before, I will specify the time be parsed to Europe/Berlin. This time zone is UTC +2 at the time of year specified:

moment.tz('2016-05-14T08:33:14-03:00', 'Europe/Berlin').format()
2016-05-14T13:33:14+02:00

As you can see, the time has been shifted five hours forward, to move from -03:00 to +02:00. The resultant time is 13:33. This moment will be time zone aware, so if you were to add six months to it, at which point Berlin will have an offset of -01:00, the moment will correctly change offsets:

moment.tz('2016-05-14T08:33:14-03:00', 'Europe/Berlin').add(6, 'months').format()
2016-11-14T13:33:14+01:00

But I Don’t WANT the Offset

A common complaint about these functions is that people just don’t want the offset to be used. They wish for the date to be interpreted as local time.

If you are thinking this, please use caution. When you get a date with an offset, you are getting valuable information about both local time, and the point on the global timeline. Throwing this information away may not be helpful down the road.

If you are sure that you do not want the offset information, the best practice would be to have the system that you are getting the date from stop sending the offset. Any other choice may be misinterpreted later.

As developers though, we live in reality. We can’t always change the data that is being sent to us. If you need to ignore the offset, you have a few options.

If the offset is z, so all dates are being sent in UTC, just parse in UTC mode. No change will be made to the time.

moment.utc('2016-05-14T08:33:14Z').format('LLL')
May 14, 2016 8:33 AM

If the offset is some other number, and you wish to ignore it, you can use .parseZone(), and simply not display the offset information:

moment.parseZone('2016-05-14T08:33:14-03:00').format('LLL')
May 14, 2016 8:33 AM

Remember though, both of the above methods will not handle DST transitions in the user’s local time. Thus, mathematical operations can have unexpected results.

If you want to interpret the date as the user’s local time, and be able to safely do math with it, you can specify a format that has no Z token, and thus ignores the offset:

moment('2016-05-14T08:33:14-03:00', 'YYYY-MM-DDTHH:mm:ss').format()
2016-05-14T08:33:14-05:00

As you can see, the time has been interpreted as local and has my offset. Note that this will not work in strict parsing mode, as there will be left over training characters. It will result in an invalid date.

Moment.js Locale Inheritance

Moment.js released version 2.12.0 a couple weeks ago, and with that came a major new feature – locale inheritance.

Defining custom locale data has been possible in Moment.js for a very long time, and it is one of the coolest features of the library.

Suppose I wanted create a locale that was specifically for mobile use, which was in French, but had shorter strings for Moment’s relative time feature. Previous to 2.12, I might have tried something like this:

moment.locale('fr-mobile', 
      {   
      relativeTime : {
          future : 'dans %s',
          past : 'il y a %s',
          s : 's',
          m : 'une m',
          mm : '%d m',
          h : 'une h',
          hh : '%d h',
          d : 'un j',
          dd : '%d j',
          M : 'un mo',
          MM : '%d mo',
          y : 'un an',
          yy : '%d ans'
      }
    });

 

With 2.11.2 and previous, the result would have been great when I called .fromNow():

moment.locale('fr-mobile');
moment().subtract(2, 'minute').fromNow()
il y a 2 m

But I would have gotten a surprise when I called .calendar():

moment.locale('fr-mobile');
moment().calendar();
Today at 10:27 PM

 

English! Whoops. This is because there was no locale inheritance in Moment.js. Locales always fell back to English if data was not defined. To create this fr-mobile locale, I would have had to define every locale property available in Moment.

Enter locale inheritance.

Now I can just define a parent locale for my mobile locale:

moment.locale('fr-mobile', 
      {  parentLocale:'fr', 
        relativeTime : {
            future : 'dans %s',
            past : 'il y a %s',
            s : 's',
            m : 'une m',
            mm : '%d m',
            h : 'une h',
            hh : '%d h',
            d : 'un j',
            dd : '%d j',
            M : 'un mo',
            MM : '%d mo',
            y : 'un an',
            yy : '%d ans'
        }
    });
    moment.locale('fr-mobile');
    moment().subtract(2, 'minute').fromNow()
    //il y a 2 m
    moment().calendar()
    //Aujourd'hui à 22:33

 

Much Better!

Previously, there was also an issue with partial object definitions. Suppose I wanted to change just a couple things about relative time in the French locale. I would have tried this:

moment.locale('fr', 
      {   
      relativeTime : {
          future : 'dans %s',
          past : 'il y a %s',
          s : 's',
          m : 'une m',
          mm : '%d m',
      }
    });
    moment.locale('fr');
    moment().subtract(2, 'minute').fromNow()
    //il y a 2 m
    moment().subtract(1, 'year').fromNow()
    //Uncaught TypeError: Cannot read property 'replace' of undefined

That didn’t go well did it?

Enter updateLocale in 2.12.

moment.updateLocale('fr', 
      { 
      relativeTime : {
          future : 'dans %s',
          past : 'il y a %s',
          s : 's',
          m : 'une m',
          mm : '%d m',
      }
    });
    moment.locale('fr');
    moment().subtract(2, 'minute').fromNow();
    //il y a 2 m
    moment().subtract(1, 'year').fromNow();
    //il y a un an

 

Big thanks to Iskren for getting this in.

Save effort, save file size, be happy!

Moment.js and Klingon Your Dates

So, I haven’t blogged in a long time. There are a lot of reasons for this, but the main one is that I’ve found a project.

I recently was invited to join the Moment.js core collaborators team. How this happened is a weird accident of networking. At CodeMash in January I was told I had to meet Matt Johnson. It turns out that Matt and I have both:

  1. Given conference talks on RavenDB
  2. Developed a time and attendance solution

This is a a very weird set of things to have in common with someone. If there is a third person in the world who has done these two things, let Matt and I know – we would like to be your friend.

Anyways, Matt is on the Moment.js team, and he decided that I should take what I know about date and time and come help out, so here I am.

Over the past month I have had a huge amount of fun working on the project. At this point I mostly write documentation and answer people’s questions on GitHub, but I have snuck a couple pull requests in :-).

On the subject of fun, it is a little known fact that as of 2.11.0 Moment.js supports Klingon as a locale, thanks to some wonderful nerds on the internet.

I felt compelled to make something out of this, so this past weekend I set out to change all the dates in your browser to Klingon via chrome extension.

As it turns out, chrome extensions are crazy easy to make. In order to support Klingon-ing of all dates, I simply queried the DOM with some regular expressions that would match a lot of common English date formats, and then fed them into a function. All of the code relevant to Moment.js is right here:

    function toKlingon (match) {
        //parse the string using the format that the Regular expression is looking for
        var a = moment(match, format);
        //change to the klingon locale and return formated as local long date
        return a.locale('tlh').format('LL');
    }

 

Rest assured that the DOM parsing involved was substantially more difficult than this.

This is a great example of just how well Moment.js does i18n. I don’t need to know anything about this language to be able to output dates formatted exactly as native speakers would.

The extension does have an issue where it will always display all date parts, even if they weren’t parsed. For example, dates from Gmail, which are written as “Mar 13” are output with a year.

I am working on a minor change to Moment that will expose what date parts were actually parsed, which would allow me to easily include only valid data in this situation.

I also need to do some more work on better/faster DOM parsing with the regular expressions, as it still doesn’t get quite every date, but generally the extension works. If you would like to try it, find it in the chrome store here.