← Índice

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 tq para este capítulo está en 4-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.