Le type Date en JS : LOLWUT?!
Par Delicious Insights • Publié le 26 nov. 2014

Ce brave constructeur natif Date est honteusement issu d’un portage quasi ligne à ligne du java.util.Date de Java 1.0, aux petites heures de la nuit un soir de mai 1995. Et il se trimballe exactement les mêmes bugs et aberrations.

Pour ne pas se faire avoir, ou juste pour le plaisir d’en rire, petit tour des subtilités à ne pas oublier.

A première vue tout va bien…

On peut créer une Date de tout un tas de façons :

// Sur le moment courant
new Date()

// Sur une *epoch* (distance depuis le 1er janvier 1970 à 0h GMT), en millisecondes
new Date(0)

// Sur des composantes numériques individuelles
new Date(2014, 10, 26, 14, 31, 56)

// Sur une chaîne RFC2822
new Date('Fri Nov 28 2014 14:31:56 GMT+0100 (CET)')

// Sur une chaîne ISO8601 / W3DTF (sans timezone, et ES5+)
new Date('2014-11-26T14:31:56')

Tout ça est dans le fuseau horaire (timezone) du navigateur (donc celui de l'OS, en général), à part le format RFC2822, qui peut préciser son fuseau. Si tu as des composantes UTC sous le coude, tu peux obtenir une epoch et l'utiliser pour construire un Date :

new Date(Date.UTC(2014, 10, 26, 13, 31, 56))

Pour rappel, l'UTC est désormais préféré au GMT, car il prend en compte les secondes bisextiles, et il est beaucoup plus précisément défini que GMT, qui remonte au XIXe siècle.

Pour récupérer les parties horaires, c'est tout tranquillou :

var d = new Date()
d.getHours() // => 14 (0-23)
d.getMinutes() // => 31 (0-59)
d.getSeconds() // => 56 (0-59)
d.getMilliseconds() // => 327

Tous ces accesseurs peuvent aussi être appelés avec UTC après le get, pour obtenir la valeur UTC de la composante (par exemple, getUTCHours() donnera ici 13).

On a aussi la conversion d'une Date vers sa valeur numérique, son epoch :

d.getTime() // => 1417008716327.  Super mal nommé…

Il s'agit d'ailleurs du protocole de conversion numérique officiel pour Date (la conversion d'un Date en Number, par exemple +d ou Number(d), appelle en fait getTime()).

Il est aussi possible de récupérer directement l'epoch actuelle, sans créer un objet Date, depuis ES5 :

Date.now()

Avant ES5, il fallait passer par un objet, en faisant new Date().getTime() par exemple.

Enfin, on a des conversions String prédéfinies, certaines seulement depuis ES5 :

d.toString() // => 'Fri Nov 28 2014 14:48:10 GMT+0100 (CET)' : RFC2822, date + heure
d.toDateString() // => 'Fri Nov 28 2014' : RFC2822, date
d.toTimeString() // => '14:48:10 GMT+0100 (CET)' : RFC2822, heure
d.toISOString() // => '2014-11-28T13:48:10.467Z' : ISO8601

On peut aussi demander des formats plus adaptés à la langue active, en rajoutant Locale après le to. Par exemple, pour la totale en français :

d.toLocaleString() // => '28/11/2014 14:48:10'

Mais attention ça glisse

d.getYear() // => 114.  But of course!
d.getMonth() // => 10.  WTF?!
d.getDay() // => 5.  Gni?!
d.getDate() // => 28.  Ah ben oui je m'en doutais

d.toUTCString() // => 'Fri, 28 Nov 2014 13:56:10 GMT'.  Alors, UTC ou GMT ?!

Et là tu te dis…

Mais c’est quoi ce bordel !?!

Je sais. Je sais. Un bon gros bordel.

LOLWUT?

Eh oui, tout ça peut être tracé jusqu'à la libc, notamment time.h, pour l'histoire des mois qui démarrent à zéro :

External declarations, as well as the tm structure definition, are contained in the
<time.h> include file.  The tm structure includes at least the following fields:

    int tm_sec;     /* seconds (0 - 60) */
    int tm_min;     /* minutes (0 - 59) */
    int tm_hour;    /* hours (0 - 23) */
    int tm_mday;    /* day of month (1 - 31) */
    int tm_mon;     /* month of year (0 - 11) */
    int tm_year;    /* year - 1900 */
    int tm_wday;    /* day of week (Sunday = 0) */

Le JDK n'ayant pas pris la peine de compenser ces conneries (bien qu'il ait choisi de ne pas retranscrire la seconde intercalaire à 60), on retrouve les mêmes blagues.

Je vous vois déjà venir, vous qui me ressortirez l'argument éculé « si mais partir de zéro c'est pratique pour les pointeurs et les tableaux ! », argument aussi ancien que spécieux, puisque le choix du zéro était juste pratique pour une certaine architecture matérielle et n'est pas sans défauts, il fait d'ailleurs toujours débat, comme par exemple dans cet article.

La vérité, c'est qu'aucun système humain au monde ne numérote janvier à zéro. Personne. Nulle part. Ça fait juste chier tout le monde, ce mois à zéro, c'est tout là haut dans le Panthéon des Décisions Techniques Pourries™, juste au même niveau que ces crétins d'itérateurs jQuery qui collent l'index avant la valeur dans leurs arguments de rappel.

D'autant que les mois qui démarrent à zéro alors que les jours non, c'est parfaitement incohérent.

Notez également la merveilleuse définition de l'année « moins 1900 », le truc hyper résistant dans l'avenir (future-proof) une fois qu'on atteint l'an 2000. Soit les mecs avaient une foi immodérée dans la vitesse du progrès info et l'obsolescence du code (ha ! dites ça aux COBOListes en 2014 !), soit ils croyaient que Terminator c'était vrai et qu'on allait tous crever avant, je sais pas. En tous les cas pour avoir la vraie année, il faut getFullYear() (qui devrait plutôt s'appeller getUnfuckedYear(), mais c'est plus long).

Et puis year % 100 c'était trop hacker pour eux, en fait, pour obtenir l'année sur 2 chiffres de façon fiable.

Quant aux accesseurs écrivains (setters, genre setHours(…)), il acceptent juste n'importe quoi et font du rollover sur les unités supérieures, pas très fiable d'ailleurs. Ça touche d'ailleurs toute forme de création de Date, que ce soit new Date ou Date.UTC :

new Date(2014, -2163, 4217) // => Thu Apr 17 1845 00:00:00 GMT+0200 (CEST)

Paye ton #facepalm. Le JDK a d'ailleurs déprécié les accesseurs écrivains depuis 15 ans, je serais vous, j'éviterais de m'en servir.

Là où tu peux pas pardonner au JDK, c'est d'avoir décrété que day était le wday, le jour de la semaine, plutôt que le jour du mois. Même si les Britanniques réfèrent parfois au jour du mois sous le nom date (d'où ici le getDate() pour le jour du mois, super logique quand tu l'appelles sur un type Date), tous les anglophones sur cette planète se réfèrent aux composantes de la date sous les noms day, month, year.

Encore un point pour les obfuscateurs. C'est vrai, une API qui passe sa vie à tendre des pièges, ça crée de l'emploi. Demandez donc à PHP, surtout dans ce pays.

Pour être zen je fais quoi ?

Les trois trucs à se rappeler absolument :

  • Les mois démarrent à zéro (argh !)
  • Le jour du mois c'est getDate() (re-argh !)
  • La vraie année c'est getFullYear() (respire)

Si tu as très peu de manips basées Date à faire, et des simples, souviens-t-en et ça ira bien.

En revanche, si tu dois faire quoi que ce soit de plus avancé, comme des calculs (ajouts, différences, décalages, conversions, analyse ou production textuelle, formatage localisé…), tu arrêtes de jouer au cow-boy et tu utilises Moment.js.

Cette merveilleuse petite lib, qui est vraiment incontournable, dans le navigateur comme côté Node, pour tripatouiller de la date dans la joie, sait absolument tout faire, et dans tout un tas de langues.

Je t'invite à aller sur son site et jouer avec les exemples (notamment changer dynamiquement la langue), tu verras, c'est que du bonheur.

Et si tu as besoin de faire des décalages de date (genre « dans 3 mois » ou « 2 mois avant cette date »), je te conseille d'y ajouter la micro-lib Moments-Away, qui donne du code si mignon qu'on en a des frissons dans le dos.

Comme toujours, lire la doc sauve la vie !