Formats de dates : arrêtons de chercher midi à quatorze heures

Date, heure, fuseaux horaires... Quand on échange des informations de temps dans un système numérique, on est souvent amené à choisir un bon format pour représenter ces données. Et si le timestamp (nombre de secondes écoulées depuis un instant de référence) est populaire, il n'est pas toujours adapté à l'usage.

Cet article est une introduction à une norme encore trop méconnue par les équipes de développement : l'ISO-8601

Today is 02/05/2023.

Si vous avez hésité à la lecture de cette phrase, c'est normal. Les formats d'écriture de dates diffèrent selon les pays et les langues, et si les Français ou les Britanniques écrivent dans l'ordre "jour, mois, année", ce n'est pas le cas aux États-Unis d'Amérique où le format est "mois, jour, année", ce qui peut entrainer des confusions.
C'est pourquoi, à la fin des années 1980, l'ISO (International Organization for Standardization) publia la norme ISO-8601 afin de standardiser les échanges internationaux de dates. L'idée principale étant d'organiser les unités de temps de la plus grande à la plus petite tout en gardant une bonne lisibilité.

Today is 2023-02-05.

On peut désormais lire "Le 5 février 2023" sans ambigüité.

Cas général

La principale utilité de la norme est la représentation des dates et des heures de façon universelle (ou du moins mondiale) avec différents degrés de précision selon les besoins.

Dates

Les dates ISO vont du 0000-01-01 au 9999-12-31, couvrant ainsi toutes les dates utiles à nos projets. Il est important de noter que ces dates font toujours le même nombre de caractères et que, par conséquent, les 0 sont indispensables.
La longueur identique de ces dates permet de les trier par ordre alphabétique pour les trier par ordre chronologique : 1998-07-12, 2000-07-02, 2002-04-19, 2018-07-15

Dates et heures

On peut également encoder des heures en utilisant le format suivant : 1998-07-12T22:33:00 ; ce qui signifie "Le 12 juillet 1998 à 22h33". Ce format permet de représenter une heure dite locale sans considération de fuseau horaire.
De la même façon que pour les dates, l'ordre alphabétique correspond à l'ordre chronologique : 1998-07-12T21:26:00, 1998-07-12T21:45:00, 1998-07-12T22:33:00.
Attention ! L'heure est obligatoirement précédée d'un "T" (comme "Time").

Décalage horaire

Pour plus de précision, on peut ajouter un décalage horaire aux heures. Par exemple : 1998-07-12T22:33:00+02:00 signifie "Le 12 juillet 1998 à 22h33 UTC+2".
L'heure UTC peut être notée avec la lettre Z à la place du fuseau horaire : 1998-07-12T20:33:00Z (Z signifiant Zulu ou Zéro).
Cette notation en Z est une autre manière de représenter les timestamps.

Note : la norme ISO-8601 ne prend pas en compte les fuseaux horaires de type "Europe/Paris", seulement le décalage horaire. Il n'y a donc pas d'information sur le lieu dans cette norme.


Lors des décollages de fusées, les heures Zulu sont utilisées pour la mission elle-même et les heures locales pour les retransmissions télévisées.

Utilité de cette norme

La norme ISO-8601 est un bon compromis entre la facilité de lecture et la taille des données échangées auquel s'ajoute un atout supplémentaire : le choix du niveau de précision. On peut spécifier une date sans heure (1998-07-12), une heure sans date (T20:33:00), une date avec heure (1998-07-12T20:33:00), et ce choix est très important lors de la spécification des logiciels.
Voyons un exemple simple montrant l'utilité de cette norme.

Exemple utilisant un timestamp

Dans la suite, on utilisera un exemple simple d'application web permettant d'enregistrer une date via un appel HTTP transportant un JSON. Le frontend est écrit en JavaScript et le backend en Java. Le JSON contiendra un timestamp.

Note : le timestamp indique le nombre de millisecondes écoulées depuis le 1er janvier 1970 à minuit UTC

Date du jour

Si on cherche à enregistrer la date d'aujourd'hui, on peut facilement obtenir ce timestamp en JavaScript via la librairie Date (Nous sommes ici le 13 février 2024 à 11h, heure française) :

const request = {
    date: Date.now();
}

Le JSON correspondant sera :

{
  "date" : 1707818400000
}

Java peut ensuite lire cette date à partir du JSON.

Date date = objectMapper.readValue("1707818400000", Date.class);
// Tue Feb 13 11:00:00 CET 2024

On retombe bien sur la date du 13 février qu'on avait initialement.

Date sélectionnée

On améliore maintenant notre programme avec une sélection de la date. On peut obtenir le timestamp via la fonction Date.UTC. On utilise pour l'exemple le 23 juin 1972.

const request = {
    date: Date.UTC(1972, 5, 23);
}

Note : JavaScript numérote les mois de l'année de 0 à 11 dans sa librairie standard. Juin est donc le mois numéro 5.
Le JSON créé est le suivant.

{
  "date" : 78105600000
}

Java peut ensuite interpréter ce timestamp.

Date date = objectMapper.readValue("78105600000", Date.class);
// Fri Jun 23 01:00:00 CET 1972

La date est bien la date du 23 juin 1972 renseignée dans le frontend.

Changement d'heure

Que se passe-t-il si notre serveur ne se trouve plus à Paris mais à New-York ? Cette opération ne devrait pas poser de problème pour notre exemple et pourtant…

Date date = objectMapper.readValue("78105600000", Date.class);
// Thu Jun 22 20:00:00 EDT 1972

