Salta ai contenuti

Date e orari in Rust con chrono

I programmi hanno a che fare con il tempo di continuo. «Quanto ci ha messo?» «Che giorno scade la fattura?» «Quanti giorni mancano al viaggio?» Rust divide queste domande in due tipi.

Il modulo std::time della libreria standard è un cronometro: misura quanto tempo passa. Non sa niente di calendari, mesi o giorni della settimana. Per lavorare con il calendario, quasi ogni progetto Rust usa il crate chrono. Questa pagina comincia dal cronometro e poi passa al calendario.

Instant::now() fa una lettura dell’orologio del computer. Più tardi, .elapsed() ti dice quanto tempo è passato da quella lettura, sotto forma di 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!("totale = {total}");
println!("tempo: {:?}", elapsed);
println!("tempo: {} ms", elapsed.as_millis());
println!("meno di 5 secondi? {}", elapsed < Duration::from_secs(5));
}
▶ Provalo tu

Qui non mostriamo l’output apposta: il tempo è diverso su ogni computer e a ogni esecuzione. Vedrai qualcosa come tempo: 6.862793ms. Prova a premere il pulsante due volte e confronta.

Un Instant serve solo a misurare. Non puoi stamparlo come data, e va solo in avanti. È esattamente quello che vuoi per cronometrare il codice, perché non viene mai influenzato da qualcuno che cambia l’orologio di sistema.

Una Duration è una quantità di tempo, come «90 secondi». La crei a partire da secondi o millisecondi, puoi sommare più durate e confrontarle:

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!("totale: {:?}", total);
println!("{} min {} s", long.as_secs() / 60, long.as_secs() % 60);
println!("{}", short < long);
}

Output:

1.5s
90s
1500 ms
1.5 s
totale: 91.5s
1 min 30 s
true
▶ Provalo tu

Incontrerai Duration in tanti posti: std::thread::sleep(Duration::from_millis(100)) mette in pausa un thread, e anche i timeout delle librerie di rete prendono una Duration.

SystemTime è l’ora dell’orologio del computer, quella che vedi nell’angolo dello schermo. Di solito la si usa per chiedere quanto tempo è passato dall’epoca Unix, il 1° gennaio 1970 (UTC). Quel numero si chiama timestamp Unix:

use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let now = SystemTime::now();
let since_1970 = now
.duration_since(UNIX_EPOCH)
.expect("l'orologio è impostato prima del 1970");
println!("Secondi dal 1° gennaio 1970: {}", since_1970.as_secs());
}
▶ Provalo tu

Il numero dipende da quando lo esegui, quindi anche qui non mostriamo l’output. Perché duration_since restituisce un Result? Perché l’orologio di sistema può essere sbagliato o essere spostato indietro. È anche per questo che dovresti usare Instant, non SystemTime, per cronometrare il codice.

Questo è più o meno tutto ciò che fa std::time. Non ha idea di cosa significhino «marzo» o «venerdì». Per quello ti serve chrono.

Aggiungi il crate al progetto (vedi Cargo e crate se è una novità per te):

Terminal window
cargo add chrono
[dependencies]
chrono = "0.4"

Anche il Rust Playground ha chrono, quindi tutti i pulsanti qui sotto funzionano. chrono ha due famiglie di tipi:

  • I tipi naive (NaiveDate, NaiveTime, NaiveDateTime) sono date e orari senza fuso orario: «14 marzo 2025» o «09:30» scritti su un calendario. La maggior parte del codice di tutti i giorni usa questi.
  • DateTime<Tz> è un istante con un fuso orario, come DateTime<Utc> o DateTime<Local>.

NaiveDate::from_ymd_opt(anno, mese, giorno) costruisce una data. Restituisce un Option, perché non tutte le combinazioni sono date reali:

use chrono::{Datelike, NaiveDate, NaiveTime};
fn main() {
let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap();
println!("{date}");
println!("anno {}, mese {}, giorno {}", 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);
}

Output:

2025-03-14
anno 2025, mese 3, giorno 14
2025-03-14 09:30:00
2025-03-14 17:45:10
None
▶ Provalo tu

Il 30 febbraio non esiste, quindi ottieni None invece di una data rotta. Con valori fissi scritti da te, .unwrap() va bene. Con valori che arrivano da un utente, gestisci il None.

La riga use chrono::Datelike porta nello scope i metodi .year(), .month() e .day(). (Timelike fa lo stesso per .hour() e .minute().)

Utc::now() dà l’istante attuale in UTC. Local::now() lo dà nel fuso orario del computer:

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!("Oggi: {}", local_now.format("%Y-%m-%d"));
let today = local_now.date_naive();
println!("Come NaiveDate: {today}");
}
▶ Provalo tu

