Ir al contenido

Fechas y horas en Rust con chrono

Los programas trabajan con el tiempo todo el rato. “¿Cuánto tardó esto?” “¿Qué día vence la factura?” “¿Cuántos días faltan para el viaje?” Rust separa estas preguntas en dos tipos.

El módulo std::time de la biblioteca estándar es un cronómetro: mide cuánto tiempo pasa. No sabe nada de calendarios, meses ni días de la semana. Para trabajar con calendarios, casi todos los proyectos de Rust usan el crate chrono. Esta página empieza con el cronómetro y luego pasa al calendario.

Instant::now() toma una lectura del reloj del ordenador. Más tarde, .elapsed() te dice cuánto tiempo ha pasado desde esa lectura, como un 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!("tardó {:?}", elapsed);
println!("tardó {} ms", elapsed.as_millis());
println!("¿menos de 5 segundos? {}", elapsed < Duration::from_secs(5));
}
▶ Pruébalo tú mismo

Aquí no mostramos la salida a propósito: el tiempo cambia en cada ordenador y en cada ejecución. Verás algo como tardó 6.862793ms. Prueba a pulsar el botón dos veces y compara.

Un Instant solo sirve para medir. No puedes imprimirlo como una fecha, y solo avanza hacia delante. Eso es justo lo que quieres para cronometrar código, porque nunca le afecta que alguien cambie el reloj del sistema.

Un Duration es una cantidad de tiempo, como “90 segundos”. Lo creas a partir de segundos o milisegundos, puedes sumar duraciones y compararlas:

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

Salida:

1.5s
90s
1500 ms
1.5 s
total: 91.5s
1 min 30 s
true
▶ Pruébalo tú mismo

Te encontrarás Duration en muchos sitios: std::thread::sleep(Duration::from_millis(100)) pausa un hilo, y los tiempos de espera (timeouts) de las bibliotecas de red también reciben un Duration.

SystemTime es la hora del reloj del ordenador, la que ves en una esquina de la pantalla. Lo habitual es preguntar cuánto tiempo ha pasado desde la época Unix (Unix epoch), el 1 de enero de 1970 (UTC). Ese número se llama marca de tiempo Unix (Unix timestamp):

use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let now = SystemTime::now();
let since_1970 = now
.duration_since(UNIX_EPOCH)
.expect("el reloj está antes de 1970");
println!("Segundos desde el 1 de enero de 1970: {}", since_1970.as_secs());
}
▶ Pruébalo tú mismo

El número depende de cuándo lo ejecutes, así que tampoco mostramos la salida. ¿Por qué duration_since devuelve un Result? Porque el reloj del sistema puede estar mal o puede retroceder. Por eso también deberías usar Instant, y no SystemTime, para cronometrar tu código.

Eso es prácticamente todo lo que hace std::time. No tiene ni idea de qué significa “marzo” o “viernes”. Para eso necesitas chrono.

Añade el crate a tu proyecto (consulta Cargo y crates si esto es nuevo para ti):

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

El Playground de Rust también tiene chrono, así que todos los botones de abajo funcionan. chrono tiene dos familias de tipos:

  • Los tipos naive (“ingenuos”: NaiveDate, NaiveTime, NaiveDateTime) son fechas y horas sin zona horaria. “14 de marzo de 2025” o “09:30” apuntados en un calendario. La mayoría del código del día a día usa estos.
  • DateTime<Tz> es un momento en el tiempo con zona horaria, como DateTime<Utc> o DateTime<Local>.

NaiveDate::from_ymd_opt(year, month, day) construye una fecha (año, mes, día). Devuelve un Option, porque no todas las combinaciones son una fecha real:

use chrono::{Datelike, NaiveDate, NaiveTime};
fn main() {
let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap();
println!("{date}");
println!("año {}, mes {}, día {}", 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);
}

Salida:

2025-03-14
año 2025, mes 3, día 14
2025-03-14 09:30:00
2025-03-14 17:45:10
None
▶ Pruébalo tú mismo

El 30 de febrero no existe, así que obtienes None en lugar de una fecha rota. Con valores fijos que escribiste tú, .unwrap() está bien. Con valores que vienen de un usuario, gestiona el None.

La línea use chrono::Datelike trae los métodos .year(), .month() y .day(). (Timelike hace lo mismo con .hour() y .minute().)

Utc::now() te da el momento actual en UTC. Local::now() te lo da en la zona horaria del propio ordenador:

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!("Hoy: {}", local_now.format("%Y-%m-%d"));
let today = local_now.date_naive();
println!("Como NaiveDate: {today}");
}
▶ Pruébalo tú mismo

