Pular para o conteúdo

Datas e horas em Rust com chrono

Programas lidam com tempo o tempo todo. “Quanto tempo isso levou?” “Em que dia vence o boleto?” “Quantos dias faltam para a viagem?” O Rust divide essas perguntas em dois tipos.

O módulo std::time da biblioteca padrão é um cronômetro: ele mede quanto tempo passa. Ele não sabe nada de calendários, meses ou dias da semana. Para trabalhar com calendário, quase todo projeto Rust usa o crate (pacote) chrono. Esta página começa pelo cronômetro e depois passa para o calendário.

Instant::now() faz uma leitura do relógio do computador. Depois, .elapsed() diz quanto tempo passou desde essa leitura, como um 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!("levou {:?}", elapsed);
println!("levou {} ms", elapsed.as_millis());
println!("menos de 5 segundos? {}", elapsed < Duration::from_secs(5));
}
▶ Experimente você mesmo

Não mostramos a saída aqui de propósito: o tempo é diferente em cada computador e em cada execução. Você vai ver algo como levou 6.862793ms. Clique no botão duas vezes e compare.

Um Instant só serve para medir. Você não consegue imprimi-lo como data, e ele só anda para a frente. É exatamente o que você quer para cronometrar código, porque ele nunca é afetado por alguém mudando o relógio do sistema.

Um Duration é uma quantidade de tempo, como “90 segundos”. Você cria um a partir de segundos ou milissegundos, soma durações e compara umas com as outras:

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

Saída:

1.5s
90s
1500 ms
1.5 s
total: 91.5s
1 min 30 s
true
▶ Experimente você mesmo

Repare que o Rust usa ponto como separador decimal (1.5), não vírgula. Você vai encontrar Duration em muitos lugares: std::thread::sleep(Duration::from_millis(100)) pausa uma thread, e os timeouts das bibliotecas de rede também recebem um Duration.

SystemTime é a hora do relógio do computador, aquela que aparece no canto da sua tela. O uso mais comum é perguntar quanto tempo passou desde a época Unix (Unix epoch), 1º de janeiro de 1970 (UTC). Esse número se chama timestamp Unix:

use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let now = SystemTime::now();
let since_1970 = now
.duration_since(UNIX_EPOCH)
.expect("o relógio está antes de 1970");
println!("Segundos desde 1º de janeiro de 1970: {}", since_1970.as_secs());
}
▶ Experimente você mesmo

O número depende de quando você roda o programa, então de novo não há saída aqui. Por que duration_since retorna um Result? Porque o relógio do sistema pode estar errado ou pode ser atrasado. É também por isso que você deve usar Instant, e não SystemTime, para cronometrar o seu código.

E é praticamente só isso que o std::time faz. Ele não tem ideia do que significa “março” ou “sexta-feira”. Para isso, você precisa do chrono.

Adicione o crate ao seu projeto (veja Cargo e crates se isso for novidade):

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

O Rust Playground também tem o chrono, então todos os botões abaixo funcionam. O chrono tem duas famílias de tipos:

  • Os tipos naive (“ingênuos”: NaiveDate, NaiveTime, NaiveDateTime) são datas e horas sem fuso horário. “14 de março de 2025” ou “09:30”, como escritos num calendário. A maior parte do código do dia a dia usa esses.
  • DateTime<Tz> é um momento no tempo com fuso horário, como DateTime<Utc> ou DateTime<Local>.

NaiveDate::from_ymd_opt(ano, mês, dia) monta uma data. Ele retorna um Option, porque nem toda combinação é uma data de verdade:

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

Saída:

2025-03-14
ano 2025, mês 3, dia 14
2025-03-14 09:30:00
2025-03-14 17:45:10
None
▶ Experimente você mesmo

O dia 30 de fevereiro não existe, então você recebe None em vez de uma data quebrada. Com valores fixos que você mesmo digitou, .unwrap() não tem problema. Com valores vindos de um usuário, trate o None.

A linha use chrono::Datelike traz os métodos .year(), .month() e .day(). (Timelike faz o mesmo para .hour() e .minute().)

Utc::now() dá o momento atual em UTC. Local::now() dá o momento no fuso horário do próprio computador:

use chrono::{Local, Utc};
fn main() {
let utc_now = Utc::now();
let local_now = Local::now();
println!("UTC: {}", utc_now);
println!("Local: {}", local_now);
println!("Hoje: {}", local_now.format("%Y-%m-%d"));
let today = local_now.date_naive();
println!("Como NaiveDate: {today}");
}
▶ Experimente você mesmo

