Date and Time - Learning JavaScript (2016)

Learning JavaScript (2016)

Chapter 15. Date and Time

Most real-world applications involve working with date and time data. Unfortunately, JavaScript’s Date object (which also stores time data) is not one of the language’s best-designed features. Because of the limited utility of this built-in object, I will be introducing Moment.js, which extends the functionality of the Date object to cover commonly needed functionality.

It’s an interesting bit of history that JavaScript’s Date object was originally implemented by Netscape programmer Ken Smith—who essentially ported Java’s java.util.Date implementation into JavaScript. So it’s not entirely true that JavaScript has nothing to do with Java: if anyone ever asks you what they have to do with each other, you can say, “Well, aside from the Date object and a common syntactic ancestor, very little.”

Because it gets tedious to keep repeating “date and time,” I will use “date” to implicitly mean “date and time.” A date without a time is implicitly 12:00 A.M. on that day.

Dates, Time Zones, Timestamps, and the Unix Epoch

Let’s face it: our modern Gregorian calendar is a fussy, overcomplicated thing, with 1-based numbering, odd divisions of time, and leap years. Time zones add even more complexity. However, it’s (mostly) universal, and we have to live with it.

We’ll start with something simple: the second. Unlike the complicated division of time in the Gregorian calendar, seconds are easy. Dates and times—as represented by seconds—are a single number, neatly ordered on a number line. Representing dates and time in seconds is therefore ideal for computation. However, it doesn’t work so well for human communication: “Hey, Byron, want to have lunch at 1437595200?” (1437595200 is Wednesday, July 22, 2015, at 1 P.M. Pacific Standard Time). If dates are represented by seconds, what date corresponds to 0? It isn’t, as it turns out, the birth of Christ, but an arbitrary date: January 1, 1970, 00:00:00 UTC.

As you’re probably aware, the world is divided into time zones (TZs) so that, no matter where you are in the morning, 7 A.M. is morning and 7 P.M. is evening. Time zones can get complicated fast, especially as you start to consider daylight saving time. I won’t attempt to explain all the nuances of the Gregorian calendar or time zones in this book—Wikipedia does an excellent job of that. However, it’s important to cover some of the basics to help us understand the JavaScript Date object (and what Moment.js brings to the table).

All time zones are defined as offsets from Coordinated Universal Time (abbreviated UTC—refer to Wikipedia for the complicated and somewhat hilarious reasons). UTC is sometimes (and not entirely correctly) referred to as Greenwich Mean Time (GMT). For example, I’m currently in Oregon, which is in the Pacific time zone. Pacific time is either eight or seven hours behind UTC. Wait, eight or seven? Which is it? Depends on the time of year. In the summer, it’s daylight saving time, and the offset is seven. The rest of the year, it’s standard time, and the offset is eight. What’s important here is not memorizing time zones, but understanding how the offsets are represented. If I open up a JavaScript terminal, and type new Date(), I see the following:

Sat Jul 18 2015 11:07:06 GMT-0700 (Pacific Daylight Time)

Note that in this very verbose format, the time zone is specified both as an offset from UTC (GMT-0700) and by its name (Pacific Daylight Time).

In JavaScript, all Date instances are stored as a single number: the number of milliseconds (not seconds) since the Unix Epoch. JavaScript normally converts that number to a human-readable Gregorian date whenever you request it (as just shown). If you want to see the numeric representation, simply use the valueOf() method:

const d = new Date();

console.log(d); // formatted Gregorian date with TZ

console.log(d.valueOf()); // milliseconds since Unix Epoch

Constructing Date Objects

The Date object can be constructed in four ways. Without any arguments (as we’ve seen already), it simply returns a Date object representing the current date. We can also provide a string that JavaScript will attempt to parse, or we can specify a specific (local) date down to the millisecond. Here are examples:

// all of the below are interpreted with respect to local time

new Date(); // current date

// note that months are zero-based in JavaScript: 0=Jan, 1=Feb, etc.

new Date(2015, 0); // 12:00 A.M., Jan 1, 2015

new Date(2015, 1); // 12:00 A.M., Feb 1, 2015

new Date(2015, 1, 14); // 12:00 A.M., Feb 14, 2015

new Date(2015, 1, 14, 13); // 3:00 P.M., Feb 14, 2015

new Date(2015, 1, 14, 13, 30); // 3:30 P.M., Feb 14, 2015

new Date(2015, 1, 14, 13, 30, 5); // 3:30:05 P.M., Feb 14, 2015