La salida es la fecha de hoy, así que cambia cada día. Los servidores del Playground usan UTC, así que allí las dos líneas mostrarán la misma hora. .date_naive() descarta la zona horaria y la hora, y deja un NaiveDate sin más. Un buen hábito: obtén “ahora” una sola vez en el borde de tu programa y pasa la fecha a tus funciones. Así son fáciles de probar, como verás en el ejemplo de la fecha límite.

.format() recibe un patrón. Cada código % se sustituye por una parte de la fecha. %Y es el año, %m el número de mes y %d el día:

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

Salida:

2025-03-14
14/03/2025
Friday, March 14, 2025
Fri 14 Mar
09:05
9:05 AM
informe-20250314.txt
▶ Pruébalo tú mismo

Los códigos que más usarás: %Y año, %m mes (01–12), %d día (01–31), %H hora (00–23), %M minuto, %S segundo, %A nombre del día de la semana, %B nombre del mes. Un - después del % quita el cero inicial, así que %-d imprime 5 en lugar de 05. .format() devuelve algo que puedes imprimir. Llama a .to_string() cuando necesites un String.

Fíjate en que los nombres de días y meses salen en inglés (“Friday”, “March”). Es lo que chrono hace por defecto. Si necesitas nombres en español, chrono tiene una feature opcional, unstable-locales, con el método format_localized. Para fechas que lee una máquina (archivos, APIs), los formatos numéricos como %Y-%m-%d son la mejor opción de todos modos.

NaiveDate::parse_from_str hace lo contrario: lee texto usando el mismo patrón con %. Devuelve un Result, porque puede que el texto no encaje:

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 marzo", "2025-03"] {
match NaiveDate::parse_from_str(text, "%Y-%m-%d") {
Ok(date) => println!("Leída: {date}"),
Err(e) => println!("No se pudo leer {text:?}: {e}"),
}
}
}

Salida:

Ok(2025-03-14)
Ok(2025-03-14)
Ok(2025-03-14)
Ok(2025-03-14T09:30:00)
No se pudo leer "2025-13-01": input is out of range
No se pudo leer "14 de marzo": input contains invalid characters
No se pudo leer "2025-03": premature end of input
▶ Pruébalo tú mismo

Para el formato estándar YYYY-MM-DD también puedes llamar simplemente a .parse(). El mes 13 no existe, así que el primer ejemplo malo da “input is out of range” (la entrada está fuera de rango). Los mensajes de error de chrono están en inglés: “input contains invalid characters” significa que la entrada tiene caracteres no válidos, y “premature end of input” que el texto se acabó antes de tiempo.

Sumar días a una fecha a mano es sorprendentemente difícil: los meses tienen longitudes distintas y existen los años bisiestos. chrono lo hace por ti. Usa checked_add_days con Days, checked_add_months con Months, o suma 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 días después: {plus_30}");
let minus_7 = start.checked_sub_days(Days::new(7)).unwrap();
println!("7 días antes: {minus_7}");
let next_month = start.checked_add_months(Months::new(1)).unwrap();
println!("1 mes después: {next_month}");
let week_later = start + TimeDelta::weeks(1);
println!("1 semana después: {week_later}");
let dt = start.and_hms_opt(23, 30, 0).unwrap();
let later = dt + TimeDelta::hours(2);
println!("2 horas después: {later}");
}

Salida:

30 días después: 2025-03-02
7 días antes: 2025-01-24
1 mes después: 2025-02-28
1 semana después: 2025-02-07
2 horas después: 2025-02-01 01:30:00
▶ Pruébalo tú mismo

Mira “1 mes después”: el 31 de enero más un mes es el 28 de febrero, porque no existe el 31 de febrero. chrono elige el último día válido. Los métodos checked_ devuelven un Option, que solo es None si el resultado se sale del rango de años de chrono (que es enorme). Sumar 2 horas a las 23:30 pasa correctamente al día siguiente.

Días de la semana y la diferencia entre dos fechas

Sección titulada «Días de la semana y la diferencia entre dos fechas»

