JavaScript and TypeScript course Β· Module 5: Advanced JavaScript

Date Object and Time Manipulation

11 min read
In this lesson7

At the Jurassic Park command center, Ray Arnold monitors the dinosaur feeding schedules. "Time is a key element of safety," he explains. "I need to know exactly when each dinosaur was fed, when the next feeding is due, and how much time has passed since the last fence inspection." Time in JavaScript can surprise you, though: June has the number 5 here, and the same date written in two ways can point to different hours. Let's defuse these traps one by one.

Creating Date Objects

You create a Date object with the new Date() constructor. Without arguments you get the current moment, and with numbers a specific date made of a year, month and day:

1// Current date and time
2const now = new Date();
3console.log(now); // e.g. "Sat Jun 15 2024 14:30:00 GMT+0200"
4
5// From a specific date (note: months start at 0!)
6const jurassicOpening = new Date(1993, 5, 11); // June 11, 1993
7console.log(jurassicOpening);
8
9// From a full date and time
10const incident = new Date(1993, 5, 11, 20, 30, 0);
11// June 11, 1993, 20:30:00

Notice the five: months are counted from zero, so January is 0 and June is 5. Days of the month are counted normally, from 1. This is the most common Date trap.

You can also recreate a date from text or from a number of milliseconds, a timestamp:

1// From a string (ISO 8601)
2const dateFromString = new Date("2024-06-15T14:30:00");
3// A date without a time is treated as UTC!
4const dateOnly = new Date("2024-06-15");
5
6// From a timestamp (milliseconds since January 1, 1970)
7const fromTimestamp = new Date(1718451000000);
8const epoch = new Date(0); // January 1, 1970, 00:00:00 UTC
9
10// Timestamp of the current moment
11const timestamp = Date.now();
12console.log(timestamp); // e.g. 1718451000000

Text with a date and time but no time zone is read as local time, while a date alone is read as midnight UTC. In Central Europe dateOnly will therefore show 1 or 2 a.m., depending on the season. Date.now() returns the number of milliseconds since January 1, 1970, not a Date object.

Getters - Extracting Date Components

Once a date exists, the get... methods extract single components from it - the year, month, day or hour:

1const feedingTime = new Date("2024-06-15T08:30:00");
2
3// Extracting components
4console.log(feedingTime.getFullYear());    // 2024
5console.log(feedingTime.getMonth());       // 5 (June - months start at 0!)
6console.log(feedingTime.getDate());        // 15 (day of the month)
7console.log(feedingTime.getDay());         // 6 (Saturday - days start at 0, Sunday = 0)
8console.log(feedingTime.getHours());       // 8
9console.log(feedingTime.getMinutes());     // 30
10console.log(feedingTime.getSeconds());     // 0
11console.log(feedingTime.getMilliseconds()); // 0
12
13// Timestamp
14console.log(feedingTime.getTime()); // milliseconds since 1970
15
16// UTC versions
17console.log(feedingTime.getUTCHours()); // the hour in UTC

getDate() returns the day of the month, and getDay() the day of the week counted from Sunday as 0 - they are easy to confuse. The methods work in the computer's time zone, and the versions with UTC in the name in universal time.

Setters - Modifying Dates

The set... methods work the other way round and change the date in place, because a Date object is mutable:

1const nextFeeding = new Date();
2
3// Setting components
4nextFeeding.setFullYear(2024);
5nextFeeding.setMonth(6);          // July (months start at 0!)
6nextFeeding.setDate(20);
7nextFeeding.setHours(9);
8nextFeeding.setMinutes(0);
9nextFeeding.setSeconds(0);
10
11// Setting several at once
12nextFeeding.setFullYear(2024, 6, 20); // year, month, day
13
14console.log(nextFeeding);

setFullYear() accepts the year, month and day at once. Setters carry the overflow over by themselves: setDate(32) in June gives July 2, and adding hours with setHours() moves past midnight into the next day.

Formatting Dates

Built-in Methods

To display a date, the Date object has several ready-made methods. Some give a fixed format, and some depend on the language settings:

