4.4 Fechas que un humano puede leer
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
En un buen libro de cuentas, cada entrada lleva una fecha. No porque sea costumbre - porque una entrada sin fecha no es una entrada sino un borrador con ambiciones. Podría ser de ayer, podría ser de hace tres meses, y ahora no hay forma de saberlo. Una tarea sobrevive entre ejecuciones: puede encontrarse, leerse, completarse. Cuándo se creó - esa es una pregunta sin respuesta. La diferencia entre “registrado” y “registrado tal día” no se nota inmediatamente. Siempre exactamente en el peor momento.
El intento
Para trabajar con el tiempo en Rust existe chrono. cargo add chrono lo añade a las
dependencias, y puede añadirse un campo a Task.
En Cargo.toml:
chrono = "0.4"
En src/task/mod.rs:
// src/task/mod.rs - intento
use chrono::{DateTime, Utc};
#[derive(Debug, Serialize, Deserialize)]
pub struct Task {
pub id: u64,
pub title: String,
pub status: Status,
pub created_at: DateTime<Utc>,
}
cargo build:
error[E0277]: the trait bound `DateTime<Utc>: Serialize` is not satisfied
--> src/task/mod.rs:XX:XX
|
XX | #[derive(Debug, Serialize, Deserialize)]
| ^^^^^^^^^ the trait `Serialize` is not implemented for `DateTime<Utc>`
|
= note: required for `Task` to implement `Serialize`
DateTime<Utc> no sabe convertirse en JSON.
Lo que sabe el compilador
El mismo mecanismo que dio a serde sus macros derive en el capítulo 4.0: chrono admite
serde, pero mantiene ese soporte detrás de una feature flag. No todo el que usa chrono
para cálculos de fechas necesita serde - añadir dependencias extra a un proyecto que no
las necesita es mala práctica.
chrono proporciona una feature "serde" - añade implementaciones de Serialize y
Deserialize para todos los tipos de chrono, incluyendo DateTime<Utc>. Actualiza
Cargo.toml:
chrono = { version = "0.4", features = ["serde"] }
El conjuro
Con la opción correcta el compilador queda satisfecho. Pero si ya existe un tasks.json sin
el campo created_at, la siguiente ejecución producirá un error missing field. Añade un
atributo a Task:
// src/task/mod.rs - CAMBIADO
#[derive(Debug, Serialize, Deserialize)]
pub struct Task {
pub id: u64,
pub title: String,
pub status: Status,
#[serde(default = "Utc::now")]
pub created_at: DateTime<Utc>,
}
#[serde(default = "Utc::now")] le dice a serde: si el campo está ausente en el JSON, llama
a Utc::now() y usa el resultado. Esta es la solución de compatibilidad: un tasks.json
del capítulo anterior no contiene created_at, y sin este atributo la carga fallaría con
missing field. Las tareas antiguas recibirán la hora actual como fecha de creación -
impreciso, pero el archivo se cargará.
Actualiza también Display - para que la fecha sea visible en el terminal, no solo en JSON:
// src/task/mod.rs - CAMBIADO
impl fmt::Display for Task {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"#{}: {} [{:?}] ({})",
self.id,
self.title,
self.status,
self.created_at.format("%Y-%m-%d")
)
}
}
.format("%Y-%m-%d") es un método de chrono para formatear una fecha. Año, mes, día -
es suficiente aquí. Otros formatos pueden explorarse en la
documentación de chrono:
%d.%m.%Y da 30.05.2026, %b %d da May 30.
Ambos lugares donde se crea una tarea necesitan actualizarse - la lógica no cambia, solo se añade el nuevo campo:
impl Task {
pub fn new(id: u64, title: &str) -> TqResult<Task> {
if title.is_empty() {
return Err(TqError::EmptyTitle);
}
Ok(Task {
id,
title: String::from(title),
status: Status::Todo,
created_at: Utc::now(), // NUEVO
})
}
}
impl From<&str> for Task {
fn from(title: &str) -> Self {
Task {
id: 0,
title: title.to_string(),
status: Status::Todo,
created_at: Utc::now(), // NUEVO
}
}
}
Utc::now() devuelve el momento actual como DateTime<Utc>. Ambos constructores reciben
la marca de tiempo de la misma manera.
Ahora tasks.json lleva la fecha de creación de cada tarea:
[
{
"id": 1,
"title": "Buy coffee",
"status": "Done",
"created_at": "2026-05-30T09:41:05.318Z"
}
]
El formato es RFC 3339, un subconjunto de ISO 8601: año, mes, día, T, hora, Z para UTC.
Legible sin decodificador.
En el terminal cada tarea tiene ahora este aspecto:
Loaded: 0 task(s)
#1: Buy coffee [Todo] (2026-05-30)
#2: Buy milk [Todo] (2026-05-30)
#3: Buy eggs [Todo] (2026-05-30)
Lo más importante a verificar es que created_at sobrevive un ciclo completo por JSON.
Añade use chrono::Utc; a src/task/tests.rs y tres tests:
// src/task/tests.rs - CAMBIADO
#[test]
fn new_task_records_creation_time() {
let before = Utc::now();
let task = Task::new(1, "Buy coffee").unwrap();
let after = Utc::now();
assert!(task.created_at >= before);
assert!(task.created_at <= after);
}
#[test]
fn task_roundtrips_through_json() {
let task = Task::new(1, "Buy coffee").unwrap();
let json = serde_json::to_string(&task).unwrap();
let restored: Task = serde_json::from_str(&json).unwrap();
assert_eq!(restored.id, task.id);
assert_eq!(restored.title, task.title);
assert_eq!(restored.created_at, task.created_at); // NUEVO
assert!(!restored.is_done());
}
#[test]
fn task_loads_without_created_at() {
let json = r#"{"id":1,"title":"Buy coffee","status":"Todo"}"#;
let task: Task = serde_json::from_str(json).unwrap();
assert_eq!(task.title, "Buy coffee");
}
make ci pasa. La suite completa de tests está en tq/.
El código completo de
tqpara este capítulo está en4-memory/04-dates-a-human-can-read/.
Lore: Cómo encontrar las feature flags de un crate
Las feature flags son práctica habitual en los crates de crates.io, no una excepción.
Siempre que un crate proporciona una capacidad que no todo el mundo necesita, la pone detrás
de una flag. serde y chrono no son casos especiales - son ejemplos ordinarios de esta
convención.
Para saber qué feature flags tiene un crate y qué activa cada una, consulta docs.rs: la
página de cada crate lista las features con sus descripciones. Para ver qué features están
activas en la compilación actual: cargo tree -e features.