.weekday() (de Datelike) te dice el día de la semana. Restar una fecha a otra te da un TimeDelta, y .num_days() lo convierte en un 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} es {day} ({})", date.format("%A"));
let weekend = matches!(day, Weekday::Sat | Weekday::Sun);
println!("¿fin 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!("{} días, o {} semanas completas", gap.num_days(), gap.num_weeks());
println!("al revés: {} días", (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!("estancia en el hotel: {} horas", (check_out - check_in).num_hours());
}

Salida:

2025-03-15 es Sat (Saturday)
¿fin de semana? true
358 días, o 51 semanas completas
al revés: -358 días
estancia en el hotel: 69 horas
▶ Pruébalo tú mismo

Weekday es un enum con las variantes Mon a Sun (de lunes a domingo, en inglés), así que puedes usar match con él. Fíjate en que la diferencia puede ser negativa si la segunda fecha es posterior. Eso es útil: un número negativo de días restantes significa “vencido”.

Vamos a juntarlo todo. La función recibe “hoy” como parámetro en lugar de llamar a Local::now() por dentro. Así la salida de abajo es siempre la misma, y la función es fácil de probar. En un programa real le pasarías 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 hoy"),
1 => String::from("vence mañana"),
d if d > 1 => format!("quedan {d} días"),
-1 => String::from("vencido hace 1 día"),
d => format!("vencido hace {} días", -d),
}
}
fn main() {
let today = NaiveDate::from_ymd_opt(2025, 6, 10).unwrap();
let tasks = [
("Pagar el alquiler", "2025-06-10"),
("Enviar el informe", "2025-06-11"),
("Renovar pasaporte", "2025-07-01"),
("Devolver un libro", "2025-06-09"),
("Pagar al dentista", "2025-06-03"),
];
println!("Hoy es {}", 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));
}
}

Salida:

Hoy es Tuesday, 10 June 2025
Pagar el alquiler 10 Jun vence hoy
Enviar el informe 11 Jun vence mañana
Renovar pasaporte 01 Jul quedan 21 días
Devolver un libro 09 Jun vencido hace 1 día
Pagar al dentista 03 Jun vencido hace 7 días
▶ Pruébalo tú mismo

Prueba a cambiar today por otra fecha y vuelve a ejecutarlo. El {:<20} rellena el nombre hasta 20 caracteres para que las columnas queden alineadas (consulta Salida por pantalla). Los nombres de las tareas no llevan tildes a propósito: así las columnas se ven alineadas en cualquier terminal.

chrono no es la única opción. El crate time es una alternativa más pequeña y popular, y el crate más reciente jiff cuida especialmente las zonas horarias y el horario de verano. Aquí usamos chrono porque es el más usado y está en el Playground.

Olvidar use chrono::Datelike. Sin él, .year(), .month() y .weekday() no funcionan. El error dice que el método es “private” (privado), lo que confunde, pero la línea de ayuda te da la solución real: el trait Datelike no está en el ámbito, así que impórtalo.

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 y %M. %m es el mes y %M es el minuto. Con una fecha y hora, el equivocado no da ningún error. Imprime los minutos sin decir nada:

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")); // mal: minutos
println!("{}", meeting.format("%Y-%m-%d")); // bien: mes
}

Salida:

2025-05-14
2025-03-14
▶ Pruébalo tú mismo

Cronometrar código con SystemTime. El reloj del sistema puede dar saltos (por ejemplo, cuando se sincroniza con internet). Usa Instant para medir, y SystemTime o chrono solo cuando necesites la fecha de verdad.

Llamar a Local::now() en lo más profundo de tu lógica. Hace que los resultados cambien cada día y que las pruebas sean difíciles de escribir. Pasa la fecha como parámetro, como hace days_left.

  • std::time::Instant mide el tiempo transcurrido; Duration es una cantidad de tiempo; SystemTime es la hora del reloj (útil para marcas de tiempo Unix).
  • Para calendarios, añade chrono. Usa NaiveDate / NaiveDateTime para fechas sin zona horaria, y Utc::now() / Local::now() para el momento actual.
  • from_ymd_opt devuelve un Option y parse_from_str devuelve un Result, porque no todas las entradas son una fecha real.
  • .format("%Y-%m-%d") convierte una fecha en texto usando códigos %. Recuerda: %m es el mes y %M es el minuto.
  • Suma tiempo con checked_add_days, checked_add_months o + TimeDelta. Resta dos fechas para obtener un TimeDelta y luego llama a .num_days().
  • Pasa “hoy” a tus funciones en lugar de leer el reloj dentro de ellas.

Comprueba lo aprendido

1. ¿Qué tipo deberías usar para medir cuánto tarda un trozo de código?

2. ¿Qué devuelve NaiveDate::from_ymd_opt(2025, 2, 30)?

3. En un patrón de formato de chrono, ¿qué significa %M?

4. Restas dos NaiveDate: deadline - today. ¿Cómo obtienes el número de días?

5. ¿Cuánto es el 31 de enero de 2025 más Months::new(1) en chrono?

Puntuación: 0 / 5