1const event = new Date("2024-06-15T14:30:00");
2
3// Different formats
4console.log(event.toString());
5// "Sat Jun 15 2024 14:30:00 GMT+0200 (Central European Summer Time)"
6
7console.log(event.toDateString());
8// "Sat Jun 15 2024"
9
10console.log(event.toTimeString());
11// "14:30:00 GMT+0200 (Central European Summer Time)"
12
13console.log(event.toISOString());
14// "2024-06-15T12:30:00.000Z" (UTC)
15
16console.log(event.toLocaleDateString());
17// "6/15/2024" (depends on the locale)
18
19console.log(event.toLocaleTimeString());
20// "2:30:00 PM" (depends on the locale)
21
22console.log(event.toLocaleString());
23// "6/15/2024, 2:30:00 PM"

toISOString() always gives UTC time, which is why 14:30 in a Central European summer becomes 12:30 with the letter Z. The results of the toLocale... methods depend on the browser's language - the comments show US English settings.

Intl.DateTimeFormat - Advanced Formatting

Full control over the format comes from Intl.DateTimeFormat. You pass a language and options, and the formatter takes care of the names of days and months:

1const date = new Date("2024-06-15T14:30:00");
2
3// US English format
4const usFormatter = new Intl.DateTimeFormat('en-US', {
5  weekday: 'long',
6  year: 'numeric',
7  month: 'long',
8  day: 'numeric'
9});
10console.log(usFormatter.format(date));
11// "Saturday, June 15, 2024"
12
13// Format with time
14const fullFormatter = new Intl.DateTimeFormat('en-US', {
15  dateStyle: 'full',
16  timeStyle: 'short'
17});
18console.log(fullFormatter.format(date));
19// "Saturday, June 15, 2024 at 2:30 PM"
20
21// Short format
22const shortFormatter = new Intl.DateTimeFormat('en-US', {
23  dateStyle: 'short'
24});
25console.log(shortFormatter.format(date));
26// "6/15/24"
27
28// Shorthand: toLocaleDateString() accepts the same options
29console.log(date.toLocaleDateString('en-US', { weekday: 'long' }));
30// "Saturday"

You create a formatter once and reuse it. dateStyle and timeStyle are ready-made option sets, and toLocaleDateString() accepts the same options for a one-off format.

Date Calculations

Difference Between Dates

Subtracting two dates gives a number of milliseconds, because JavaScript converts each date to its timestamp:

1const lastFeeding = new Date("2024-06-15T08:00:00");
2const now = new Date("2024-06-15T14:30:00");
3
4// Difference in milliseconds
5const diffMs = now - lastFeeding;
6console.log(diffMs); // 23400000
7
8// Conversion to different units
9const diffSeconds = diffMs / 1000;
10const diffMinutes = diffMs / (1000 * 60);
11const diffHours = diffMs / (1000 * 60 * 60);
12const diffDays = diffMs / (1000 * 60 * 60 * 24);
13
14console.log(`Last fed ${diffHours} hours ago`);
15// "Last fed 6.5 hours ago"
16
17// Comparisons: < and > work, === compares objects
18console.log(lastFeeding < now); // true
19console.log(new Date("2024-06-15T08:00:00") === lastFeeding); // false
20console.log(new Date("2024-06-15T08:00:00").getTime() === lastFeeding.getTime()); // true

Dividing by 1000, 60 and 60 turns milliseconds into hours. The < and > operators compare dates correctly, but === checks whether it is the same object - compare separate dates with getTime().

Adding and Subtracting Time

You add time by adding milliseconds to the timestamp or by moving a component with a setter. A helper function wraps both variants in one place:

1const baseDate = new Date("2024-06-15T08:00:00");
2
3// Add 3 hours
4const plus3Hours = new Date(baseDate.getTime() + 3 * 60 * 60 * 1000);
5console.log(plus3Hours.toLocaleTimeString()); // "11:00:00 AM"
6
7// Add 7 days
8const plus7Days = new Date(baseDate);
9plus7Days.setDate(plus7Days.getDate() + 7);
10console.log(plus7Days.toLocaleDateString()); // "6/22/2024"
11
12// Subtract 1 month
13const minus1Month = new Date(baseDate);
14minus1Month.setMonth(minus1Month.getMonth() - 1);
15console.log(minus1Month.toLocaleDateString()); // "5/15/2024"
16
17// Helper function for adding time
18function addTime(date, amount, unit) {
19  const result = new Date(date);
20
21  switch(unit) {
22    case 'seconds':
23      result.setSeconds(result.getSeconds() + amount);
24      break;
25    case 'minutes':
26      result.setMinutes(result.getMinutes() + amount);
27      break;
28    case 'hours':
29      result.setHours(result.getHours() + amount);
30      break;
31    case 'days':
32      result.setDate(result.getDate() + amount);
33      break;
34    case 'months':
35      result.setMonth(result.getMonth() + amount);
36      break;
37    case 'years':
38      result.setFullYear(result.getFullYear() + amount);
39      break;
40  }
41
42  return result;
43}
44
45console.log(addTime(baseDate, 2, 'hours').toLocaleTimeString()); // "10:00:00 AM"

