Les dates et heures en Rust avec chrono
Les programmes ont sans cesse affaire au temps. « Combien de temps ça a pris ? » « Quand la facture doit-elle être payée ? » « Combien de jours reste-t-il avant le voyage ? » Rust range ces questions en deux familles.
Le module std::time de la bibliothèque standard est un chronomètre : il mesure le temps qui passe. Il ne connaît ni les calendriers, ni les mois, ni les jours de la semaine. Pour tout ce qui touche au calendrier, presque tous les projets Rust utilisent le crate chrono. Cette page commence par le chronomètre, puis passe au calendrier.
Mesurer le temps écoulé avec Instant
Section intitulée « Mesurer le temps écoulé avec Instant »Instant::now() lit l’horloge de l’ordinateur. Plus tard, .elapsed() vous dit combien de temps s’est écoulé depuis cette lecture, sous forme de Duration :
use std::time::{Duration, Instant};
fn main() { let start = Instant::now();
let mut total: u64 = 0; for i in 0..1_000_000 { total += i; }
let elapsed = start.elapsed(); println!("total = {total}"); println!("durée : {:?}", elapsed); println!("durée : {} ms", elapsed.as_millis()); println!("moins de 5 secondes ? {}", elapsed < Duration::from_secs(5));}Nous ne montrons pas la sortie ici, et c’est voulu : le temps mesuré change d’un ordinateur à l’autre et à chaque exécution. Vous verrez quelque chose comme durée : 6.862793ms. Cliquez deux fois sur le bouton et comparez.
Un Instant ne sert qu’à mesurer. Vous ne pouvez pas l’afficher comme une date, et il ne fait qu’avancer. C’est exactement ce qu’il faut pour chronométrer du code : il n’est jamais perturbé si quelqu’un change l’heure du système.
Les durées
Section intitulée « Les durées »Une Duration représente une durée, comme « 90 secondes ». Vous en créez une à partir de secondes ou de millisecondes, vous pouvez les additionner et les comparer :
use std::time::Duration;
fn main() { let short = Duration::from_millis(1500); let long = Duration::from_secs(90);
println!("{:?}", short); println!("{:?}", long); println!("{} ms", short.as_millis()); println!("{:.1} s", short.as_secs_f64());
let total = short + long; println!("total : {:?}", total); println!("{} min {} s", long.as_secs() / 60, long.as_secs() % 60); println!("{}", short < long);}Sortie :
1.5s90s1500 ms1.5 stotal : 91.5s1 min 30 strueRemarquez que Rust affiche les nombres décimaux avec un point (1.5s), pas avec une virgule. Vous croiserez Duration un peu partout : std::thread::sleep(Duration::from_millis(100)) met un thread en pause, et les délais d’attente (timeouts) des bibliothèques réseau prennent eux aussi une Duration.
L’heure de l’horloge avec SystemTime
Section intitulée « L’heure de l’horloge avec SystemTime »SystemTime, c’est l’heure de l’horloge de l’ordinateur, celle que vous voyez dans un coin de votre écran. L’usage le plus courant consiste à demander combien de temps s’est écoulé depuis l’epoch Unix, le 1er janvier 1970 (UTC). Ce nombre s’appelle un timestamp Unix (horodatage) :
use std::time::{SystemTime, UNIX_EPOCH};
fn main() { let now = SystemTime::now(); let since_1970 = now .duration_since(UNIX_EPOCH) .expect("l'horloge est réglée avant 1970"); println!("Secondes depuis le 1er janvier 1970 : {}", since_1970.as_secs());}Le nombre dépend du moment où vous lancez le programme, donc là encore, pas de sortie affichée. Pourquoi duration_since renvoie-t-il un Result ? Parce que l’horloge du système peut être fausse, ou être reculée. C’est aussi pour ça qu’il faut utiliser Instant, et pas SystemTime, pour chronométrer votre code.
C’est à peu près tout ce que fait std::time. Il n’a aucune idée de ce que veulent dire « mars » ou « vendredi ». Pour ça, il vous faut chrono.
Ajouter chrono
Section intitulée « Ajouter chrono »Ajoutez le crate à votre projet (voir Cargo et les crates si c’est nouveau pour vous) :
cargo add chrono[dependencies]chrono = "0.4"Le Rust Playground dispose aussi de chrono, donc tous les boutons ci-dessous fonctionnent. chrono a deux familles de types :
- Les types naïfs (
NaiveDate,NaiveTime,NaiveDateTime) sont des dates et des heures sans fuseau horaire. « 14 mars 2025 » ou « 09:30 », comme on les écrit sur un agenda. La plupart du code de tous les jours utilise ceux-là. DateTime<Tz>est un instant précis avec un fuseau horaire, commeDateTime<Utc>ouDateTime<Local>.
Créer des dates à partir de valeurs fixes
Section intitulée « Créer des dates à partir de valeurs fixes »NaiveDate::from_ymd_opt(année, mois, jour) construit une date. Elle renvoie une Option, car toutes les combinaisons ne sont pas de vraies dates :
use chrono::{Datelike, NaiveDate, NaiveTime};
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap(); println!("{date}"); println!("année {}, mois {}, jour {}", date.year(), date.month(), date.day());
let time = NaiveTime::from_hms_opt(9, 30, 0).unwrap(); let meeting = date.and_time(time); println!("{meeting}");
let other = date.and_hms_opt(17, 45, 10).unwrap(); println!("{other}");
let impossible = NaiveDate::from_ymd_opt(2025, 2, 30); println!("{:?}", impossible);}Sortie :
2025-03-14année 2025, mois 3, jour 142025-03-14 09:30:002025-03-14 17:45:10NoneLe 30 février n’existe pas : vous obtenez donc None au lieu d’une date cassée. Avec des valeurs fixes que vous avez tapées vous-même, .unwrap() ne pose pas de problème. Avec des valeurs qui viennent d’un utilisateur, gérez le cas None.
La ligne use chrono::Datelike apporte les méthodes .year(), .month() et .day(). (Timelike fait la même chose pour .hour() et .minute().)
La date et l’heure actuelles
Section intitulée « La date et l’heure actuelles »Utc::now() donne l’instant présent en UTC. Local::now() le donne dans le fuseau horaire de l’ordinateur :
use chrono::{Local, Utc};
fn main() { let utc_now = Utc::now(); let local_now = Local::now();
println!("UTC : {}", utc_now); println!("Locale : {}", local_now); println!("Aujourd'hui : {}", local_now.format("%Y-%m-%d"));
let today = local_now.date_naive(); println!("En NaiveDate : {today}");}La sortie, c’est la date du jour, donc elle change tous les jours. Les serveurs du Playground sont réglés sur UTC : là-bas, les deux lignes afficheront la même heure. .date_naive() laisse tomber le fuseau horaire et l’heure, et il reste une simple NaiveDate. Une bonne habitude : lisez « maintenant » une seule fois, à l’entrée de votre programme, puis passez la date à vos fonctions. Elles restent ainsi faciles à tester, comme vous le verrez dans l’exemple de l’échéance.
Formater des dates
Section intitulée « Formater des dates ».format() prend un motif. Chaque code % est remplacé par une partie de la date. %Y est l’année, %m le numéro du mois, %d le jour :
use chrono::NaiveDate;
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap(); let dt = date.and_hms_opt(9, 5, 0).unwrap();
println!("{}", date.format("%Y-%m-%d")); println!("{}", date.format("%d/%m/%Y")); println!("{}", date.format("%A, %B %-d, %Y")); println!("{}", date.format("%a %d %b")); println!("{}", dt.format("%H:%M")); println!("{}", dt.format("%-I:%M %p"));
let file_name = date.format("rapport-%Y%m%d.txt").to_string(); println!("{file_name}");}Sortie :
2025-03-1414/03/2025Friday, March 14, 2025Fri 14 Mar09:059:05 AMrapport-20250314.txtLes codes que vous utiliserez le plus : %Y l’année, %m le mois (01–12), %d le jour (01–31), %H l’heure (00–23), %M les minutes, %S les secondes, %A le nom du jour, %B le nom du mois. Un - juste après le % supprime le zéro initial : %-d affiche 5 au lieu de 05. .format() renvoie quelque chose que vous pouvez afficher. Appelez .to_string() quand vous avez besoin d’une String.
Vous avez sans doute remarqué « Friday » et « March » : par défaut, chrono écrit les noms des jours et des mois en anglais. Pour le format français habituel en chiffres, %d/%m/%Y suffit (14/03/2025). Si vous avez besoin de noms en français comme « vendredi », chrono propose une feature optionnelle, unstable-locales, avec la méthode format_localized. Et pour les dates lues par une machine (fichiers, API), les formats numériques comme %Y-%m-%d restent de toute façon le meilleur choix.
Transformer du texte en date
Section intitulée « Transformer du texte en date »NaiveDate::parse_from_str fait l’inverse : elle lit du texte avec le même motif %. Elle renvoie un Result, car le texte peut ne pas correspondre :
use chrono::{NaiveDate, NaiveDateTime};
fn main() { let a = NaiveDate::parse_from_str("2025-03-14", "%Y-%m-%d"); println!("{:?}", a);
let b = NaiveDate::parse_from_str("14/03/2025", "%d/%m/%Y"); println!("{:?}", b);
let c: Result<NaiveDate, _> = "2025-03-14".parse(); println!("{:?}", c);
let d = NaiveDateTime::parse_from_str("2025-03-14 09:30", "%Y-%m-%d %H:%M"); println!("{:?}", d);
for text in ["2025-13-01", "14 mars", "2025-03"] { match NaiveDate::parse_from_str(text, "%Y-%m-%d") { Ok(date) => println!("Date lue : {date}"), Err(e) => println!("Impossible de lire {text:?} : {e}"), } }}Sortie :
Ok(2025-03-14)Ok(2025-03-14)Ok(2025-03-14)Ok(2025-03-14T09:30:00)Impossible de lire "2025-13-01" : input is out of rangeImpossible de lire "14 mars" : input contains invalid charactersImpossible de lire "2025-03" : premature end of inputPour le format standard AAAA-MM-JJ, vous pouvez aussi appeler simplement .parse(). Le mois 13 n’existe pas, donc le premier mauvais exemple donne « input is out of range ». Les messages d’erreur viennent de chrono et sont en anglais : « hors de la plage autorisée », « contient des caractères non valides » et « fin prématurée du texte ». Ils sont courts, mais assez clairs pour être montrés à un utilisateur.
Calculer avec des dates
Section intitulée « Calculer avec des dates »Ajouter des jours à une date à la main est étonnamment difficile : les mois n’ont pas tous la même longueur, et il y a les années bissextiles. chrono s’en charge pour vous. Utilisez checked_add_days avec Days, checked_add_months avec Months, ou ajoutez un TimeDelta :
use chrono::{Days, Months, NaiveDate, TimeDelta};
fn main() { let start = NaiveDate::from_ymd_opt(2025, 1, 31).unwrap();
let plus_30 = start.checked_add_days(Days::new(30)).unwrap(); println!("30 jours plus tard : {plus_30}");
let minus_7 = start.checked_sub_days(Days::new(7)).unwrap(); println!("7 jours plus tôt : {minus_7}");
let next_month = start.checked_add_months(Months::new(1)).unwrap(); println!("1 mois plus tard : {next_month}");
let week_later = start + TimeDelta::weeks(1); println!("1 semaine plus tard : {week_later}");
let dt = start.and_hms_opt(23, 30, 0).unwrap(); let later = dt + TimeDelta::hours(2); println!("2 heures plus tard : {later}");}Sortie :
30 jours plus tard : 2025-03-027 jours plus tôt : 2025-01-241 mois plus tard : 2025-02-281 semaine plus tard : 2025-02-072 heures plus tard : 2025-02-01 01:30:00Regardez « 1 mois plus tard » : le 31 janvier plus un mois donne le 28 février, puisqu’il n’y a pas de 31 février. chrono choisit le dernier jour valide. Les méthodes checked_ renvoient une Option, qui ne vaut None que si le résultat sort de la plage d’années (très large) de chrono. Et ajouter 2 heures à 23:30 passe correctement au jour suivant.
Jours de la semaine et écart entre deux dates
Section intitulée « Jours de la semaine et écart entre deux dates ».weekday() (qui vient de Datelike) vous donne le jour de la semaine. Soustraire une date d’une autre donne un TimeDelta, et .num_days() le transforme en nombre :
use chrono::{Datelike, NaiveDate, Weekday};
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap(); let day = date.weekday(); println!("{date} est un {day} ({})", date.format("%A"));
let weekend = matches!(day, Weekday::Sat | Weekday::Sun); println!("week-end ? {weekend}");
let start = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(); let end = NaiveDate::from_ymd_opt(2025, 12, 25).unwrap(); let gap = end - start; println!("{} jours, soit {} semaines complètes", gap.num_days(), gap.num_weeks()); println!("à l'envers : {} jours", (start - end).num_days());
let check_in = start.and_hms_opt(14, 0, 0).unwrap(); let check_out = NaiveDate::from_ymd_opt(2025, 1, 4).unwrap().and_hms_opt(11, 0, 0).unwrap(); println!("séjour à l'hôtel : {} heures", (check_out - check_in).num_hours());}Sortie :
2025-03-15 est un Sat (Saturday)week-end ? true358 jours, soit 51 semaines complètesà l'envers : -358 joursséjour à l'hôtel : 69 heuresWeekday est un enum dont les variantes vont de Mon à Sun (du lundi au dimanche, en anglais) : vous pouvez donc faire un match dessus. Remarquez que l’écart peut être négatif si la deuxième date est plus tardive. C’est pratique : un nombre de jours restants négatif veut dire « en retard ».
Exemple : les jours avant une échéance
Section intitulée « Exemple : les jours avant une échéance »Assemblons tout ça. La fonction reçoit « aujourd’hui » en paramètre au lieu d’appeler Local::now() à l’intérieur. Comme ça, la sortie ci-dessous est toujours la même, et la fonction est facile à tester. Dans un vrai programme, vous lui passeriez Local::now().date_naive().
use chrono::NaiveDate;
fn days_left(today: NaiveDate, deadline: NaiveDate) -> String { let days = (deadline - today).num_days(); match days { 0 => String::from("pour aujourd'hui"), 1 => String::from("pour demain"), d if d > 1 => format!("encore {d} jours"), -1 => String::from("1 jour de retard"), d => format!("{} jours de retard", -d), }}
fn main() { let today = NaiveDate::from_ymd_opt(2025, 6, 10).unwrap();
let tasks = [ ("Payer le loyer", "2025-06-10"), ("Envoyer le rapport", "2025-06-11"), ("Refaire passeport", "2025-07-01"), ("Rendre un livre", "2025-06-09"), ("Payer le dentiste", "2025-06-03"), ];
println!("Aujourd'hui : {}", today.format("%A, %-d %B %Y")); for (name, date_text) in tasks { let deadline = NaiveDate::parse_from_str(date_text, "%Y-%m-%d").unwrap(); println!("{:<20} {} {}", name, deadline.format("%d %b"), days_left(today, deadline)); }}Sortie :
Aujourd'hui : Tuesday, 10 June 2025Payer le loyer 10 Jun pour aujourd'huiEnvoyer le rapport 11 Jun pour demainRefaire passeport 01 Jul encore 21 joursRendre un livre 09 Jun 1 jour de retardPayer le dentiste 03 Jun 7 jours de retardEssayez de remplacer today par une autre date et relancez. Le {:<20} complète le nom avec des espaces jusqu’à 20 caractères pour que les colonnes soient alignées (voir Afficher du texte). Les noms des tâches sont volontairement sans accents : ainsi, les colonnes restent alignées dans n’importe quel terminal. Et comme plus haut, les noms des jours et des mois (« Tuesday », « Jun ») sont ceux que chrono écrit par défaut, en anglais.
chrono n’est pas le seul choix. Le crate time est une alternative plus légère et populaire, et le crate plus récent jiff soigne particulièrement les fuseaux horaires et l’heure d’été. Nous utilisons chrono ici parce que c’est le plus répandu et qu’il est disponible dans le Playground.
Erreurs courantes
Section intitulée « Erreurs courantes »Oublier use chrono::Datelike. Sans cette ligne, .year(), .month() et .weekday() ne fonctionnent pas. L’erreur dit que la méthode est « private » (privée), ce qui est déroutant, mais la ligne help vous donne la vraie solution : le trait Datelike n’est pas dans la portée, il faut donc l’importer.
error[E0624]: method `year` is private --> src/main.rs:5:25 |5 | println!("{}", date.year()); | ^^^^ private method | = help: items from traits can only be used if the trait is in scopehelp: trait `Datelike` which provides `year` is implemented but not in scope; perhaps you want to import it |1 + use chrono::Datelike; |Confondre %m et %M. %m est le mois, %M les minutes. Avec une date et une heure, le mauvais code ne provoque aucune erreur. Il affiche les minutes sans rien dire :
use chrono::NaiveDate;
fn main() { let meeting = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap().and_hms_opt(9, 5, 0).unwrap(); println!("{}", meeting.format("%Y-%M-%d")); // faux : les minutes println!("{}", meeting.format("%Y-%m-%d")); // juste : le mois}Sortie :
2025-05-142025-03-14Chronométrer du code avec SystemTime. L’horloge du système peut faire des sauts (par exemple quand elle se synchronise avec Internet). Utilisez Instant pour mesurer, et SystemTime ou chrono seulement quand vous avez besoin de la vraie date.
Appeler Local::now() au fond de votre logique. Les résultats changent alors tous les jours, et les tests deviennent difficiles à écrire. Passez plutôt la date en paramètre, comme le fait days_left.
Récapitulatif
Section intitulée « Récapitulatif »std::time::Instantmesure le temps écoulé ;Durationreprésente une durée ;SystemTimeest l’heure de l’horloge (pratique pour les timestamps Unix).- Pour le calendrier, ajoutez
chrono. UtilisezNaiveDate/NaiveDateTimepour des dates sans fuseau horaire, etUtc::now()/Local::now()pour l’instant présent. from_ymd_optrenvoie uneOptionetparse_from_strunResult, car toutes les entrées ne sont pas de vraies dates..format("%Y-%m-%d")transforme une date en texte avec des codes%. Retenez :%mpour le mois,%Mpour les minutes.- Ajoutez du temps avec
checked_add_days,checked_add_monthsou+ TimeDelta. Soustrayez deux dates pour obtenir unTimeDelta, puis appelez.num_days(). - Passez « aujourd’hui » en paramètre à vos fonctions au lieu de lire l’horloge à l’intérieur.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Quel type utiliser pour mesurer le temps que prend un morceau de code ?
2. Que renvoie NaiveDate::from_ymd_opt(2025, 2, 30) ?
3. Dans un motif de formatage chrono, que signifie %M ?
4. Vous soustrayez deux NaiveDate : deadline - today. Comment obtenir le nombre de jours ?
5. Avec chrono, que donne le 31 janvier 2025 plus Months::new(1) ?
Score : 0 / 5
Pour aller plus loin
Section intitulée « Pour aller plus loin »- std::time — bibliothèque standard de Rust (en anglais) :
Instant,DurationetSystemTime, avec plus d’exemples. - Documentation de chrono — docs.rs (en anglais) : le guide complet de tous les types et méthodes de chrono.
- Codes de formatage de chrono — docs.rs (en anglais) : la liste complète des codes
%pourformatetparse_from_str. - Date and Time — Rust Cookbook (en anglais) : de courtes recettes pour les tâches courantes avec les dates et les heures.
- Documentation de jiff — docs.rs (en anglais) : un crate de dates et d’heures plus récent, très solide sur les fuseaux horaires, si vous voulez comparer.