Skip to content
elephantoo

Dates & Intl

Lesson 23 of 34 15 min read

Create and compare Dates, time zones, and format dates, numbers and currencies with Intl.


Dates are famously tricky: time zones, daylight saving, months that start at zero, and every country writing dates differently. This lesson covers JavaScript's Date object — how to create, read, compare and calculate with dates — and the Intl API, which formats dates, numbers, currencies and more for any language and region.

Some outputs below depend on your computer's time zone. Examples that show exact output pass an explicit timeZone so they're the same everywhere.

What a Date really is#

A Date stores a single number: milliseconds since 1 January 1970, 00:00 UTC (the "Unix epoch"). Everything else — the day, hour, time zone — is calculated from that number when you ask.

JavaScript
const now = new Date();
console.log(now.getTime());  // e.g. 1790908200000
console.log(Date.now());     // same idea, without creating an object

const epoch = new Date(0);
console.log(epoch.toISOString()); // 1970-01-01T00:00:00.000Z

Creating dates#

JavaScript
// From an ISO 8601 string – the safest string format
const launch = new Date("2026-09-30T10:00:00+05:30"); // explicit offset (IST)
console.log(launch.toISOString()); // 2026-09-30T04:30:00.000Z – stored as UTC

const utcMidnight = new Date("2026-09-30"); // date-only ISO strings are treated as UTC
console.log(utcMidnight.toISOString());    // 2026-09-30T00:00:00.000Z

// From parts – local time, months are 0-based!
const birthday = new Date(2026, 0, 15, 9, 30); // 15 Jan 2026, 09:30 local time

// In UTC from parts
const utc = new Date(Date.UTC(2026, 11, 25)); // 25 Dec 2026, 00:00 UTC
console.log(utc.toISOString()); // 2026-12-25T00:00:00.000Z

Avoid parsing other string formats like "30/09/2026" or "Sep 30 2026" — results vary between browsers. And beware: "2026-09-30" (date only) is UTC, but "2026-09-30T10:00" (no offset) is local time.

Invalid dates

JavaScript
const bad = new Date("not a date");
console.log(bad.getTime());           // NaN
console.log(Number.isNaN(bad.getTime())); // true – the way to check validity

Reading parts of a date#

JavaScript
const d = new Date(Date.UTC(2026, 8, 30, 4, 30)); // 30 Sep 2026 04:30 UTC

console.log(d.getUTCFullYear()); // 2026
console.log(d.getUTCMonth());    // 8  (September – zero-based!)
console.log(d.getUTCDate());     // 30 (day of month)
console.log(d.getUTCDay());      // 3  (day of week: 0 = Sunday … 6 = Saturday)
console.log(d.getUTCHours(), d.getUTCMinutes()); // 4 30

Each getUTC* method has a local-time twin (getFullYear, getMonth, getHours…) that uses the computer's time zone. Setters (setDate, setHours…) mutate the date in place.

Comparing and calculating#

Since dates are numbers underneath, comparisons and subtraction just work:

JavaScript
const start = new Date("2026-09-01T00:00:00Z");
const end = new Date("2026-09-30T00:00:00Z");

console.log(end > start);                      // true
console.log(end - start);                      // 2505600000 (ms)
const MS_PER_DAY = 24 * 60 * 60 * 1000;
console.log((end - start) / MS_PER_DAY);       // 29 days

// Equality: compare the numbers, not the objects
const a = new Date("2026-01-01T00:00:00Z");
const b = new Date("2026-01-01T00:00:00Z");
console.log(a === b);                     // false – different objects
console.log(a.getTime() === b.getTime()); // true

Adding time

JavaScript
function addDays(date, days) {
  const copy = new Date(date); // don't mutate the original
  copy.setUTCDate(copy.getUTCDate() + days);
  return copy;
}

const due = addDays(new Date("2026-09-25T00:00:00Z"), 10);
console.log(due.toISOString().slice(0, 10)); // 2026-10-05 – month rolls over automatically

const inOneHour = new Date(Date.now() + 60 * 60 * 1000);

setDate handles overflow (day 35 of September becomes 5 October). Adding days via setDate is safer than adding 24 * 60 * 60 * 1000 ms in local time, because a "day" around a daylight-saving change can be 23 or 25 hours.

Sorting dates

JavaScript
const events = [
  { name: "Launch", at: new Date("2026-10-01T00:00:00Z") },
  { name: "Beta", at: new Date("2026-08-15T00:00:00Z") },
];
const ordered = events.toSorted((x, y) => x.at - y.at);
console.log(ordered.map((e) => e.name)); // [ 'Beta', 'Launch' ]

Converting dates to strings#

JavaScript
const d = new Date("2026-09-30T04:30:00Z");

console.log(d.toISOString()); // 2026-09-30T04:30:00.000Z – for storage and APIs
console.log(JSON.stringify({ d })); // {"d":"2026-09-30T04:30:00.000Z"}

JSON.stringify turns dates into ISO strings, but JSON.parse does not turn them back — you get a string, and must call new Date(str) yourself.

For anything a human will read, use Intl (or toLocaleString, which uses Intl underneath).

Formatting dates with Intl.DateTimeFormat#

JavaScript
const d = new Date("2026-09-30T04:30:00Z");

const ist = { timeZone: "Asia/Kolkata" };
console.log(d.toLocaleDateString("en-IN", ist)); // 30/9/2026
console.log(d.toLocaleDateString("en-US", ist)); // 9/30/2026
console.log(d.toLocaleDateString("de-DE", ist)); // 30.9.2026