Each example starts from a copy, new Date(baseDate), so baseDate stays untouched. For days and months I recommend setters, because milliseconds know nothing about the daylight saving change, when a day has 23 or 25 hours.

Practical Examples in Jurassic Park

1. Feeding Schedule System

The first scheduler keeps specific feeding times in a Map and calculates how much time is left until the meal:

1class FeedingScheduler {
2  constructor() {
3    this.schedule = new Map();
4  }
5
6  scheduleFeed(dinoId, feedingTime) {
7    this.schedule.set(dinoId, {
8      scheduledTime: new Date(feedingTime),
9      status: 'pending'
10    });
11  }
12
13  getNextFeeding(dinoId) {
14    const feeding = this.schedule.get(dinoId);
15    if (!feeding) return null;
16
17    const now = new Date();
18    const timeUntil = feeding.scheduledTime - now;
19
20    if (timeUntil < 0) {
21      return { status: 'overdue', overdue: this.formatDuration(-timeUntil) };
22    }
23
24    return {
25      status: 'scheduled',
26      timeUntil: this.formatDuration(timeUntil),
27      scheduledTime: feeding.scheduledTime.toLocaleString('en-US')
28    };
29  }
30
31  formatDuration(ms) {
32    const hours = Math.floor(ms / (1000 * 60 * 60));
33    const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
34
35    if (hours > 0) {
36      return `${hours}h ${minutes}min`;
37    }
38    return `${minutes}min`;
39  }
40}
41
42const scheduler = new FeedingScheduler();
43scheduler.scheduleFeed('TREX-001', '2024-06-15T09:00:00');
44console.log(scheduler.getNextFeeding('TREX-001'));

When the time has passed, the date difference is negative - hence the overdue status, which you will see for the date in the example. formatDuration() breaks milliseconds into hours and minutes with the remainder operator %.

2. A Schedule Every Few Hours

The second variant only knows the interval between meals. The next feeding time is the time of the last feeding plus the interval in milliseconds:

1class FeedingScheduler {
2  constructor() {
3    this.schedules = new Map();
4  }
5
6  addSchedule(dinoId, feedingIntervalHours) {
7    this.schedules.set(dinoId, {
8      lastFed: new Date(),
9      interval: feedingIntervalHours,
10      nextFeeding: new Date(Date.now() + feedingIntervalHours * 3600000)
11    });
12  }
13
14  getNextFeeding(dinoId) {
15    const schedule = this.schedules.get(dinoId);
16    if (!schedule) return null;
17    return schedule.nextFeeding;
18  }
19
20  isOverdue(dinoId) {
21    const schedule = this.schedules.get(dinoId);
22    if (!schedule) return false;
23    return new Date() > schedule.nextFeeding;
24  }
25
26  getOverdueDinosaurs() {
27    const overdue = [];
28    const now = new Date();
29
30    for (const [dinoId, schedule] of this.schedules) {
31      if (now > schedule.nextFeeding) {
32        const overdueHours = (now - schedule.nextFeeding) / 3600000;
33        overdue.push({
34          dinoId,
35          overdueBy: `${overdueHours.toFixed(1)} hours`,
36          nextFeeding: schedule.nextFeeding
37        });
38      }
39    }
40
41    // The dinosaur waiting longest goes first
42    return overdue.sort((a, b) => a.nextFeeding - b.nextFeeding);
43  }
44
45  recordFeeding(dinoId) {
46    const schedule = this.schedules.get(dinoId);
47    if (!schedule) return false;
48
49    const now = new Date();
50    schedule.lastFed = now;
51    schedule.nextFeeding = new Date(now.getTime() + schedule.interval * 3600000);
52
53    console.log(`${dinoId} fed at ${now.toLocaleTimeString()}. Next feeding: ${schedule.nextFeeding.toLocaleTimeString()}`);
54    return true;
55  }
56}