new Date(2015, 1, 14, 13, 30, 5, 500); // 3:30:05.5 P.M., Feb 14, 2015

// creates dates from Unix Epoch timestamps

new Date(0); // 12:00 A.M., Jan 1, 1970 UTC

new Date(1000); // 12:00:01 A.M., Jan 1, 1970 UTC

new Date(1463443200000); // 5:00 P.M., May 16, 2016 UTC

// use negative dates to get dates prior to the Unix Epoch

new Date(-365*24*60*60*1000); // 12:00 A.M., Jan 1, 1969 UTC

// parsing date strings (defaults to local time)

new Date('June 14, 1903'); // 12:00 A.M., Jun 14, 1903 local time

new Date('June 14, 1903 GMT-0000'); // 12:00 A.M., Jun 14, 1903 UTC

If you’re trying these examples out, one thing you’ll notice is that the results you get are always in local time. Unless you are on UTC (hello, Timbuktu, Madrid, and Greenwich!), the results listed in UTC will be different than shown in this example. This brings us to one of the main frustrations of the JavaScript Date object: there’s no way to specify what time zone it should be in. It will always store objects internally as UTC, and format them according to local time (which is defined by your operating system). Given JavaScript’s origin as a browser-based scripting language, this has traditionally been the “right thing to do.” If you’re working with dates, you probably want to display dates in the user’s time zone. However, with the global nature of the Internet—and with Node bringing JavaScript to the server—more robust handling of time zones would be useful.

Moment.js

While this book is about the JavaScript language itself—not libraries—date manipulation is such an important and common problem that I have decided to introduce a prominent and robust date library, Moment.js.

Moment.js comes in two flavors: with or without time zone support. Because the time zone version is significantly larger (it has information about all the world’s time zones), you have the option of using Moment.js without time zone support. For simplicity, the following instructions all reference the time zone–enabled version. If you want to use the smaller version, visit http://momentjs.com for information about your options.

If you’re doing a web-based project, you can reference Moment.js from a CDN, such as cdnjs:

<script src="//cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.4.0/ ↩

moment-timezone.min.js"></script>

If you’re working with Node, you can install Moment.js with npm install --save moment-timezone and then reference it from your script with require:

const moment = require('moment-timezone');

Moment.js is large and robust, and chances are, if you need to manipulate dates, it has exactly the functionality you need. Consult its excellent documentation for more information.

A Practical Approach to Dates in JavaScript

Now that we have the basics out of the way, and you have Moment.js available, we’re going to take a slightly different approach to covering this information. An exhaustive coverage of the methods available on the Date object would be dry and not very useful to most people. Furthermore, if you need that reference, MDN’s coverage of the Date object is exhaustive and well-written.

Instead, this book will take a more cookbook-based approach and cover the common date processing needs that people have, and use Date and Moment.js where appropriate.

Constructing Dates

We’ve already covered the construction options available to you with JavaScript’s Date object, which are, for the most part, adequate. Whenever you construct a date without an explicit time zone, you have to think about what time zone is being used, which is going to be dependent on wherethe date is being constructed. This has tripped up many a beginner in the past: they use the same date code on a server in Arlington, Virginia, as they use in a user’s browser connecting from Los Angeles, California, and they are surprised when the dates are off by three hours.

Constructing Dates on the Server

If you’re constructing dates on the server, I recommend always either using UTC or explicitly specifying the time zone. With today’s cloud-based approach to application development, the same code base could be running on servers all over the world. If you’re constructing local dates, you’re asking for trouble. If you are able to use UTC dates, you can construct them with the Date object’s UTC method:

const d = new Date(Date.UTC(2016, 4, 27)); // May 27, 2016 UTC

NOTE

Date.UTC takes all the same variations of arguments as the Date constructor, but instead of returning a new Date instance, it returns the numeric value of the date. That number can then be passed into the Date constructor to create a date instance.

If you need to construct dates on the server that are in a specific time zone (and don’t want to do the time zone conversion by hand), you can use moment.tz to construct Date instances using a specific time zone:

// passing an array to Moment.js uses the same parameters as JavaScript's Date

// constructor, including zero-based moths (0=Jan, 1=Feb, etc.). toDate()

// converts back to a JavaScript Date object.

const d = moment.tz([2016, 3, 27, 9, 19], 'America/Los_Angeles').toDate();

Constructing Dates in the Browser

Generally, JavaScript’s default behavior is appropriate in the browser. The browser knows from the operating system what time zone it’s in, and users generally like to work in local time. If you’re building an app that needs to handle dates in other time zones, then you’ll want to use Moment.js to handle the conversion and display of dates in other time zones.

