Skip to content
academia.sh

Lesson 21 / 21

Date and Time

The timestamp model, the distinction between local time and coordinated universal time, parsing traps, date arithmetic, and locale-aware formatting.

Contents

Up to this lesson, the measurement records were timeless: it was not known when any given reading was taken. This lesson adds time representation and closes the course.

Date–time is the language’s most misused area. The reason is a single confusion: an instant and a representation of that instant are not the same thing. This lesson establishes the distinction and derives the traps from it.

The output of the examples below depends on the timezone of the machine they run on. To make them reproducible, all of them were run with the timezone given explicitly:

TZ=Europe/Istanbul node measurement.mjs

The Timestamp

The only data a date object carries is the number of milliseconds elapsed since a fixed starting instant. This number is the timestamp, and it is independent of timezone.

const stamp = 1709287200000;
const t = new Date(stamp);
console.log(t.getTime());
console.log(t.toISOString());
console.log(Number(t) === stamp);
1709287200000
2024-03-01T10:00:00.000Z
true

When the same script is run in a different timezone, these three lines do not change. The stamp is an instant; the ISO representation is also fixed, since it is written relative to coordinated universal time.

In a numeric context, the object converts to its stamp; this is the basis of date arithmetic.

Local Time and Universal Time

Reading methods come in two families. Those with UTC in their name give a value relative to universal time; those without give a value relative to the machine’s local timezone:

const t = new Date(1709287200000);
console.log("ISO        :", t.toISOString());
console.log("local year :", t.getFullYear(), "month:", t.getMonth(), "day:", t.getDate());
console.log("local hour :", t.getHours());
console.log("UTC hour   :", t.getUTCHours());
console.log("offset(min):", t.getTimezoneOffset());

With TZ=Europe/Istanbul:

ISO        : 2024-03-01T10:00:00.000Z
local year : 2024 month: 2 day: 1
local hour : 13
UTC hour   : 10
offset(min): -180

With TZ=UTC, the same script gives 10 on the local-hour line and 0 on the offset line; the other lines do not change.

Two traps meet here. First, month numbering starts at zero: March gave the value 2. Day and year, on the other hand, start at one. This inconsistency comes from the language’s earliest form and has been kept for backward compatibility.

Second, the sign of the timezone offset is reversed: a zone three hours ahead of universal time gives -180. The value is the number of minutes that has to be added to go from local time to universal time.

The Parsing Trap

Producing a date from text assumes a different timezone depending on the format:

const formats = [
  "2024-03-01T10:00:00Z",
  "2024-03-01T10:00:00",
  "2024-03-01",
  "2024-03-01T10:00:00+03:00",
];
for (const m of formats) {
  console.log(m.padEnd(26), new Date(m).toISOString());
}

With TZ=Europe/Istanbul:

2024-03-01T10:00:00Z       2024-03-01T10:00:00.000Z
2024-03-01T10:00:00        2024-03-01T07:00:00.000Z
2024-03-01                 2024-03-01T00:00:00.000Z
2024-03-01T10:00:00+03:00  2024-03-01T07:00:00.000Z

The four lines show four different rules.

In the first and fourth lines, the timezone is written into the text; the result is independent of the machine. In the second line, no timezone is written, and local time is assumed; the same text corresponds to a different instant on a different machine. In the third line, only a date is given, and universal time is assumed — the exact opposite of the second line.

The rule follows from this: every time text meant to be parsed should carry timezone information. If it does not, which rule applies depends on the format, and two formats behave as opposites.

Text that cannot be parsed does not throw; it produces an invalid date:

const t = new Date("broken");
console.log(String(t));
console.log(Number.isNaN(t.getTime()));
try {
  t.toISOString();
} catch (error) {
  console.log(error.name + ": " + error.message);
}
Invalid Date
true
RangeError: Invalid time value

Invalidity is detected by the stamp being NaN. Used without a check, the error only surfaces at the formatting stage — far from its source.

Date Arithmetic

Arithmetic is done through the timestamp; subtracting objects gives the stamp difference:

const start = new Date("2024-03-01T10:00:00Z");
const end = new Date("2024-03-01T10:00:00.800Z");
console.log(end.getTime() - start.getTime(), "ms");

const oneDayLater = new Date(start.getTime() + 24 * 60 * 60 * 1000);
console.log(oneDayLater.toISOString());
800 ms
2024-03-02T10:00:00.000Z

Adding a fixed number of milliseconds adds duration, not a calendar day. The two usually coincide; in zones with daylight-saving transitions, a day’s duration may not equal a calendar day. Calendar operations use setter methods:

const start = new Date("2024-03-01T10:00:00Z");
const copy = new Date(start.getTime());
copy.setUTCDate(copy.getUTCDate() + 1);
console.log(copy.toISOString(), start.toISOString());
2024-03-02T10:00:00.000Z 2024-03-01T10:00:00.000Z

Date objects are mutable: setter methods update the object in place. This is why a copy was taken first; without it, start would have changed too. The rule from the Objects lesson applies here too — a transformation does not change its input.

Formatting

Text meant to be shown to a user is produced by giving the locale and timezone explicitly:

