Aller au contenu

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.

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));
}
▶ Essayez par vous-même

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.

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.5s
90s
1500 ms
1.5 s
total : 91.5s
1 min 30 s
true
▶ Essayez par vous-même

Remarquez 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.

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());
}
▶ Essayez par vous-même

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.

Ajoutez le crate à votre projet (voir Cargo et les crates si c’est nouveau pour vous) :

Terminal window
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, comme DateTime<Utc> ou DateTime<Local>.

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-14
année 2025, mois 3, jour 14
2025-03-14 09:30:00
2025-03-14 17:45:10
None
▶ Essayez par vous-même

Le 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().)

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}");
}
▶ Essayez par vous-même

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.

.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-14
14/03/2025
Friday, March 14, 2025
Fri 14 Mar
09:05
9:05 AM
rapport-20250314.txt
▶ Essayez par vous-même

Les 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.

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 range
Impossible de lire "14 mars" : input contains invalid characters
Impossible de lire "2025-03" : premature end of input
▶ Essayez par vous-même

Pour 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.

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-02
7 jours plus tôt : 2025-01-24
1 mois plus tard : 2025-02-28
1 semaine plus tard : 2025-02-07
2 heures plus tard : 2025-02-01 01:30:00
▶ Essayez par vous-même

Regardez « 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.

.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 ? true
358 jours, soit 51 semaines complètes
à l'envers : -358 jours
séjour à l'hôtel : 69 heures
▶ Essayez par vous-même

Weekday 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 ».

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 2025
Payer le loyer 10 Jun pour aujourd'hui
Envoyer le rapport 11 Jun pour demain
Refaire passeport 01 Jul encore 21 jours
Rendre un livre 09 Jun 1 jour de retard
Payer le dentiste 03 Jun 7 jours de retard
▶ Essayez par vous-même

Essayez 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.

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 scope
help: 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-14
2025-03-14
▶ Essayez par vous-même

Chronomé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.

  • std::time::Instant mesure le temps écoulé ; Duration représente une durée ; SystemTime est l’heure de l’horloge (pratique pour les timestamps Unix).
  • Pour le calendrier, ajoutez chrono. Utilisez NaiveDate / NaiveDateTime pour des dates sans fuseau horaire, et Utc::now() / Local::now() pour l’instant présent.
  • from_ymd_opt renvoie une Option et parse_from_str un Result, car toutes les entrées ne sont pas de vraies dates.
  • .format("%Y-%m-%d") transforme une date en texte avec des codes %. Retenez : %m pour le mois, %M pour les minutes.
  • Ajoutez du temps avec checked_add_days, checked_add_months ou + TimeDelta. Soustrayez deux dates pour obtenir un TimeDelta, puis appelez .num_days().
  • Passez « aujourd’hui » en paramètre à vos fonctions au lieu de lire l’horloge à l’intérieur.

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