const long = new Intl.DateTimeFormat("en-IN", {
  dateStyle: "full",
  timeStyle: "short",
  timeZone: "Asia/Kolkata",
});
console.log(long.format(d)); // Wednesday, 30 September 2026 at 10:00 am

const custom = new Intl.DateTimeFormat("en-GB", {
  weekday: "short",
  day: "numeric",
  month: "short",
  hour: "2-digit",
  minute: "2-digit",
  timeZone: "America/New_York",
  timeZoneName: "short",
});
console.log(custom.format(d)); // Wed 30 Sept, 00:30 GMT-4
  • The first argument is a locale ("en-IN", "hi-IN", "fr-FR"), or undefined to use the user's own.
  • dateStyle/timeStyle ("full", "long", "medium", "short") give sensible presets.
  • timeZone accepts IANA names like "Asia/Kolkata", "Europe/London", "UTC".
  • Create a formatter once and reuse it — it's faster than calling toLocaleString repeatedly.

The exact punctuation can change slightly between browser and Node versions, so never parse formatted output — it's for display only.

Relative times

JavaScript
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
console.log(rtf.format(-1, "day"));   // yesterday
console.log(rtf.format(3, "hour"));   // in 3 hours
console.log(rtf.format(-2, "week"));  // 2 weeks ago
JavaScript
function timeAgo(date, now = new Date()) {
  const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
  const seconds = Math.round((date - now) / 1000);
  const units = [
    ["year", 31536000], ["month", 2592000], ["week", 604800],
    ["day", 86400], ["hour", 3600], ["minute", 60], ["second", 1],
  ];
  for (const [unit, size] of units) {
    if (Math.abs(seconds) >= size || unit === "second") {
      return rtf.format(Math.round(seconds / size), unit);
    }
  }
}

const now = new Date("2026-09-30T12:00:00Z");
console.log(timeAgo(new Date("2026-09-30T11:15:00Z"), now)); // 45 minutes ago
console.log(timeAgo(new Date("2026-09-27T12:00:00Z"), now)); // 3 days ago

Formatting numbers with Intl.NumberFormat#

JavaScript
const n = 1234567.891;

console.log(new Intl.NumberFormat("en-IN").format(n)); // 12,34,567.891
console.log(new Intl.NumberFormat("en-US").format(n)); // 1,234,567.891
console.log(new Intl.NumberFormat("de-DE").format(n)); // 1.234.567,891

const inr = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" });
console.log(inr.format(1234567)); // ₹12,34,567.00

const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
console.log(usd.format(19.5));    // $19.50

console.log(new Intl.NumberFormat("en", { style: "percent" }).format(0.256)); // 26%
console.log(new Intl.NumberFormat("en", { notation: "compact" }).format(2_500_000)); // 2.5M
console.log(new Intl.NumberFormat("en-IN", { notation: "compact" }).format(2_500_000)); // 25L
console.log(new Intl.NumberFormat("en", { style: "unit", unit: "kilometer-per-hour" }).format(80)); // 80 km/h
console.log(new Intl.NumberFormat("en", { maximumFractionDigits: 1 }).format(3.14159)); // 3.1

Prefer Intl.NumberFormat over toFixed for display: it handles separators, currency symbols and rounding for each locale.

Other handy Intl tools#

JavaScript
// Lists: "a, b and c"
const list = new Intl.ListFormat("en-GB", { style: "long", type: "conjunction" });
console.log(list.format(["HTML", "CSS", "JavaScript"])); // HTML, CSS and JavaScript

// Plural rules: choose the right word
const pr = new Intl.PluralRules("en");
const lessonWord = (n) => (pr.select(n) === "one" ? "lesson" : "lessons");
console.log(`1 ${lessonWord(1)}, 5 ${lessonWord(5)}`); // 1 lesson, 5 lessons

// Sorting with locale rules (accents, case)
const collator = new Intl.Collator("en", { sensitivity: "base" });
console.log(["zebra", "Émile", "apple"].toSorted(collator.compare)); // [ 'apple', 'Émile', 'zebra' ]

// The user's own settings
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone); // e.g. "Asia/Calcutta"

Time zone advice#

  • Store and transmit dates in UTC (ISO strings or timestamps).
  • Display in the user's time zone (the default) or a specific one via timeZone.
  • For "all-day" values like birthdays, store just the date string ("2026-01-15") rather than a moment in time, so it doesn't shift across time zones.
  • For heavy date logic (recurring events, calendars across zones), use a library such as date-fns/date-fns-tz, or the new built-in Temporal API, which is rolling out in browsers and fixes most of Date's design problems. Check support before relying on it.

Common mistakes#

  • Forgetting months are 0-based: new Date(2026, 9, 30) is October 30.
  • Parsing non-ISO strings like "30/09/2026".
  • Comparing dates with ===.
  • Mutating a date passed into a function (date.setDate(...)) — copy it first.
  • Building display strings by hand with getMonth() + 1 and padding instead of using Intl.

What's next#

Next, a tour of modern JavaScript features — optional chaining, nullish coalescing, logical assignment, at(), Object.groupBy and more.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What does new Date(2026, 0, 15) create?

  2. 2.Which is the best way to store or send a moment in time to a server?

  3. 3.What does new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" }).format(1234567) produce?

Finished reading?

Mark this lesson complete to track your progress.