Cours JavaScript et TypeScript · Module 5: JavaScript avancé
L'objet Date et le travail avec le temps
Dans cette leçon7
Au centre de commandement de Jurassic Park, Ray Arnold surveille les horaires des repas des dinosaures. "Le temps est un élément clé de la sécurité", explique-t-il. "Je dois savoir exactement quand chaque dinosaure a été nourri, quand aura lieu le prochain repas, et combien de temps s'est écoulé depuis la dernière inspection des clôtures." Le temps en JavaScript sait pourtant surprendre : juin porte ici le numéro 5, et une même date écrite de deux façons peut désigner des heures différentes. Désamorçons ces pièges un par un.
Créer des objets Date
Tu crées un objet Date avec le constructeur new Date(). Sans argument, tu obtiens l'instant présent, et avec des nombres, une date précise composée de l'année, du mois et du jour :
1// Date et heure actuelles
2const now = new Date();
3console.log(now); // p. ex. "Sat Jun 15 2024 14:30:00 GMT+0200"
4
5// Avec une date précise (attention : les mois commencent à 0 !)
6const jurassicOpening = new Date(1993, 5, 11); // 11 juin 1993
7console.log(jurassicOpening);
8
9// Avec la date et l'heure complètes
10const incident = new Date(1993, 5, 11, 20, 30, 0);
11// 11 juin 1993, 20:30:00Remarque le cinq : les mois se comptent à partir de zéro, donc janvier vaut 0 et juin 5. Les jours du mois, eux, se comptent normalement, à partir de 1. C'est le piège le plus fréquent de Date.
Tu peux aussi recréer une date à partir d'un texte ou d'un nombre de millisecondes, c'est-à-dire d'un horodatage (timestamp) :
1// À partir d'une chaîne (ISO 8601)
2const dateFromString = new Date("2024-06-15T14:30:00");
3// Une date sans heure est traitée comme de l'UTC !
4const dateOnly = new Date("2024-06-15");
5
6// À partir d'un timestamp (millisecondes depuis le 1er janvier 1970)
7const fromTimestamp = new Date(1718451000000);
8const epoch = new Date(0); // 1er janvier 1970, 00:00:00 UTC
9
10// Timestamp de l'instant présent
11const timestamp = Date.now();
12console.log(timestamp); // p. ex. 1718451000000Un texte avec une date et une heure, mais sans fuseau, est lu comme une heure locale, alors qu'une date seule est lue comme minuit UTC. En France, dateOnly affichera donc 1 h ou 2 h du matin, selon la saison. Date.now() renvoie le nombre de millisecondes écoulées depuis le 1er janvier 1970, et non un objet Date.
Lire les composants d'une date
Une fois la date créée, les méthodes get... en extraient les composants un par un - l'année, le mois, le jour ou l'heure :
1const feedingTime = new Date("2024-06-15T08:30:00");
2
3// Lecture des composants
4console.log(feedingTime.getFullYear()); // 2024
5console.log(feedingTime.getMonth()); // 5 (juin - les mois commencent à 0 !)
6console.log(feedingTime.getDate()); // 15 (jour du mois)
7console.log(feedingTime.getDay()); // 6 (samedi - les jours commencent à 0, dimanche = 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()); // millisecondes depuis 1970
15
16// Versions UTC
17console.log(feedingTime.getUTCHours()); // l'heure en UTCgetDate() renvoie le jour du mois, et getDay() le jour de la semaine compté à partir du dimanche, qui vaut 0 - il est facile de les confondre. Ces méthodes travaillent dans le fuseau horaire de l'ordinateur, et les versions avec UTC dans leur nom en temps universel.
Définir les composants d'une date
Les méthodes set... font l'inverse et modifient la date sur place, car l'objet Date est mutable :
1const nextFeeding = new Date();
2
3// Définition des composants
4nextFeeding.setFullYear(2024);
5nextFeeding.setMonth(6); // Juillet (les mois commencent à 0 !)
6nextFeeding.setDate(20);
7nextFeeding.setHours(9);
8nextFeeding.setMinutes(0);
9nextFeeding.setSeconds(0);
10
11// En définir plusieurs à la fois
12nextFeeding.setFullYear(2024, 6, 20); // année, mois, jour
13
14console.log(nextFeeding);setFullYear() accepte directement l'année, le mois et le jour. Les setters reportent eux-mêmes le surplus : setDate(32) en juin donnera le 2 juillet, et ajouter des heures avec setHours() fera passer minuit vers le jour suivant.
Formater une date
Les méthodes intégrées
Pour afficher une date, l'objet Date dispose de plusieurs méthodes toutes prêtes. Certaines donnent un format fixe, d'autres dépendent des réglages de langue :
1const event = new Date("2024-06-15T14:30:00");
2
3// Différents 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// "15/06/2024" (dépend de la locale)
18
19console.log(event.toLocaleTimeString());
20// "14:30:00" (dépend de la locale)
21
22console.log(event.toLocaleString());
23// "15/06/2024 14:30:00"toISOString() donne toujours l'heure UTC, c'est pourquoi 14:30 en été à Paris devient 12:30 suivi de la lettre Z. Les résultats des méthodes toLocale... dépendent de la langue du navigateur - les commentaires montrent des réglages français.
Intl.DateTimeFormat - le formatage avancé
Intl.DateTimeFormat donne un contrôle total sur le format. Tu indiques la langue et les options, et le formateur s'occupe lui-même des noms des jours et des mois :
1const date = new Date("2024-06-15T14:30:00");
2
3// Format français
4const frFormatter = new Intl.DateTimeFormat('fr-FR', {
5 weekday: 'long',
6 year: 'numeric',
7 month: 'long',
8 day: 'numeric'
9});
10console.log(frFormatter.format(date));
11// "samedi 15 juin 2024"
12
13// Format avec l'heure
14const fullFormatter = new Intl.DateTimeFormat('fr-FR', {
15 dateStyle: 'full',
16 timeStyle: 'short'
17});
18console.log(fullFormatter.format(date));
19// "samedi 15 juin 2024 à 14:30"
20
21// Format court
22const shortFormatter = new Intl.DateTimeFormat('fr-FR', {
23 dateStyle: 'short'
24});
25console.log(shortFormatter.format(date));
26// "15/06/2024"
27
28// Raccourci : toLocaleDateString() accepte les mêmes options
29console.log(date.toLocaleDateString('fr-FR', { weekday: 'long' }));
30// "samedi"Tu crées le formateur une fois et tu le réutilises autant que tu veux. dateStyle et timeStyle sont des ensembles d'options tout prêts, et toLocaleDateString() accepte les mêmes options pour un formatage ponctuel.
Les calculs sur les dates
La différence entre deux dates
Soustraire deux dates donne un nombre de millisecondes, car JavaScript convertit chaque date en son horodatage :
1const lastFeeding = new Date("2024-06-15T08:00:00");
2const now = new Date("2024-06-15T14:30:00");
3
4// Différence en millisecondes
5const diffMs = now - lastFeeding;
6console.log(diffMs); // 23400000
7
8// Conversion en différentes unités
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(`Dernier repas il y a ${diffHours} heures`);
15// "Dernier repas il y a 6.5 heures"
16
17// Comparaisons : < et > fonctionnent, === compare les objets
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()); // trueDiviser par 1000, 60 et 60 convertit les millisecondes en heures. Les opérateurs < et > comparent correctement les dates, mais === vérifie s'il s'agit du même objet - compare des dates distinctes avec getTime().
Ajouter et retirer du temps
Tu ajoutes du temps en ajoutant des millisecondes à l'horodatage, ou en décalant un composant avec un setter. Une fonction utilitaire réunit les deux variantes en un seul endroit :
1const baseDate = new Date("2024-06-15T08:00:00");
2
3// Ajouter 3 heures
4const plus3Hours = new Date(baseDate.getTime() + 3 * 60 * 60 * 1000);
5console.log(plus3Hours.toLocaleTimeString()); // "11:00:00"
6
7// Ajouter 7 jours
8const plus7Days = new Date(baseDate);
9plus7Days.setDate(plus7Days.getDate() + 7);
10console.log(plus7Days.toLocaleDateString()); // "22/06/2024"
11
12// Retirer 1 mois
13const minus1Month = new Date(baseDate);
14minus1Month.setMonth(minus1Month.getMonth() - 1);
15console.log(minus1Month.toLocaleDateString()); // "15/05/2024"
16
17// Fonction utilitaire pour ajouter du temps
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"Chaque exemple part d'une copie new Date(baseDate), donc baseDate reste intacte. Pour les jours et les mois, je te recommande les setters, car les millisecondes ne savent rien du changement d'heure, quand une journée compte 23 ou 25 heures.
Des exemples pratiques à Jurassic Park
1. Le système de planning des repas
Le premier planning garde dans une Map des heures de repas précises et calcule combien de temps il reste avant le repas :
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('fr-FR')
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'));Quand l'échéance est passée, la différence de dates est négative - d'où le statut overdue, que tu verras avec la date de l'exemple. formatDuration() décompose les millisecondes en heures et en minutes grâce à l'opérateur de reste %.
2. Un planning toutes les quelques heures
La seconde variante ne connaît que l'intervalle entre les repas. L'échéance suivante correspond à l'heure du dernier repas plus l'intervalle en millisecondes :
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)} h`,
36 nextFeeding: schedule.nextFeeding
37 });
38 }
39 }
40
41 // Le dinosaure qui attend depuis le plus longtemps en tête de liste
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} nourri à ${now.toLocaleTimeString()}. Prochain repas : ${schedule.nextFeeding.toLocaleTimeString()}`);
54 return true;
55 }
56}isOverdue() compare les dates avec un simple >, et getOverdueDinosaurs() parcourt la map avec une boucle for...of et trie le résultat en commençant par ceux qui attendent depuis le plus longtemps. recordFeeding() fait repartir le nouvel intervalle à partir du moment du repas.
Chaque espèce reçoit son propre rythme de repas :
1// Utilisation
2const scheduler = new FeedingScheduler();
3scheduler.addSchedule("TREX-001", 8); // T-Rex toutes les 8 heures
4scheduler.addSchedule("RAPTOR-01", 6); // Raptor toutes les 6 heures
5scheduler.addSchedule("BRACHIOSAURUS", 4); // Herbivore toutes les 4 heures
6
7const overdue = scheduler.getOverdueDinosaurs();
8if (overdue.length > 0) {
9 console.log("Repas en retard :", overdue);
10}Juste après l'ajout, personne n'est en retard, donc rien ne s'affiche - les retards apparaîtront quand un intervalle se sera écoulé sans recordFeeding().
3. Le formatage relatif du temps
Les visiteurs ne s'intéressent pas à l'horodatage, mais à un message du genre "il y a 5 minutes". On peut le calculer à la main, ou confier ce travail à 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 'à l’instant';
11 if (minutes < 60) return `il y a ${minutes} min`;
12 if (hours < 24) return `il y a ${hours} h`;
13 if (days < 7) return `il y a ${days} jours`;
14
15 return new Date(date).toLocaleDateString('fr-FR');
16}
17
18// Ou avec Intl.RelativeTimeFormat
19function formatRelativeTimeIntl(date) {
20 const rtf = new Intl.RelativeTimeFormat('fr', { 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))); // "il y a 1 h"
35
36// Le formateur seul : numeric: 'auto' transforme les nombres en mots
37const rtf = new Intl.RelativeTimeFormat('fr', { numeric: 'auto' });
38console.log(rtf.format(-1, 'day')); // "hier"
39console.log(rtf.format(-3, 'hour')); // "il y a 3 heures"
40console.log(rtf.format(2, 'day')); // "après-demain"
41console.log(rtf.format(1, 'week')); // "la semaine prochaine"La version manuelle ne connaît qu'une seule langue et des terminaisons figées, alors que Intl.RelativeTimeFormat accorde lui-même les mots. L'option numeric: 'auto' transforme moins un jour en "hier", et plus deux jours en "après-demain".
Résumé
Ray Arnold conclut la formation : "Dans des systèmes critiques comme Jurassic Park, une gestion précise du temps est une question de vie ou de mort. Souvenez-vous :"
- Les mois commencent à 0 - janvier vaut 0, décembre vaut 11
- Utilisez l'ISO 8601 pour stocker les dates ("2024-06-15T14:30:00Z")
- Intl.DateTimeFormat pour l'internationalisation
- Évitez de manipuler des chaînes - utilisez les méthodes de Date
- Envisagez des bibliothèques (date-fns, dayjs) pour les opérations complexes
Mon conseil : enregistre les dates en UTC, et ne les convertis en heure locale qu'au moment de l'affichage. Une nouvelle API, Temporal, est aussi en préparation, conçue comme le successeur complet de Date - avant de t'en servir, vérifie sa prise en charge sur MDN. Dans la prochaine leçon, tu enregistreras des données dans localStorage, d'où les dates reviennent sous forme de simples chaînes.
Retiens bien : l'horloge du parc compte les mois à partir de zéro, et le temps en millisecondes - surveille les deux, et aucun dinosaure ne manquera son repas.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que renvoie Date.now() en JavaScript ?
2. Quel est le piège important à connaître avec les mois dans l'objet Date ?
Tâches pratiques dans le jeu
- Éditeur de code
Crée un planning d'alimentation qui calcule l'heure du prochain repas et détecte les retards.