A saída é a data de hoje, então ela muda todo dia. Os servidores do Playground usam UTC, então as duas primeiras linhas mostram a mesma hora lá. .date_naive() descarta o fuso horário e a hora, deixando um NaiveDate simples. Um bom hábito: pegue o “agora” uma única vez, na borda do seu programa, e depois passe a data para as suas funções. Assim elas ficam fáceis de testar, como você vai ver no exemplo do prazo.

.format() recebe um padrão. Cada código com % é trocado por uma parte da data. %Y é o ano, %m o número do mês, %d o dia:

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

Saída:

2025-03-14
14/03/2025
Friday, March 14, 2025
Fri 14 Mar
09:05
9:05 AM
relatorio-20250314.txt
▶ Experimente você mesmo

Os códigos que você mais vai usar: %Y ano, %m mês (01–12), %d dia (01–31), %H hora (00–23), %M minuto, %S segundo, %A nome do dia da semana, %B nome do mês. Um - depois do % tira o zero à esquerda, então %-d imprime 5 em vez de 05. .format() retorna algo que você pode imprimir. Chame .to_string() quando precisar de uma String.

Repare que os nomes de dias e meses (Friday, March) saem em inglês: é o padrão do chrono. Para datas em português, o formato numérico %d/%m/%Y, que é o que usamos no Brasil, resolve a maioria dos casos.

NaiveDate::parse_from_str faz o caminho inverso: ele lê um texto usando o mesmo padrão com %. Ele retorna um Result, porque o texto pode não bater com o padrão:

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 de março", "2025-03"] {
match NaiveDate::parse_from_str(text, "%Y-%m-%d") {
Ok(date) => println!("Data lida: {date}"),
Err(e) => println!("Não foi possível ler {text:?}: {e}"),
}
}
}

Saída:

Ok(2025-03-14)
Ok(2025-03-14)
Ok(2025-03-14)
Ok(2025-03-14T09:30:00)
Não foi possível ler "2025-13-01": input is out of range
Não foi possível ler "14 de março": input contains invalid characters
Não foi possível ler "2025-03": premature end of input
▶ Experimente você mesmo

Para o formato padrão AAAA-MM-DD você também pode simplesmente chamar .parse(). O mês 13 não existe, então o primeiro exemplo ruim dá “out of range” (fora do intervalo). As mensagens de erro do chrono vêm em inglês: “invalid characters” quer dizer que o texto tem caracteres que não cabem no padrão, e “premature end of input” quer dizer que o texto acabou antes do padrão terminar. Elas são curtas, mas claras o bastante para mostrar a um usuário (ou para você trocar por uma mensagem sua).

Somar dias a uma data na mão é surpreendentemente difícil: os meses têm tamanhos diferentes, e existem anos bissextos. O chrono faz isso para você. Use checked_add_days com Days, checked_add_months com Months, ou some um 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 dias depois: {plus_30}");
let minus_7 = start.checked_sub_days(Days::new(7)).unwrap();
println!("7 dias antes: {minus_7}");
let next_month = start.checked_add_months(Months::new(1)).unwrap();
println!("1 mês depois: {next_month}");
let week_later = start + TimeDelta::weeks(1);
println!("1 semana depois: {week_later}");
let dt = start.and_hms_opt(23, 30, 0).unwrap();
let later = dt + TimeDelta::hours(2);
println!("2 horas depois: {later}");
}

Saída:

30 dias depois: 2025-03-02
7 dias antes: 2025-01-24
1 mês depois: 2025-02-28
1 semana depois: 2025-02-07
2 horas depois: 2025-02-01 01:30:00
▶ Experimente você mesmo

Olhe o “1 mês depois”: 31 de janeiro mais um mês dá 28 de fevereiro, porque 31 de fevereiro não existe. O chrono escolhe o último dia válido. Os métodos checked_ retornam um Option, que só é None se o resultado cair fora do intervalo de anos do chrono (que é enorme). Somar 2 horas a 23:30 passa corretamente para o dia seguinte.

.weekday() (de Datelike) diz o dia da semana. Subtrair uma data de outra dá um TimeDelta, e .num_days() transforma isso em um número:

use chrono::{Datelike, NaiveDate, Weekday};
fn main() {
let date = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap();
let day = date.weekday();
println!("{date} cai num {day} ({})", date.format("%A"));
let weekend = matches!(day, Weekday::Sat | Weekday::Sun);
println!("fim de semana? {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!("{} dias, ou {} semanas completas", gap.num_days(), gap.num_weeks());
println!("ao contrário: {} dias", (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!("estadia no hotel: {} horas", (check_out - check_in).num_hours());
}

Saída:

2025-03-15 cai num Sat (Saturday)
fim de semana? true
358 dias, ou 51 semanas completas
ao contrário: -358 dias
estadia no hotel: 69 horas
▶ Experimente você mesmo

Weekday é um enum com as variantes de Mon (segunda) a Sun (domingo), então você pode usar match nele. Repare que a diferença pode ser negativa se a segunda data for a mais tarde. Isso é útil: um número negativo de dias restantes quer dizer “atrasado”.

Vamos juntar tudo. A função recebe “hoje” como parâmetro, em vez de chamar Local::now() lá dentro. Assim a saída abaixo é sempre a mesma, e a função fica fácil de testar. Num programa de verdade, você passaria 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("vence hoje"),
1 => String::from("vence amanhã"),
d if d > 1 => format!("faltam {d} dias"),
-1 => String::from("1 dia de atraso"),
d => format!("{} dias de atraso", -d),
}
}
fn main() {
let today = NaiveDate::from_ymd_opt(2025, 6, 10).unwrap();
let tasks = [
("Pay rent", "2025-06-10"),
("Send report", "2025-06-11"),
("Renew passport", "2025-07-01"),
("Return library book", "2025-06-09"),
("Dentist bill", "2025-06-03"),
];
println!("Hoje é {}", 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));
}
}

Saída:

Hoje é 10/06/2025
Pay rent 10/06 vence hoje
Send report 11/06 vence amanhã
Renew passport 01/07 faltam 21 dias
Return library book 09/06 1 dia de atraso
Dentist bill 03/06 7 dias de atraso
▶ Experimente você mesmo

Tente mudar today para outra data e rode de novo. O {:<20} completa o nome com espaços até 20 caracteres para as colunas ficarem alinhadas (veja Saída). Os nomes das tarefas ficaram em inglês de propósito: a largura do {:<20} é contada em chars, e alguns caracteres (como emojis, ou acentos escritos como dois chars) não ocupam exatamente uma coluna na tela. Em vez dos nomes de mês em inglês (%d %b), usamos o formato brasileiro %d/%m.

O chrono não é a única opção. O crate time é uma alternativa menor e popular, e o crate jiff, mais novo, dá atenção extra a fusos horários e ao horário de verão. Usamos o chrono aqui porque ele é o mais usado e está disponível no Playground.

Esquecer o use chrono::Datelike. Sem ele, .year(), .month() e .weekday() não funcionam. O erro diz que o método é “private” (privado), o que confunde, mas a linha de help mostra a correção de verdade: importar o trait Datelike.

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

Confundir %m com %M. %m é o mês, %M é o minuto. Com uma data e hora, o código errado não dá erro. Ele simplesmente imprime os minutos, sem avisar:

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")); // errado: minutos
println!("{}", meeting.format("%Y-%m-%d")); // certo: mês
}

Saída:

2025-05-14
2025-03-14
▶ Experimente você mesmo

Cronometrar código com SystemTime. O relógio do sistema pode dar saltos (por exemplo, quando ele se sincroniza com a internet). Use Instant para medir, e SystemTime ou o chrono só quando você precisar da data de verdade.

Chamar Local::now() lá no meio da sua lógica. Isso faz os resultados mudarem todo dia e deixa os testes difíceis de escrever. Passe a data como parâmetro, como a days_left faz.

  • std::time::Instant mede o tempo decorrido; Duration é uma quantidade de tempo; SystemTime é o relógio do sistema (bom para timestamps Unix).
  • Para calendários, adicione o chrono. Use NaiveDate / NaiveDateTime para datas sem fuso horário, e Utc::now() / Local::now() para o momento atual.
  • from_ymd_opt retorna um Option, e parse_from_str retorna um Result, porque nem toda entrada é uma data de verdade.
  • .format("%Y-%m-%d") transforma uma data em texto usando códigos com %. Lembre: %m é mês, %M é minuto.
  • Some tempo com checked_add_days, checked_add_months ou + TimeDelta. Subtraia duas datas para obter um TimeDelta e depois chame .num_days().
  • Passe “hoje” para as suas funções em vez de ler o relógio dentro delas.

Teste o que aprendeu

1. Qual tipo você deve usar para medir quanto tempo um trecho de código leva?

2. O que NaiveDate::from_ymd_opt(2025, 2, 30) retorna?

3. Numa string de formato do chrono, o que %M significa?

4. Você subtrai dois NaiveDates: deadline - today. Como você obtém o número de dias?

5. Quanto é 31 de janeiro de 2025 mais Months::new(1) no chrono?

Pontuação: 0 / 5