Transmitting Dates

Where things get interesting is transmitting dates—either the server sending dates to the browser or vice versa. The server and browser could be in different time zones, and users want to see dates in their local time zone. Fortunately, because JavaScript Date instances store the date as a numeric offset from the UTC, Unix Epoch, it’s generally safe to pass Date objects back and forth.

We’ve been talking about “transmitting” very vaguely, though: what exactly do we mean? The surest way to ensure that dates are transmitted safely in JavaScript is using JavaScript Object Notation (JSON). The JSON specification doesn’t actually specify a data type for dates, which is unfortunate, because it prevents symmetric parsing of JSON:

const before = { d: new Date() };

before.d instanceof date // true

const json = JSON.stringify(before);

const after = JSON.parse(json);

after.d instdanceof date // false

typeof after.d // "string"

So the bad news is that JSON can’t seamlessly and symmetrically handle dates in JavaScript. The good news is that the string serialization that JavaScript uses is always consistent, so you can “recover” a date:

after.d = new Date(after.d);

after.d instanceof date // true

No matter what time zone was originally used to create the date, when it is encoded as JSON, it will be in UTC, and when the JSON-encoded string is passed to the Date constructor, the date will be displayed in the local time zone.

The other safe way to pass dates between client and server is to simply use the numeric value of the date:

const before = { d: new Date().valueOf() };

typeof before.d // "number"

const json = JSON.stringify(before);

const after = JSON.parse(json);

typeof after.d // "number"

const d = new Date(after.d);

WARNING

While JavaScript is happily consistent when JSON-encoding dates as strings, the JSON libraries provided by other languages and platforms are not. The .NET JSON serializers in particular wrap JSON-encoded date objects in their own proprietary format. So if you’re interfacing with JSON from another system, take care to understand how it serializes dates. If you have control over the source code, this is an instance where it may be safer to transmit numeric dates as offsets from the Unix Epoch. Even here you must be careful, however: often date libraries will give the numeric value in seconds, not milliseconds.

Displaying Dates

Formatting dates for display is often one of the most frustrating problems for beginners. JavaScript’s built-in Date object includes only a handful of prepackaged date formats, and if they don’t meet your needs, it can be painful to do the formatting yourself. Fortunately, Moment.js excels in this area, and if you are particular about how dates are displayed, then I recommend you use it.

To format a date with Moment.js, use its format method. This method takes a string with metacharacters that are replaced with the appropriate component of the date. For example, the string "YYYY" will be replaced with the four-digit year. Here are some examples of date formatting with the Date object’s built-in methods, and the more robust Moment.js methods:

const d = new Date(Date.UTC(1930, 4, 10));

// these show output for someone in Los Angeles

d.toLocaleDateString() // "5/9/1930"

d.toLocaleFormat() // "5/9/1930 4:00:00 PM"

d.toLocaleTimeString() // "4:00:00 PM"

d.toTimeString() // "17:00:00 GMT-0700 (Pacific Daylight Time)"

d.toUTCString() // "Sat, 10 May 1930, 00:00:00 GMT"

moment(d).format("YYYY-MM-DD"); // "1930-05-09"

moment(d).format("YYYY-MM-DD HH:mm"); // "1930-05-09 17:00

moment(d).format("YYYY-MM-DD HH:mm Z"); // "1930-05-09 17:00 -07:00

moment(d).format("YYYY-MM-DD HH:mm [UTC]Z"); // "1930-05-09 17:00 UTC-07:00

moment(d).format("dddd, MMMM [the] Do, YYYY"); // "Friday, May the 9th, 1930"

moment(d).format("h:mm a"); // "5:00 pm"

This example shows how inconsistent and inflexible the built-in date formatting options are. In JavaScript’s favor, these built-in formatting options do attempt to provide formatting that’s appropriate for the user’s locale. If you need to support date formatting in multiple locales, this is an inexpensive but inflexible way to do it.

This is not designed to be an exhaustive reference to the formatting options available in Moment.js; see the online documentation for that. What it is designed to communicate is that if you have date formatting needs, Moment.js can almost certainly meet them. Like many such date formatting metalanguages, there are some common conventions. More letters means more verbose; that is, "M" gets you 1, 2, 3,...; "MM" gets you 01, 02, 03,...; "MMM" gets you Jan, Feb, Mar,...; and "MMMM" gets you January, February, March,.... A lowercase "o" will give you an ordinal: "Do"will give you 1st, 2nd, 3rd, and so on. If you want to include letters that you don’t want interpreted as metacharacters, surround them with square brackets: "[M]M" will give you M1, M2, and so on.