const t = new Date("2024-03-01T10:00:00Z");
const format = new Intl.DateTimeFormat("en-US", {
  dateStyle: "short",
  timeStyle: "medium",
  timeZone: "Europe/Istanbul",
});
console.log(format.format(t));

const utcFormat = new Intl.DateTimeFormat("en-US", {
  dateStyle: "short",
  timeStyle: "medium",
  timeZone: "UTC",
});
console.log(utcFormat.format(t));
console.log(t.toISOString().slice(0, 19).replace("T", " "));
3/1/24, 1:00:00 PM
3/1/24, 10:00:00 AM
2024-03-01 10:00:00

The same instant was shown with two different times; both are correct. The difference is which timezone it is being looked at from.

The details of the text the formatter produces — separators, digit count, letter case — depend on locale data and can vary across runtimes. This is why formatted text is for display; it is not stored to be parsed or compared. The format to store is the ISO representation.

Together with JSON

Date objects convert to ISO text on serialization, but they do not come back as objects when read:

const record = { station: "A1", time: new Date("2024-03-01T10:00:00Z") };
const text = JSON.stringify(record);
console.log(text);
const back = JSON.parse(text);
console.log(typeof back.time, back.time);

const liveBack = JSON.parse(text, (key, value) =>
  key === "time" ? new Date(value) : value,
);
console.log(liveBack.time instanceof Date);
{"station":"A1","time":"2024-03-01T10:00:00.000Z"}
string 2024-03-01T10:00:00.000Z
true

The conversion is one-way asymmetric: writing happens on its own, reading requires a callback. The symmetry rule from the JSON lesson applies here too.

The Measurement Script’s Final Form

The course’s thread completes: a measurement list read from text, sorted by time, and formatted.

const incoming = [
  { station: "A1", time: "2024-03-01T10:00:00Z", temperature: 21.5 },
  { station: "A1", time: "2024-03-01T10:15:00Z", temperature: 22.75 },
  { station: "B1", time: "2024-03-01T10:05:00Z", temperature: 23 },
];

const format = new Intl.DateTimeFormat("en-US", {
  timeStyle: "short",
  timeZone: "Europe/Istanbul",
});

const records = incoming
  .map((r) => ({ ...r, time: new Date(r.time) }))
  .filter((r) => !Number.isNaN(r.time.getTime()))
  .sort((a, b) => a.time - b.time);

for (const r of records) {
  console.log(
    `${r.station.padEnd(4)} ${format.format(r.time).padStart(9)} ${r.temperature.toFixed(2).padStart(7)}`,
  );
}

const span = records[records.length - 1].time - records[0].time;
console.log("span covered (min):", span / 60000);
A1     1:00 PM   21.50
B1     1:05 PM   23.00
A1     1:15 PM   22.75
span covered (min): 15

Every step of the chain comes from one of the course’s lessons. New objects were produced with spread (Objects), invalid dates were filtered out (Truthy and Falsy Values, and this lesson), sorting was written with a comparison function (Arrays), the text was aligned with a template literal (Strings), and the span was computed with stamp arithmetic.

The sort comparison was written as a.time - b.time; since the subtraction operator converts date objects to their stamps, the result is a number. This is one of the rare places where the rule from the Type Conversion and Coercion lesson is put to useful work.

Summary

  • A date object carries a single piece of data: a timestamp independent of timezone. Representation, on the other hand, depends on timezone.
  • Month numbering starts at zero; day and year start at one. The sign of the timezone offset is reversed.
  • Time text with no timezone written is assumed local; text with only a date is assumed universal time; text meant to be parsed should carry timezone information.
  • An invalid date does not throw; its stamp becomes NaN, and it only produces an error at formatting time.
  • Arithmetic is done through the stamp; since setter methods mutate the object in place, a copy is taken first.
  • Formatted text is for display; storage and comparison are done with the ISO representation.

Course Wrap-Up

This course established JavaScript’s value model and basic structures. Four topics completed one another: how the language is executed and how names are bound; which values exist and how they are converted and compared; how control flow and functions are written; how the built-in data structures are used.

One single example ran through the whole course. The measurement list started as an array of four numbers; it turned into a data set read from text, validated, grouped, formatted, and written back as JSON. Every lesson added one capability to that list, and also showed the limit of the capability it added.

Three areas were deliberately left out. First, the prototype model behind objects: how properties are looked up, what class syntax corresponds to on that model, and the rules of this binding. Second, using closures to carry state. Third, asynchronous execution.

The first two are the subject of the Objects and Functions in JavaScript course. That course answers questions deferred several times in this one: where an array’s map method comes from, what an arrow function’s missing prototype property prevents, what this binds to in object methods, and how a function returned by another function remembers an outer variable. The value model this course established is the ground that discussion stands on.

The third comes later. In this course, data was always already in hand; had it come from a network request or a file read, waiting for the result to be ready would have been necessary, and the program would not have stopped in the meantime. The event loop, promises, and asynchronous waiting are the subject of a later course in the curriculum. The next course, Objects and Functions in JavaScript, opens up the structure behind values first.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close