La date est désormais au 22 juin et non au 23 comme demandé. Notre enregistrement ne fonctionne plus à cause d'un simple changement d'heure. Pourquoi cela arrive-t-il ?

Un premier problème vient de l'interprétation du timestamp par Java : l'objet Date, contrairement à ce que son nom indique, ne contient pas une date, mais un timestamp qui sera ensuite interprété selon le fuseau horaire de la JVM. Mais Java aurait-il pu mieux faire ?

Il est 20h le 22 juin à New-York quand il est 1h le 23 juin à Paris
L'heure à Paris et à New-York le 23 juin 1972 à minuit UTC

Le problème du timestamp est qu'il représente un instant, un événement sur une frise chronologique or, comme on le voit sur l'illustration, il peut correspondre à deux dates différentes selon l'endroit où on se trouve. Java a fait avec ce qu'il avait sous la main : une information ambigüe. C'est donc le JSON de notre exemple qui est à l'origine du problème plus que l'interprétation par Java.

Exemple utilisant une date ISO

En utilisant un format plus adapté, on peut éliminer les erreurs d'interprétation du timestamp. C'est là que la norme ISO intervient : en spécifiant un format de date de la forme yyyy-mm-dd, on retire toute ambiguïté sur l'information transmise.
On cherche désormais à créer et à consommer un JSON ayant le format suivant :

{
  "date": "1991-03-21"
}

Note : JSON ne supportant pas la norme ISO-8601 directement, il faut embarquer les dates dans des chaînes de caractères.

Implémentation Java

En Java, la bibliothèque Java.time possède de nombreux objets permettant de convertir des dates et heures au format ISO. LocalDate est la classe appropriée pour le cas présent.

LocalDate date = objectMapper.readValue("\"1991-03-21\"", LocalDate.class);
// 1991-03-21

Implémentation JavaScript

JavaScript ne possède pas de manière native de consommer et d'écrire les dates en ISO. Heureusement, de nombreuses bibliothèques permettent de pallier cette absence. Voici un exemple avec js-joda.

const ld = LocalDate.of(1991, Month.MARCH, 21);
const request = {
    date: dt.toString() // "1991-03-21"
}

Faut-il jeter le timestamp aux oubliettes pour autant ?

Bien sûr que non. Chaque problème a ses outils adaptés et si les dates en ISO sont utiles pour les échanges, le timestamp a l'avantage d'avoir une taille très réduite (un timestamp fait 8 octets contre 24 pour un format ISO) ce qui facilite son stockage et sa nature de nombre entier facilite les comparaisons.

Pour aller plus loin

Des formats ISO plus exotiques

La norme permet l'écriture de beaucoup d'autres formats de données :

  • Des intervalles : 1998-07-12/2018-07-15 pour dire "Du 12 juillet 1998 au 15 juillet 2018"
  • Des jours sans années : --07-12 pour parler du 12 juillet sans préciser l'année
  • Des durées : P3Y6M4DT12H30M5S pour dire "Période (P) de 3 ans (Y), 6 Mois (M), 4 jours (D), 12 heures (H), 30 minutes (M) et 5 secondes (S)"
  • Des formats abrégés : 2000-06 pour parler du mois de juin 2000, 1998-06-10/07-12 pour dire "du 10 juin au 12 juillet 1998" sans préciser l'année une deuxième fois.

Les bibliothèques standard ne supportent que rarement ces formats exotiques, mais il est fréquent de trouver des bibliothèques tierces dédiées. Dans le pire des cas, il est facile de les implémenter soi-même.

Une implémentation Java poussée

La bibliothèque Java.time introduite avec Java 8 possède de nombreux formats équivalents aux représentations de la norme ISO. Les fonctions parse et toString, ainsi que les principales bibliothèques JSON permettant le passage de l'un à l'autre. Voici un résumé des classes Java correspondant aux formats ISO.

Classe Java Représentation ISO Note
Year 1998
YearMonth 1998-07
MonthDay --07-12
LocalDate 1998-07-12
LocalDateTime 1998-07-12T22:33:00
LocalTime 22:33:00
Instant 1998-07-12T20:33:00Z Est équivalent à un timestamp ou à un objet Date
OffsetDateTime 1998-07-12T22:33:00+02:00
Period P1Y2M3D Ne permet pas d'enregistrer des durées plus petites qu'un jour
Duration PT1H30M Ne convertit pas les heures en jours

Note : il est très simple de passer d'un format à un autre. Par exemple, la fonction YearMonth.from(localDate) permet d'extraire un mois d'une date choisie.

Que retenir ?

La norme ISO-8601 est une norme à connaître lors des phases de conception de logiciels puisqu'elle est très simple à lire pour un humain et très simple à interpréter pour un programme informatique. Et si elle n'est pas la seule façon de représenter des données de temps, elle est une bonne solution pour la spécification des interfaces.

Sources

iso.org (résumé sur Wikipédia)
Documentation Java.time

Images

https://commons.wikimedia.org/wiki/File:Hoelzelnaturalearth.png (License CC BY-SA 3.0 Deed)
https://pixabay.com/fr/photos/calendrier-mois-bois-gris-dé-3109374/
https://pixabay.com/fr/photos/planificateur-planifier-3485976/
https://pixabay.com/fr/photos/lancement-de-fusée-fusée-décoller-67643/

Relecture

Thomas Boutin
Alexandre Cailliaud
Dylan Le Hir
Matthieu Yvernault