isOverdue() compares dates with a plain >, and getOverdueDinosaurs() walks the map with a for...of loop and sorts the result so that those waiting longest come first. recordFeeding() starts a new interval from the moment of feeding.

Each species gets its own feeding rhythm:

1// Usage
2const scheduler = new FeedingScheduler();
3scheduler.addSchedule("TREX-001", 8);   // T-Rex every 8 hours
4scheduler.addSchedule("RAPTOR-01", 6);  // Raptor every 6 hours
5scheduler.addSchedule("BRACHIOSAURUS", 4); // Herbivore every 4 hours
6
7const overdue = scheduler.getOverdueDinosaurs();
8if (overdue.length > 0) {
9  console.log("Overdue feedings:", overdue);
10}

Right after they are added nobody is overdue, so nothing is printed - delays appear once an interval passes without recordFeeding().

3. Relative Time Formatting

Visitors don't care about a timestamp, only about a message like "5 minutes ago". You can calculate it by hand or hand the job over to Intl.RelativeTimeFormat:

1function formatRelativeTime(date) {
2  const now = new Date();
3  const diff = now - new Date(date);
4
5  const seconds = Math.floor(diff / 1000);
6  const minutes = Math.floor(seconds / 60);
7  const hours = Math.floor(minutes / 60);
8  const days = Math.floor(hours / 24);
9
10  if (seconds < 60) return 'just now';
11  if (minutes < 60) return `${minutes} min ago`;
12  if (hours < 24) return `${hours} h ago`;
13  if (days < 7) return `${days} days ago`;
14
15  return new Date(date).toLocaleDateString('en-US');
16}
17
18// Or using Intl.RelativeTimeFormat
19function formatRelativeTimeIntl(date) {
20  const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
21  const diff = new Date(date) - new Date();
22
23  const seconds = Math.round(diff / 1000);
24  const minutes = Math.round(seconds / 60);
25  const hours = Math.round(minutes / 60);
26  const days = Math.round(hours / 24);
27
28  if (Math.abs(seconds) < 60) return rtf.format(seconds, 'second');
29  if (Math.abs(minutes) < 60) return rtf.format(minutes, 'minute');
30  if (Math.abs(hours) < 24) return rtf.format(hours, 'hour');
31  return rtf.format(days, 'day');
32}
33
34console.log(formatRelativeTime(new Date(Date.now() - 3600000))); // "1 h ago"
35
36// The formatter itself: numeric: 'auto' turns numbers into words
37const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
38console.log(rtf.format(-1, 'day'));  // "yesterday"
39console.log(rtf.format(-3, 'hour')); // "3 hours ago"
40console.log(rtf.format(2, 'day'));   // "in 2 days"
41console.log(rtf.format(1, 'week'));  // "next week"

The hand-written version knows only one language and rigid endings, while Intl.RelativeTimeFormat handles grammar by itself. The numeric: 'auto' option turns minus one day into "yesterday" and one week ahead into "next week".

Summary

Ray Arnold wraps up the training: "In critical systems like Jurassic Park, precise time management is a matter of life and death. Remember:"

  1. Months start at 0 - January is 0, December is 11
  2. Use ISO 8601 to store dates ("2024-06-15T14:30:00Z")
  3. Intl.DateTimeFormat for internationalization
  4. Avoid string manipulation - use Date methods
  5. Consider libraries (date-fns, dayjs) for complex operations

My advice: store dates in UTC and convert them to local time only when displaying them. A new API called Temporal is also on its way, designed as a full replacement for Date - before you reach for it, check its support on MDN. In the next lesson you will save data in localStorage, where dates come back as plain strings.

Remember: the park clock counts months from zero and time in milliseconds - keep an eye on both, and no dinosaur will miss a feeding.

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does Date.now() return in JavaScript?

  2. 2. What is the important gotcha when working with months in the Date object?

Hands-on tasks in the game

  • Code editor

    Create a feeding schedule that calculates the next feeding time and checks for delays.

Useful articles