NOTE

One frustration that Moment.js doesn’t completely solve is the use of time zone abbreviations, such as EST and PST. Moment.js has deprecated the z formatting character due to lack of consistent international standards. See the Moment.js documentation for a detailed discussion of the issues with time zone abbreviations.

Date Components

If you need to access individual components of a Date instance, there are methods for that:

const d = new Date(Date.UTC(1815, 9, 10));

// these are the results someone would see in Los Angeles

d.getFullYear() // 1815

d.getMonth() // 9 - October

d.getDate() // 9

d.getDay() // 1 - Monday

d.getHours() // 17

d.getMinutes() // 0

d.getSeconds() // 0

d.getMilliseconds() // 0

// there are allso UTC equivalents to the above:

d.getUTCFullYear() // 1815

d.getUTCMonth() // 9 - October

d.getUTCDate() // 10

// ...etc.

If you’re using Moment.js, you’ll find little need to work with individual components, but it’s good to know that they’re there.

Comparing Dates

For simple date comparisons—does date A come after date B or vice versa?—you can use JavaScript’s built-in comparison operators. Remember that Date instances store the date as a number, so the comparison operators simply work on the numbers:

const d1 = new Date(1996, 2, 1);

const d2 = new Date(2009, 4, 27);

d1 > d2 // false

d1 < d2 // true

Date Arithmetic

Because dates are just numbers, you can subtract dates to get the number of milliseconds between them:

const msDiff = d2 - d1; // 417740400000 ms

const daysDiff = msDiff/1000/60/60/24; // 4834.96 days

This property also makes it easy to sort dates using Array.prototype.sort:

const dates = [];

// create some random dates

const min = new Date(2017, 0, 1).valueOf();

const delta = new Date(2020, 0, 1).valueOf() - min;

for(let i=0; i<10; i++)

dates.push(new Date(min + delta*Math.random()));

// dates are random and (probably) jumbled

// we can sort them (descending):

dates.sort((a, b) => b - a);

// or ascending:

dates.sort((a, b) => a - b);

Moment.js brings many powerful methods for doing common date arithmetic, allowing you to add or subtract arbitrary units of time:

const m = moment(); // now

m.add(3, 'days'); // m is now three days in the future

m.subtract(2, 'years'); // m is now two years minus three days in the past

m = moment(); // reset

m.startOf('year'); // m is now Jan 1 of this year

m.endOf('month'); // m is now Jan 31 of this year

Moment.js also allows you to chain methods:

const m = moment()

.add(10, 'hours')

.subtract(3, 'days')

.endOf('month');

// m is the end of the month you would be in if you

// traveled 10 hours into the future then 3 days back

User-Friendly Relative Dates

Very often, it’s nice to be able to present date information in a relative fashion: “three days ago” as opposed to a date. Moment.js makes this easy:

moment().subtract(10, 'seconds').fromNow(); // a few seconds ago

moment().subtract(44, 'seconds').fromNow(); // a few seconds ago

moment().subtract(45, 'seconds').fromNow(); // a minute ago

moment().subtract(5, 'minutes').fromNOw(); // 5 minutes ago

moment().subtract(44, 'minutes').fromNOw(); // 44 minutes ago

moment().subtract(45, 'minutes').fromNOw(); // an hour ago

moment().subtract(5, 'hours').fromNOw(); // 4 hours ago

moment().subtract(21, 'hours').fromNOw(); // 21 hours ago

moment().subtract(22, 'hours').fromNOw(); // a day ago

moment().subtract(344, 'days').fromNOw(); // 344 days ago

moment().subtract(345, 'days').fromNOw(); // a year ago

As you can see, Moment.js has chosen some arbitrary (but reasonable) breakpoints for when to switch to displaying a different unit. It’s a handy way to get user-friendly relative dates.

Conclusion

If you take away three things from this chapter, they should be the following:

§ Internally, dates are represented as the number of milliseconds from the Unix Epoch (Jan 1, 1970 UTC).

§ Be aware of the time zone when you’re constructing dates.

§ If you want sophisticated date formatting, consider Moment.js.

In most real-world applications, it’s hard to get away from date processing and manipulation. Hopefully this chapter has given you a foundation in the important concepts. The Mozilla Developer Network and Moment.js documentation serve as a thorough and detailed reference.