L’output è la data di oggi, quindi cambia ogni giorno. I server del Playground usano UTC, quindi lì le due righe mostreranno la stessa ora. .date_naive() scarta il fuso orario e l’ora, lasciando una semplice NaiveDate. Una buona abitudine: prendi «adesso» una volta sola ai bordi del programma, poi passa la data alle tue funzioni. Così restano facili da testare, come vedrai nell’esempio delle scadenze.

.format() prende un pattern. Ogni codice con % viene sostituito da una parte della data. %Y è l’anno, %m il numero del mese, %d il giorno:

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("report-%Y%m%d.txt").to_string();
println!("{file_name}");
}

Output:

2025-03-14
14/03/2025
Friday, March 14, 2025
Fri 14 Mar
09:05
9:05 AM
report-20250314.txt
▶ Provalo tu

I codici che userai di più: %Y anno, %m mese (01–12), %d giorno (01–31), %H ora (00–23), %M minuto, %S secondo, %A nome del giorno della settimana, %B nome del mese. Un - dopo il % toglie lo zero iniziale, quindi %-d stampa 5 invece di 05. .format() restituisce qualcosa che puoi stampare. Chiama .to_string() quando ti serve una String.

Nota che chrono scrive i nomi di giorni e mesi in inglese («Friday», «March»). Per i nomi in italiano serve la feature unstable-locales di chrono con format_localized; per le date in formato numerico, come %d/%m/%Y, la lingua non conta.

NaiveDate::parse_from_str fa il contrario: legge il testo usando lo stesso pattern con %. Restituisce un Result, perché il testo potrebbe non corrispondere:

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", "March 14", "2025-03"] {
match NaiveDate::parse_from_str(text, "%Y-%m-%d") {
Ok(date) => println!("Letta: {date}"),
Err(e) => println!("Impossibile leggere {text:?}: {e}"),
}
}
}

Output:

Ok(2025-03-14)
Ok(2025-03-14)
Ok(2025-03-14)
Ok(2025-03-14T09:30:00)
Impossibile leggere "2025-13-01": input is out of range
Impossibile leggere "March 14": input contains invalid characters
Impossibile leggere "2025-03": premature end of input
▶ Provalo tu

Per la forma standard AAAA-MM-GG puoi anche chiamare semplicemente .parse(). Il mese 13 non esiste, quindi il primo esempio sbagliato è «out of range» (fuori intervallo). I messaggi di errore di chrono sono in inglese: oltre a «fuori intervallo», dicono «contiene caratteri non validi» e «fine dell’input prematura». Sono brevi, ma abbastanza chiari da mostrare a un utente.

Aggiungere giorni a una data a mano è sorprendentemente difficile: i mesi hanno lunghezze diverse, ed esistono gli anni bisestili. chrono lo fa per te. Usa checked_add_days con Days, checked_add_months con Months, oppure somma 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 giorni dopo: {plus_30}");
let minus_7 = start.checked_sub_days(Days::new(7)).unwrap();
println!("7 giorni prima: {minus_7}");
let next_month = start.checked_add_months(Months::new(1)).unwrap();
println!("1 mese dopo: {next_month}");
let week_later = start + TimeDelta::weeks(1);
println!("1 settimana dopo: {week_later}");
let dt = start.and_hms_opt(23, 30, 0).unwrap();
let later = dt + TimeDelta::hours(2);
println!("2 ore dopo: {later}");
}

Output:

30 giorni dopo: 2025-03-02
7 giorni prima: 2025-01-24
1 mese dopo: 2025-02-28
1 settimana dopo: 2025-02-07
2 ore dopo: 2025-02-01 01:30:00
▶ Provalo tu

Guarda «1 mese dopo»: il 31 gennaio più un mese fa 28 febbraio, perché il 31 febbraio non esiste. chrono sceglie l’ultimo giorno valido. I metodi checked_ restituiscono un Option, che è None solo se il risultato cade fuori dall’intervallo di anni (molto ampio) gestito da chrono. Aggiungere 2 ore alle 23:30 passa correttamente al giorno dopo.

.weekday() (da Datelike) ti dice il giorno della settimana. Sottraendo una data da un’altra ottieni un TimeDelta, e .num_days() lo trasforma in un numero:

use chrono::{Datelike, NaiveDate, Weekday};
fn main() {
let date = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap();
let day = date.weekday();
println!("{date} è un {day} ({})", date.format("%A"));
let weekend = matches!(day, Weekday::Sat | Weekday::Sun);
println!("fine settimana? {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!("{} giorni, cioè {} settimane intere", gap.num_days(), gap.num_weeks());
println!("al contrario: {} giorni", (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!("soggiorno in hotel: {} ore", (check_out - check_in).num_hours());
}

Output:

2025-03-15 è un Sat (Saturday)
fine settimana? true
358 giorni, cioè 51 settimane intere
al contrario: -358 giorni
soggiorno in hotel: 69 ore
▶ Provalo tu

Weekday è un enum con le varianti da Mon a Sun (abbreviazioni inglesi dei giorni), quindi puoi usarci match. Nota che la differenza può essere negativa se la seconda data è successiva. È utile: un numero negativo di giorni rimasti significa «in ritardo».

Mettiamo tutto insieme. La funzione prende «oggi» come parametro invece di chiamare Local::now() al suo interno. In questo modo l’output qui sotto è sempre lo stesso, e la funzione è facile da testare. In un programma vero le passeresti 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("scade oggi"),
1 => String::from("scade domani"),
d if d > 1 => format!("mancano {d} giorni"),
-1 => String::from("1 giorno di ritardo"),
d => format!("{} giorni di ritardo", -d),
}
}
fn main() {
let today = NaiveDate::from_ymd_opt(2025, 6, 10).unwrap();
let tasks = [
("Pagare l'affitto", "2025-06-10"),
("Inviare il report", "2025-06-11"),
("Rinnovare passaporto", "2025-07-01"),
("Restituire il libro", "2025-06-09"),
("Fattura del dentista", "2025-06-03"),
];
println!("Oggi è {}", today.format("%d/%m/%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/%m"), days_left(today, deadline));
}
}

Output:

Oggi è 10/06/2025
Pagare l'affitto 10/06 scade oggi
Inviare il report 11/06 scade domani
Rinnovare passaporto 01/07 mancano 21 giorni
Restituire il libro 09/06 1 giorno di ritardo
Fattura del dentista 03/06 7 giorni di ritardo
▶ Provalo tu

Prova a cambiare today con un’altra data ed eseguilo di nuovo. {:<20} allunga il nome fino a 20 caratteri, così le colonne restano allineate (vedi Output). Qui usiamo date numeriche (%d/%m), così non compaiono i nomi inglesi dei mesi.

chrono non è l’unica scelta. Il crate time è un’alternativa più piccola e diffusa, e il più recente jiff dedica un’attenzione particolare ai fusi orari e all’ora legale; qui usiamo chrono perché è il più usato ed è disponibile nel Playground.

Dimenticare use chrono::Datelike. Senza, .year(), .month() e .weekday() non funzionano. L’errore dice che il metodo è «private» (privato), il che confonde, ma la riga di aiuto ti indica la vera soluzione:

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;
|

Il suggerimento dice che il trait Datelike, che fornisce year, è implementato ma non è nello scope: basta importarlo.

Confondere %m e %M. %m è il mese, %M è il minuto. Con una data e un’ora, quello sbagliato non dà errore. Stampa i minuti senza dire niente:

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")); // sbagliato: minuti
println!("{}", meeting.format("%Y-%m-%d")); // giusto: mese
}

Output:

2025-05-14
2025-03-14
▶ Provalo tu

Cronometrare il codice con SystemTime. L’orologio di sistema può saltare (per esempio quando si sincronizza con internet). Usa Instant per misurare, e SystemTime o chrono solo quando ti serve la data vera.

Chiamare Local::now() in profondità nella tua logica. Fa cambiare i risultati ogni giorno e rende difficile scrivere i test. Passa la data come parametro, come fa days_left.

  • std::time::Instant misura il tempo trascorso; Duration è una quantità di tempo; SystemTime è l’orologio di sistema (utile per i timestamp Unix).
  • Per i calendari, aggiungi chrono. Usa NaiveDate / NaiveDateTime per date senza fuso orario, e Utc::now() / Local::now() per l’istante attuale.
  • from_ymd_opt restituisce un Option, e parse_from_str restituisce un Result, perché non ogni input è una data reale.
  • .format("%Y-%m-%d") trasforma una data in testo usando i codici con %. Ricorda: %m è il mese, %M il minuto.
  • Aggiungi tempo con checked_add_days, checked_add_months o + TimeDelta. Sottrai due date per ottenere un TimeDelta, poi chiama .num_days().
  • Passa «oggi» alle tue funzioni invece di leggere l’orologio al loro interno.

Verifica veloce

1. Quale tipo dovresti usare per misurare quanto tempo impiega un pezzo di codice?

2. Cosa restituisce NaiveDate::from_ymd_opt(2025, 2, 30)?

3. In una stringa di formato di chrono, cosa significa %M?

4. Sottrai due NaiveDate: deadline - today. Come ottieni il numero di giorni?

5. Quanto fa in chrono il 31 gennaio 2025 più Months::new(1)?

Punteggio: 0 / 5