diff --git a/book/19-fechas.md b/book/19-fechas.md new file mode 100644 index 0000000..d0b0f6e --- /dev/null +++ b/book/19-fechas.md @@ -0,0 +1,1405 @@ +# Capítulo 19 — Fechas: de la etiqueta del calendario al número de día + +> Capítulo avanzado, pero **escrito para que se entienda de verdad**. Se apoya en variables y tipos +> (`02`), operadores (`03`), `if` y bucles (`04`), funciones (`05`), arreglos y cadenas (`06`), punteros +> (`07`), `struct` (`08`), tipos como `size_t` y `time_t` (`16`), entrada/salida (`14`), archivos (`15`) +> y tu librería de cadenas (`18`). +> +> Acá vas a dejar de pelear con un calendario lleno de reglas raras (meses de distinto largo, años +> bisiestos, husos horarios) y vas a **construir tu propia librería de fechas**: un conjunto de +> funciones que validan, comparan, suman y formatean fechas **convirtiéndolas primero a un simple +> número de días**. Al final vas a poder imprimir un calendario entero, calcular vencimientos y +> entender por qué existe el "efecto 2038". + +--- + +## Cómo leer este capítulo + +Este es el capítulo donde "sé usar `time()`" se convierte en "entiendo **qué** es una fecha, **por qué** +es tan incómoda, y cómo domarla". No es teoría: cada idea viene con **código para mirar**, y cada bloque +cierra con **práctica** para que la ejercites con las manos. + +Está dividido en **seis partes**. Después de cada concepto importante hay uno o varios ejercicios +cortos. **No los saltees**: son la mitad del capítulo. + +| Parte | De qué habla | La idea en una frase | +|---|---|---| +| **1** | El calendario es una convención | Una fecha es una **etiqueta humana**, no un número: tiene reglas raras (meses de distinto largo y años bisiestos). | +| **2** | El paradigma: linealizar | Convertí la fecha a **un solo número de día** y toda la aritmética se vuelve trivial. | +| **3** | Aritmética de fechas | Sumar días, restar días, comparar y ubicar el día de la semana: todo con el número de día. | +| **4** | El reloj del sistema | `time_t`, `struct tm`, `mktime` y por qué tu computadora **no sabe** en qué huso horario estás hasta que se lo decís. | +| **5** | Formatear y parsear | Mostrar una fecha como texto (a tu búfer o **prestada**) y volver del texto a la fecha (`strftime`, `sscanf`, `strptime`). | +| **6** | El catálogo completo + jefe | `time_t`, UTC vs local, `difftime`… y un impresor de calendarios para cerrar. | + +Los **ejercicios** se numeran en orden: `19.1`, `19.2`, `19.3`, y así. Cada uno trae su **escenario de +prueba** (qué entra y qué tiene que salir, **nunca** el algoritmo) para que compiles y compruebes. + +> **Regla de oro del libro:** el capítulo explica y muestra **cómo funciona** cada cosa (con código +> ilustrativo), pero **los ejercicios no traen la solución**. El enunciado te da el problema; la +> respuesta la escribís vos. + +## Tu librería objetivo + +Antes de arrancar, mirá el plano de la casa que vas a construir. Estas son las funciones que vas a +tener escritas **vos** al terminar el capítulo. Acá van **solo las declaraciones** (el *contrato*: qué +recibe y qué devuelve cada una), no el cuerpo. Es tu mapa de ruta. + +```c +/* ==== date.h — el contrato de tu librería (solo prototipos) ==== */ +#include /* size_t */ +#include /* time_t, struct tm */ + +typedef struct { + int year; /* año completo, ej. 2024 */ + int month; /* 1 = enero ... 12 = diciembre (1-based) */ + int day; /* 1 = primero ... 31 (1-based) */ +} Date; + +/* --- Calendario --- */ +int date_is_leap(int year); /* ¿año bisiesto? */ +int date_days_in_month(int year, int month); /* 28, 29, 30 o 31 */ +int date_is_valid(Date d); /* ¿existe esa fecha? */ + +/* --- Linealización (el paradigma) --- */ +long date_to_day_number(Date d); /* fecha → número de día */ +Date date_from_day_number(long n); /* número de día → fecha */ + +/* --- Aritmética --- */ +Date date_add_days(Date d, long n); /* fecha + n días */ +long date_diff_days(Date a, Date b); /* días entre a y b */ +int date_compare(Date a, Date b); /* -1, 0 o 1 */ +int date_day_of_week(Date d); /* 0 = domingo ... 6 = sábado */ +int date_day_of_year(Date d); /* 1 = 1 de enero ... 365/366 */ + +/* --- Reloj del sistema --- */ +time_t date_to_epoch(Date d, int hour, int min, int sec); +Date date_today(void); +void date_now_local(struct tm *out); /* "ahora" en tu huso */ + +/* --- Formato y parseo --- */ +size_t date_format(Date d, const char *fmt, char *out, size_t cap); /* a TU búfer */ +const char *date_to_string(Date d, const char *fmt); /* búfer interno PRESTADO (¡ojo!) */ +int date_parse(const char *s, const char *fmt, Date *out); /* 1 = ok */ +``` + +> **Fijate en tres decisiones de diseño.** (1) El año, el mes y el día van **1-based** (enero es `1`, +> no `0`): es lo natural para un humano y evita la trampa de `struct tm`. (2) Hay **dos mundos**: tu +> `Date` (para lógica de calendario pura, sin husos horarios) y `time_t`/`struct tm` (para hablar con +> el reloj del sistema). Los vas a conectar en la Parte 4. Mantenerlos separados es lo que te va a +> salvar de los bugs de huso horario. (3) El formato tiene **dos contratos**: `date_format` escribe en +> **tu** búfer (seguro, tuyo); `date_to_string` te **presta** un búfer interno (cómodo, pero se pisa en +> la próxima llamada). Vas a ver por qué en la Parte 5. + +--- + +# PARTE 1 — El calendario es una convención + +Antes de escribir una línea, hay que ver **por qué** las fechas son tan molestas. No es casualidad: es +el resultado de siglos de humanos tratando de hacer coincidir su calendario con el cielo. Si entendés +eso, todas las reglas raras dejan de ser magia. + +## 1.1 Una fecha no es un número: es una etiqueta + +Pensá en el **calendario de la pared de la cocina**. Cuando decís "14 de octubre de 2026", no estás +diciendo un número: estás leyendo una **etiqueta** en una grilla. Y hay una trampa enorme escondida: +**esas etiquetas no se pueden sumar directamente**. + +- `2026 + 1` **no** es "el año que viene": es... `2027`, que casualmente *podría* serlo, pero solo + porque el año coincide con la etiqueta del año. +- `14 + 1` **no** es "mañana": es `15`, y `15` de octubre es mañana, pero solo porque el mes tiene más + de 14 días. +- `10 + 3` (mes + 3) **no** es "tres meses después": `10 + 3 = 13`, y no existe el mes 13. + +Moraleja: una fecha es una **etiqueta de tres piezas** (año, mes, día) que vive en una estructura +irregular. Para operar con fechas vas a necesitar **otra representación**. Eso es la Parte 2. Primero, +veamos por qué el calendario es tan irregular. + +## 1.2 El calendario gregoriano: un edificio con cuartos de distinto tamaño + +El calendario que usás todos los días se llama **gregoriano** (por el papa Gregorio XIII, que lo +implantó en 1582). Es un edificio con **doce cuartos**, pero **no todos del mismo tamaño**: + +| Mes | Días | Mes | Días | +|---|---:|---|---:| +| enero | 31 | julio | 31 | +| febrero | **28 o 29** | agosto | 31 | +| marzo | 31 | septiembre | 30 | +| abril | 30 | octubre | 31 | +| mayo | 31 | noviembre | 30 | +| junio | 30 | diciembre | 31 | + +Mirá el desorden: meses de 31, meses de 30, y **febrero**, el mes raro que cambia de tamaño cada tanto. +No hay ninguna fórmula elegante detrás: es **historia**. Los romanos tenían otro calendario, y cuando +agregaron meses y movieron días, quedó este enredo. Y lo vas a tener que codificar tal cual. + +> **El truco para no memorizar.** La tabla es la ley, pero hay un patrón nemotécnico: **31 - 30 - 31 - +> 30**, salvo el último par (julio y agosto, ambos de 31, por vanidad de dos emperadores). Y febrero, +> aparte, con su regla propia (1.3). Guardá la **tabla**; la nemotecnia es solo para humanos. + +## 1.3 El año bisiesto: por qué febrero a veces tiene 29 + +Acá está la parte que a todo el mundo le enseñaron mal. La regla **completa** es: + +> Un año es **bisiesto** si es divisible por **4**, **excepto** si es divisible por **100**, **salvo** +> que sea divisible por **400**. + +O sea: **2024** es bisiesto (divisible por 4). **1900** **no** es bisiesto (divisible por 100, y no por +400). **2000** **sí** es bisiesto (divisible por 400). Febrero de un año bisiesto tiene **29** días; en +el resto, **28**. + +¿Por qué semejante regla? Porque el **año solar** —el tiempo real que tarda la Tierra en dar una vuelta +al Sol— dura unos **365,2422 días**, no 365 exactos: + +```text + año solar ≈ 365,2422 días + calendario simple = 365 días → se atrasa 0,2422 días por año + 0,2422 × 4 ≈ 0,97 días → cada 4 años, casi un día de atraso + → agregamos 1 día cada 4 años (el "bisiesto") + pero ese "casi" deja un sobrante → corregimos con las reglas del 100 y del 400 +``` + +Cada regla **corrige** el error de la anterior: el 4 agrega días de más, el 100 los quita, el 400 los +vuelve a poner. Es una **cascada de correcciones** para que el calendario no se desincronice de las +estaciones (si no, en unos siglos la Navidad caería en verano). + +> **Por qué te importa de verdad.** `date_days_in_month` (y medio mundo de la aritmética de fechas) +> dependen de acertar el bisiesto. Y acertarlo **mal** es el bug de fechas más famoso: hay sistemas +> reales que asumieron "cada 4 años" y se rompieron el **29 de febrero de 1900** o el de **2100** (fines +> de siglo que **no** son bisiestos). La regla de los 400 no es un detalle: es la que casi todos +> olvidan. + +> **Nota histórica fina.** El gregoriano no rige desde siempre: se adoptó en 1582. Las fechas +> **anteriores** a esa época, en el calendario gregoriano, se llaman **prolépticas** (o sea: "si el +> calendario hubiera existido"). Para este capítulo uses fechas modernas, la distinción no te cambia +> nada; pero es bueno saber que "año 0" o "año 1" son convenciones, no hechos. + +## 1.4 Validar: no toda etiqueta existe + +Con las reglas de arriba ya podés responder la pregunta más básica: **¿esta fecha existe?** +`2024-02-29` sí; `2023-02-29` no; `2024-04-31` tampoco (abril tiene 30); `2024-13-01` no (no hay mes +13); `2024-00-15` no (los meses van del 1 al 12). Validar **antes** de calcular es la primera defensa +contra bugs raros. + +```text + ¿existe? + 2024-02-29 → sí (2024 es bisiesto) + 2023-02-29 → no (2023 no es bisiesto → febrero tiene 28) + 1900-02-29 → no (1900 divisible por 100 y no por 400) + 2000-02-29 → sí (2000 divisible por 400) + 2024-04-31 → no (abril tiene 30) + 2024-12-31 → sí +``` + +## Práctica de la Parte 1 + +### Ejercicio 19.1 — El detector de bisiestos (`date_is_leap`) + +**Conceptos** · operadores `%`, `&&`, `||`, reglas de divisibilidad, `int` + +**Contexto** · Medio calendario depende de saber si un año es bisiesto. Simplificación: una función +que responde sí/no a esa pregunta, con la regla **completa** (no la de "cada 4 años"). + +**Consigna** · Implementá `date_is_leap(int year)`: devuelve **distinto de cero** si `year` es bisiesto +y `0` si no. Usá la regla de los 4 / 100 / 400. Comprobá con los **casos de fin de siglo**, que son los +que separan una solución correcta de una que "casi" anda. + +**Entrada** · Un año (`int`). +**Salida** · "Bisiesto" o "No bisiesto". + +**Escenario de prueba** + +```text +2024 → Bisiesto +2023 → No bisiesto +1900 → No bisiesto (divisible por 100, no por 400) +2000 → Bisiesto (divisible por 400) +2100 → No bisiesto +1600 → Bisiesto +``` + +**Identificadores sugeridos** · `date_is_leap`, `year` +**Criterios de aceptación** +- [ ] Se usa `%` para las tres divisibilidades. +- [ ] `1900` da **no** y `2000` da **sí** (la parte que casi todos olvidan). +- [ ] `2024` da **sí** y `2023` da **no**. +- [ ] El retorno es "verdadero/falso" al estilo C (distinto de cero / cero). + +--- + +### Ejercicio 19.2 — ¿Cuántos días tiene el mes? (`date_days_in_month`) + +**Conceptos** · tabla de largos de mes, reusar `date_is_leap`, `switch` o arreglo + +**Contexto** · Para validar fechas y para armar calendarios necesitás el largo de cada mes, que depende +de febrero y del bisiesto. Simplificación: una función que devuelve 28, 29, 30 o 31. + +**Consigna** · Implementá `date_days_in_month(int year, int month)`: devuelve la cantidad de días de +`month` en `year`. Los meses de 31 y de 30 son fijos (tabla); febrero es `29` si `date_is_leap(year)` y +`28` si no. Podés usar un `switch`, un arreglo, o la lógica que prefieras. Decidí y documentá qué +devolvés si `month` está fuera de `1..12`. + +**Entrada** · Un año y un mes. +**Salida** · La cantidad de días. + +**Escenario de prueba** + +```text +2024, 2 → 29 +2023, 2 → 28 +1900, 2 → 28 +2000, 2 → 29 +2024, 4 → 30 +2024, 1 → 31 +2024, 12 → 31 +``` + +**Identificadores sugeridos** · `date_days_in_month`, `year`, `month`, `days` +**Criterios de aceptación** +- [ ] Los meses de 31 y 30 dan lo correcto (no memorizados a mano en cada `case` sin orden). +- [ ] Febrero reusa `date_is_leap` (no repite la regla). +- [ ] Los cuatro casos de febrero del escenario dan lo esperado. +- [ ] El caso `month` fuera de rango está decidido y documentado. + +--- + +### Ejercicio 19.3 — El validador de fechas (`date_is_valid`) + +**Conceptos** · `struct`, validación por rangos, reusar `date_days_in_month` + +**Contexto** · Antes de confiar en una fecha leída del usuario o de un archivo, hay que verificar que +exista. Simplificación: una función que dice si una `Date` es real. + +**Consigna** · Definí la estructura `Date` (con `year`, `month`, `day`). Implementá +`date_is_valid(Date d)`: devuelve "verdadero" solo si + +1. el `month` está entre `1` y `12`; +2. el `day` está entre `1` y `date_days_in_month(d.year, d.month)`. + +Comprobá con varios casos, incluidos los que **parecen** válidos pero no lo son. + +**Entrada** · Una `Date` (posiblemente inválida). +**Salida** · "Válida" o "Inválida". + +**Escenario de prueba** + +```text +2024-02-29 → Válida +2023-02-29 → Inválida (febrero de 2023 tiene 28) +2024-04-31 → Inválida (abril tiene 30) +2024-13-01 → Inválida (no hay mes 13) +2024-00-10 → Inválida (los meses empiezan en 1) +2024-06-00 → Inválida (los días empiezan en 1) +2024-12-31 → Válida +``` + +**Identificadores sugeridos** · `Date`, `date_is_valid`, `d` +**Criterios de aceptación** +- [ ] Se valida el **rango del mes** antes de usar el día. +- [ ] El largo del mes sale de `date_days_in_month` (no está hardcodeado). +- [ ] Los siete casos del escenario dan lo esperado. +- [ ] No se accede a datos de la fecha sin haberla validado antes de usarla. + +--- + +# PARTE 2 — El paradigma: linealizar la fecha + +Este es el corazón del capítulo, y la idea que te va a volar la cabeza. En el capítulo `18` cambiaste +"caminar hasta el `'\0'`" por "guardar el largo". Acá vas a cambiar "operar con tres piezas +irregulares" por **"operar con un solo número"**. + +## 2.1 El problema: restar dos fechas es un infierno + +Imaginate que querés saber **cuántos días pasaron** entre `2024-02-27` y `2024-03-02`. Con las +etiquetas no podés hacer `02 - 27` (da negativo y sin sentido) ni `03 - 02` (ignora los meses). Tenés +que: restar partes, ajustar por fin de mes, sumar los días de los meses del medio, y acordarte del +bisiesto. Un festival de casos borde. + +```text + 2024-02-27 → 2024-03-02 + ¿cuántos días? + + feb 2024 es bisiesto → tiene 29 días + 27 → 29 : 2 días + 29 → 01 mar: 1 día (o al revés: 29 feb + 1 = 1 mar) + 01 → 02 mar: 1 día + total: 4 días ← ¡y esto fue un caso FÁCIL! +``` + +Ahora imaginate hacer lo mismo con **años** de por medio, o comparar dos fechas, o calcular el día de +la semana. Cada operación es una receta distinta, llena de reglas. Es insoportable. + +## 2.2 La solución: un número de día + +Acá está el truco. Elegí un **día cero** (un *epoch*, un origen) y **contá los días** desde ahí. Cada +fecha pasa a ser **un solo número entero**: + +```text + elijo origen: 1970-01-01 → día 0 + + 1969-12-31 → -1 + 1970-01-01 → 0 + 1970-01-02 → 1 + 2000-01-01 → 10957 + 2024-01-01 → 19723 +``` + +Y ahora, mágicamente, **toda la aritmética se vuelve de primaria**: + +```text + días entre a y b = b Día − a Día (una resta) + fecha + n días = fecha Día + n (una suma) + comparar dos fechas = comparar dos números (un <) + día de la semana = (Día + constante) % 7 (un módulo) +``` + +Ese número se llama **número de día serial** (o *Rata Die*, o *Julian Day Number*, según el origen que +elijas). El nombre importa poco: lo que importa es que **una fecha irregular se convierte en un punto +en una recta**, y en la recta todo es fácil. + +> **Pensalo como el "kilómetro 0".** Es como cuando medís distancias por ruta: no decís "entre la +> ciudad A y la B hay 3 provincias y 2 peajes", decís **"hay 250 km"**. El número de día es el +> kilometraje del calendario. Y elegir el origen (1970, el año 1, lo que sea) es elegir dónde ponés el +> mojón 0: no cambia las distancias, solo la numeración. + +> **El paralelo con el capítulo `18`.** Allá, "caminar la calle hasta el `'\0'`" era lento y fragil; +> guardar el `count` lo resolvía. Acá, "operar con tres piezas irregulares" es frágil; **linealizar a +> un número de día** lo resuelve. Misma filosofía: **cambiá la representación para que la operación sea +> trivial**. + +## 2.3 Convertir ida y vuelta + +Necesitás **dos** funciones: una que lleve fecha → número, y otra que vuelva número → fecha. + +**De fecha a número** (la ida) es "contar". La forma directa: contar los días de **todos los años +completos** anteriores, más los días de los **meses completos** del año actual, más los días del mes +que ya pasaron. + +```text + número_de_día(2024-03-02) + = (días de 1970..2023 completos) ← años enteros + + (días de ene 2024 + días de feb 2024) ← meses enteros (¡feb respeta bisiesto!) + + (2 - 1) ← días ya pasados de marzo +``` + +Cada año aporta **365** o **366** (según bisiesto); cada mes aporta lo suyo. Es una suma ordenada. + +**De número a fecha** (la vuelta) es "descontar". Vas restando bloques: primero grandes (siglos, y +mejor todavía: bloques de 400 años, que tienen una cantidad **fija** de días), después años, después +meses, y lo que sobra es el día del mes. + +> **El detalle que hace la diferencia: el bloque de 400 años.** El calendario gregoriano se **repite** +> cada 400 años: **400 años = 146097 días** exactos, con 97 bisiestos. Ese bloque fijo es la clave para +> hacer la vuelta sin adivinar. (Por eso el 400 de la regla del bisiesto no es casualidad: es lo que +> hace que todo cierre cada 400 años.) + +> **No hace falta que memorices la fórmula.** En este capítulo el **algoritmo es tuyo**: podés contar +> con bucles simples (año a año, mes a mes). Va a ser más lento que la fórmula cerrada, pero es +> **correcto y transparente**, que para aprender es lo que importa. Si después querés la versión +> rápida, la vas a poder derivar vos. **La clave es que la ida y la vuelta sean inversas**: si +> `date_from_day_number(date_to_day_number(d))` te devuelve `d`, ganaste. + +## Práctica de la Parte 2 + +### Ejercicio 19.4 — Linealizar (`date_to_day_number`) + +**Conceptos** · contar años/meses/días, reusar `date_is_leap` y `date_days_in_month`, `long` + +**Contexto** · Es la operación que habilita toda la aritmética posterior. Simplificación: convertir una +fecha a la cantidad de días transcurridos desde el **1 de enero de 1970**. + +**Consigna** · Implementá `date_to_day_number(Date d)`, usando como origen el **1970-01-01 = día 0**. +Contá los días de los años **completos** anteriores a `d.year`, más los días de los meses completos de +`d.year` antes de `d.month`, más `d.day - 1`. El retorno es `long` (puede ser negativo para fechas +anteriores al origen). + +**Entrada** · Una `Date` válida. +**Salida** · El número de día serial. + +**Escenario de prueba** + +```text +1970-01-01 → 0 +1970-01-02 → 1 +1969-12-31 → -1 +2000-01-01 → 10957 +2000-03-01 → 11017 +2024-01-01 → 19723 +``` + +> **Cómo comprobar 2000-01-01 = 10957:** son 30 años desde 1970 hasta 2000, con los bisiestos +> `1972, 1976, …, 1996` (7 en total). `30 × 365 + 7 = 10957`. Y `2000-03-01` agrega enero (31) + febrero +> bisiesto (29) = 60 → `10957 + 60 = 11017`. + +**Identificadores sugeridos** · `date_to_day_number`, `d`, `days`, `y`, `m` +**Criterios de aceptación** +- [ ] Se usa `long` (o más ancho) para el resultado (puede ser negativo). +- [ ] Cada año suma 365 o 366 según `date_is_leap`. +- [ ] Cada mes suma lo que dice `date_days_in_month` (febrero respeta el bisiesto). +- [ ] Los seis valores del escenario salen exactos, incluido el negativo. + +--- + +### Ejercicio 19.5 — Volver atrás (`date_from_day_number`) + +**Conceptos** · descontar bloques, bucles, condición de corte, ida y vuelta + +**Contexto** · Para mostrar una fecha ("dentro de 45 días") o para sumar/restar, hay que poder +recorrer el camino inverso. Simplificación: la operación recíproca de 19.4. + +**Consigna** · Implementá `date_from_day_number(long n)` (mismo origen: `0` = 1970-01-01). Descontá +años (365 o 366 según bisiesto) hasta que no quepa uno más; después descontá meses enteros; lo que +sobre, `+1`, es el día del mes. Comprobá que es **inversa** de 19.4: para varias fechas, +`date_from_day_number(date_to_day_number(d))` devuelve exactamente `d`. + +**Entrada** · Un número de día (puede ser negativo). +**Salida** · La `Date` correspondiente. + +**Escenario de prueba** + +```text +0 → 1970-01-01 +1 → 1970-01-02 +-1 → 1969-12-31 +10957 → 2000-01-01 +11017 → 2000-03-01 +19723 → 2024-01-01 +20740 → 2026-10-14 +``` + +> **Prueba de ida y vuelta (la que importa):** para cada fecha del escenario de 19.4, +> `date_from_day_number(date_to_day_number(d)) == d`. Si eso se cumple en muchos casos (incluidos fines +> de mes y fines de año), tus dos funciones están bien. + +**Identificadores sugeridos** · `date_from_day_number`, `n`, `year`, `month` +**Criterios de aceptación** +- [ ] Maneja números **negativos** (fechas anteriores a 1970). +- [ ] Es **inversa** de `date_to_day_number` en los casos probados. +- [ ] Los fines de mes y de año se calculan bien (no hay "mes 13" ni "día 32"). +- [ ] Devolvió una `Date` **válida** en todos los casos (`date_is_valid` da verdadero). + +--- + +# PARTE 3 — Aritmética de fechas + +Con el número de día, esta parte es casi una formalidad: cada operación es **dos lines** de código. +Pero son las funciones que más vas a usar. + +## 3.1 Sumar y restar días + +"Vence en 30 días", "hace una semana", "3 días hábiles después". Todo eso es: + +```text + fecha + n días = date_from_day_number( date_to_day_number(fecha) + n ) +``` + +Nada más. El número de día hace que "cruzar un fin de mes", "cruzar un año bisiesto", "cruzar un fin de +año" sean **exactamente el mismo caso**: sumar `n`. Los casos borde que antes eran un infierno, ahora +**no existen**. + +## 3.2 Diferencia en días + +Cuántos días entre dos fechas = **restar los números de día**. El orden importa: `b - a` da cuántos días +hay que sumarle a `a` para llegar a `b`. Si `a` es posterior, da negativo. + +```text + diff(2024-01-01, 2024-03-01) = 11017 − ... = 60 días + diff(2024-03-01, 2024-01-01) = -60 días +``` + +> **Decidí el signo explícitamente.** ¿Querés el resultado **con signo** (te dice cuál es anterior) o +> siempre **positivo** (la distancia "a secas")? Los dos son válidos; lo que **no** es válido es no +> saber qué devolvés. Documentalo. + +## 3.3 Comparar fechas + +Comparar `a < b` es comparar sus números de día. Devolvés `-1`, `0` o `1` (el mismo contrato que +`strcmp`, capítulo `06`, para que sea familiar). + +## 3.4 Día de la semana y día del año + +Estos dos salen **gratis** del número de día: + +- **Día de la semana:** como sabés qué día fue el origen (1970-01-01 fue **jueves**), el resto de dividir + por 7 te da el día. En la convención de C (`struct tm`), **domingo = 0**. Entonces + + ```text + día_semana = (número_de_día + 4) mod 7 (el +4 alinea el jueves con el 4) + ``` + + Cuidado con el `mod` de números **negativos** en C: `-1 % 7` da `-1`, no `6`. Para que dé siempre + `0..6`, la fórmula segura es `((x % 7) + 7) % 7`. + +- **Día del año:** cuántos días pasaron desde el 1 de enero (1 = 1 de enero, 366 = 31 de diciembre en + bisiesto). Se calcula sumando los meses anteriores + el día, o bien con la diferencia de números de + día contra el 1 de enero del mismo año. + +> **El `mod` negativo: la trampa N.º 1 de esta parte.** En C, `%` con un operando negativo puede dar +> negativo. Como los números de día **sí** pueden ser negativos (fechas antes de 1970), cualquier +> `% 7` sin blindar te puede devolver un día de semana inválido (como `-3`). Blindalo siempre con +> `((x % 7) + 7) % 7`. + +## Práctica de la Parte 3 + +### Ejercicio 19.6 — Sumar días (`date_add_days`) + +**Conceptos** · composición `from(to(d) + n)`, cruce de fin de mes/año, `long` + +**Contexto** · Calcular vencimientos, plazos y fechas relativas es sumar días. Simplificación: una +función que suma (o resta, con `n` negativo) días a una fecha. + +**Consigna** · Implementá `date_add_days(Date d, long n)` usando **solo** `date_to_day_number` y +`date_from_day_number` (no reimplementes los saltos de mes). Comprobá los cruces difíciles: fin de +febrero en bisiesto, cambio de año, y `n = 0`. + +**Entrada** · Una `Date` y un `n` (puede ser negativo). +**Salida** · La `Date` resultante. + +**Escenario de prueba** + +```text +2024-02-28 + 2 → 2024-03-01 (2024 es bisiesto: el 29 existe) +2023-02-28 + 1 → 2023-03-01 (2023 no: salta directo a marzo) +2024-12-31 + 1 → 2025-01-01 +2024-01-01 - 1 → 2023-12-31 +2024-06-15 + 0 → 2024-06-15 +2024-01-31 + 1 → 2024-02-01 (cruzar el fin de un mes de 31) +``` + +**Identificadores sugeridos** · `date_add_days`, `d`, `n` +**Criterios de aceptación** +- [ ] Se implementa con `to_day_number` + `from_day_number` (no con bucles de mes propios). +- [ ] Los seis casos del escenario (incluidos los bisiestos) dan lo esperado. +- [ ] `n` negativo resta correctamente. +- [ ] El resultado siempre pasa `date_is_valid`. + +--- + +### Ejercicio 19.7 — Diferencia en días (`date_diff_days`) + +**Conceptos** · resta de números de día, signo, `long` + +**Contexto** · "¿Cuántos días faltan?", "¿cuánto tiempo estuvo abierto el ticket?" Simplificación: la +distancia en días entre dos fechas. + +**Consigna** · Implementá `date_diff_days(Date a, Date b)` que devuelva `b − a` **en días** (positivo si +`b` es posterior a `a`, negativo si es anterior, `0` si son el mismo día). Documentá el signo. + +**Entrada** · Dos `Date`. +**Salida** · La diferencia en días (con signo). + +**Escenario de prueba** + +```text +a = 2024-01-01, b = 2024-03-01 → 60 +a = 2024-03-01, b = 2024-01-01 → -60 +a = 2024-02-28, b = 2024-03-01 → 2 (2024 bisiesto) +a = 2024-01-01, b = 2024-01-01 → 0 +a = 2000-01-01, b = 2024-01-01 → 8766 +``` + +**Identificadores sugeridos** · `date_diff_days`, `a`, `b` +**Criterios de aceptación** +- [ ] Es una simple resta de los números de día. +- [ ] El signo es el documentado (positivo cuando `b` es posterior). +- [ ] `diff(a, a) == 0`. +- [ ] Los cinco valores del escenario dan exactos. + +--- + +### Ejercicio 19.8 — Comparar (`date_compare`) + +**Conceptos** · contrato `-1/0/1`, comparación de números, orden + +**Contexto** · Ordenar una lista de turnos, facturas o eventos necesita comparar fechas. Simplificación: +una función de comparación al estilo `strcmp`. + +**Consigna** · Implementá `date_compare(Date a, Date b)`: devuelve `-1` si `a` es anterior a `b`, `0` si +son iguales, y `1` si `a` es posterior. Basate en los números de día (no compares año/mes/día por +separado; ya tenés el número). Después, usala para **ordenar** un pequeño arreglo de fechas (un +ordenamiento por burbuja sencillo alcanza). + +**Entrada** · Dos `Date` (y, para la demostración, un arreglo). +**Salida** · El veredicto de la comparación y el arreglo ordenado. + +**Escenario de prueba** + +```text +compare(2024-01-01, 2024-03-01) → -1 +compare(2024-03-01, 2024-01-01) → 1 +compare(2024-02-29, 2024-02-29) → 0 + +arreglo: [2024-03-01, 2020-12-31, 2024-01-01, 2023-06-15] +ordenado: [2020-12-31, 2023-06-15, 2024-01-01, 2024-03-01] +``` + +**Identificadores sugeridos** · `date_compare`, `a`, `b`, `dates` +**Criterios de aceptación** +- [ ] Devuelve exactamente `-1`, `0` o `1`. +- [ ] Se apoya en el número de día (no en comparaciones encadenadas a mano). +- [ ] El arreglo queda ordenado de menor a mayor. +- [ ] Fechas del mismo año y mes comparan bien por día. + +--- + +### Ejercicio 19.9 — Día de la semana (`date_day_of_week`) + +**Conceptos** · módulo, `mod` de negativos blindado, convención domingo = 0 + +**Contexto** · Saber en qué día de la semana cae una fecha es la base de cualquier calendario o agenda. +Simplificación: devolver el día de la semana (0 = domingo … 6 = sábado), como `struct tm`. + +**Consigna** · Implementá `date_day_of_week(Date d)`: usá el número de día y la constante que alinea +`1970-01-01` (que fue **jueves**, o sea `4`) con la convención domingo = 0. **Blindá** el módulo para +valores negativos (fechas antes de 1970). Mostrá el resultado con **nombre** (por ejemplo, un arreglo +de nombres de días), no solo el número. + +**Entrada** · Una `Date`. +**Salida** · El nombre del día de la semana. + +**Escenario de prueba** + +```text +1970-01-01 → jueves (0) +2000-01-01 → sábado (10957) +2024-01-01 → lunes (19723) +2024-02-29 → jueves +1969-12-31 → miércoles (número −1: probá que el módulo NO dé un índice inválido) +``` + +> **Comprobación de la constante.** `1970-01-01` fue **jueves**. Con domingo = 0, jueves = **4**. Por eso +> la fórmula es `(día + 4) mod 7`. Si te da otro día, revisá el origen y la convención. + +**Identificadores sugeridos** · `date_day_of_week`, `names`, `w` +**Criterios de aceptación** +- [ ] Usa el número de día + una constante (no cuenta días desde un punto fijo con un bucle gigante). +- [ ] El resultado siempre está en `0..6`, incluso con números de día negativos. +- [ ] Los cinco casos del escenario dan exactos. +- [ ] Se imprime el **nombre** del día, no solo el índice. + +--- + +### Ejercicio 19.10 — Día del año (`date_day_of_year`) + +**Conceptos** · suma de meses anteriores, bisiesto, rango `1..366` + +**Contexto** · Los calendarios y los reportes anuales numeran los días del año (el "día 100"). Es un +dato que también sirve para medir distancias dentro del mismo año. Simplificación: el ordinal del día. + +**Consigna** · Implementá `date_day_of_year(Date d)`: devuelve `1` para el 1 de enero y `365`/`366` para +el 31 de diciembre (según bisiesto). Podés sumar los días de los meses anteriores + el día, o restar +números de día contra el 1 de enero del mismo año. Comprobá el 1 de enero, un fin de año y el **29 de +febrero**. + +**Entrada** · Una `Date`. +**Salida** · El día del año (`1..366`). + +**Escenario de prueba** + +```text +2024-01-01 → 1 +2024-02-29 → 60 (31 de enero + 29 de febrero) +2024-03-01 → 61 +2023-03-01 → 60 (2023 no bisiesto: febrero tuvo 28) +2024-12-31 → 366 +2023-12-31 → 365 +``` + +**Identificadores sugeridos** · `date_day_of_year`, `sum`, `m` +**Criterios de aceptación** +- [ ] El 1 de enero da `1` (no `0`). +- [ ] El fin de año da `365` o `366` según el bisiesto. +- [ ] `2024-03-01` da `61` y `2023-03-01` da `60` (la diferencia es el bisiesto). +- [ ] No se accede a meses fuera de `1..12`. + +--- + +# PARTE 4 — El reloj del sistema: `time_t`, `struct tm` y `mktime` + +Hasta acá manejaste fechas **puras** (año, mes, día), sin horas ni husos horarios. Pero tarde o +temprano vas a necesitar **el ahora** del sistema, o convertir tu fecha a un instante absoluto. Ahí +entra ``, y con él, una buena dosis de trampas. + +## 4.1 El epoch y `time_t`: segundos desde el kilómetro 0 + +La computadora, por dentro, **no** guarda "2026-10-14". Guarda **un número**: la cantidad de +**segundos** transcurridos desde un instante de referencia llamado **epoch**. + +- **`time_t`**: el tipo que representa ese número. El estándar **no fija** su unidad ni su epoch, pero + en la práctica (Linux, Windows, macOS) es **segundos** desde el **1970-01-01 00:00:00 UTC** (el + "Unix epoch"). +- **`time(NULL)`**: devuelve el `time_t` de **ahora**. + +```text + epoch: 1970-01-01 00:00:00 UTC → time_t = 0 + + 2000-01-01 00:00:00 UTC → 946684800 + 2024-01-01 00:00:00 UTC → 1704067200 +``` + +> **Los segundos son UTC.** El epoch y `time_t` **no dependen** de tu huso horario: son un instante +> absoluto. Si hoy a las 12:00 en Buenos Aires y a las 12:00 en Tokio son instantes distintos, sus +> `time_t` **también** lo son. `time_t` es "un punto en la recta del tiempo universal"; el huso horario +> recién aparece cuando **mostrás** ese instante a un humano (4.3). + +## 4.2 `struct tm`: la fecha desarmada en piezas + +Cuando querés "abrir" un `time_t` en año, mes, día, hora, etc., C te da una estructura: **`struct tm`**. +Es como un **sobre con etiquetas**, una casilla por campo. Pero tiene **tres trampas históricas** que +arruinan a todo el mundo la primera vez. Leelas dos veces. + +| Campo | Significado | Trampa | +|---|---|---| +| `tm_sec` | segundos `0–60` | El `60` es para los *leap seconds*. | +| `tm_min` | minutos `0–59` | — | +| `tm_hour` | horas `0–23` | No hay 24. | +| `tm_mday` | día del mes `1–31` | **1-based** (empieza en 1). | +| `tm_mon` | mes `0–11` | **¡Enero es 0!** (0-based) | +| `tm_year` | años **desde 1900** | **¡2026 se guarda como 126!** | +| `tm_wday` | día de semana `0–6` | Domingo = 0 (0-based) | +| `tm_yday` | día del año `0–365` | **0-based** (¡el 1 de enero es 0!) | +| `tm_isdst` | horario de verano | `>0` sí, `0` no, `<0` "no sé" | + +> **El trío de trampas.** (1) **`tm_year` desde 1900**: para pasar de `struct tm` a año real, **sumás +> 1900**; para ir al revés, **restás 1900**. (2) **`tm_mon` 0-based**: enero es `0`, diciembre es `11`; +> por eso casi todo el mundo escribe `month - 1` al armar una `struct tm`. (3) **`tm_yday` 0-based**: el +> 1 de enero es `0`, no `1`. Cada una de estas te va a morder si te olvidás. + +> **Compará con tu `Date`.** ¿Ves por qué tu `Date` es 1-based y `struct tm` es 0-based? Vos elegiste lo +> **natural para un humano**; C eligió, por razones históricas, lo que era cómodo para **restar**. Tu +> librería te protege de eso: convertís a `struct tm` **solo** en la frontera (cuando hablás con el +> sistema) y volvés enseguida a tu mundo 1-based. + +## 4.3 `mktime`: de la fecha al instante (y la normalización) + +`mktime` es la función que convierte una `struct tm` (fecha **local**) en un `time_t`: + +```text + struct tm (año/mes/día/hora LOCAL) ── mktime ──► time_t (segundos desde el epoch) +``` + +Y tiene un **superpoder escondido**: **normaliza**. Si le pasás un `tm_mday = 32`, no se queja: entiende +"el día 32 del mes" y lo convierte al **1 del mes siguiente**. Lo mismo con `tm_mon = 12` (→ enero del +año siguiente), o valores negativos. ¡Es una calculadora de fechas incorporada! + +```c +#include +#include + +int main(void) { + /* Armamos una struct tm "desde cero". Ojo con las trampas: */ + struct tm t = { 0 }; /* todos los campos en 0 (buena práctica) */ + + t.tm_year = 2000 - 1900; /* ¡años desde 1900! → 100 */ + t.tm_mon = 1 - 1; /* ¡enero es 0! → 0 */ + t.tm_mday = 1; /* día 1 */ + t.tm_isdst = -1; /* "-1 = averiguá vos el horario de verano" */ + + /* mktime interpreta la struct como hora LOCAL y devuelve el time_t. + Con TZ=UTC, 2000-01-01 00:00:00 da exactamente 946684800. */ + time_t epoch = mktime(&t); + printf("epoch = %ld\n", (long)epoch); + return 0; +} +``` + +> **`tm_isdst = -1`: por qué.** El horario de verano (DST) cambia según la fecha y el país. Si no querés +> pelear, poné `-1` y dejá que `mktime` lo **averigüe** solo. Si lo pones mal, obtenés un `time_t` +> corrido una hora. La regla práctica: **`-1` y listo**. + +## 4.4 UTC vs local: dos relojes, y uno que a veces no existe + +Acá está la fuente de **la mitad** de los bugs de fechas del mundo: + +- **UTC** (Tiempo Universal Coordinado) es el reloj **absoluto**. No tiene husos ni verano. `time_t` + vive en UTC. +- **Local** es el reloj **de tu pared**: UTC **más** el *offset* de tu huso horario (por ejemplo, + Buenos Aires suele ser UTC−3) **más** el corrimiento de horario de verano, si aplica. + +Dos funciones mueven entre ambos: + +| Función | Qué hace | +|---|---| +| `gmtime(&t)` | `time_t` → `struct tm` en **UTC** (Greenwich) | +| `localtime(&t)` | `time_t` → `struct tm` en **tu huso** (usa la variable `TZ`) | + +> **El horario que no existe.** En los cambios de DST, hay una hora que **se repite** (cuando atrasan el +> reloj) y otra que **nunca ocurre** (cuando lo adelantan). Por eso `local` no es una función +> matemática: esa hora local puede ser **ambigua o inexistente**. La conclusión de oro: **guardá y +> calculá siempre en UTC** (`time_t`); convertí a local **solo al mostrar**. + +> **Persistencia con UTC.** Si guardás fechas en un archivo (capítulo `15`) o las enviás por la red, +> **guardalas como `time_t` o como UTC**, nunca como "hora local sin huso". Si guardás "14:00" sin +> decir en qué reloj, otro programa en otro país las va a leer mal. Esto es un clásico de los sistemas +> distribuidos. + +## Práctica de la Parte 4 + +### Ejercicio 19.11 — De fecha a instante (`date_to_epoch`) + +**Conceptos** · `struct tm`, resta de 1900, `mon - 1`, `mktime`, `tm_isdst` + +**Contexto** · Para comparar con el reloj del sistema, o para guardar una fecha como número, hay que +pasar de tu `Date` al `time_t` absoluto. Simplificación: convertir una `Date` + hora a epoch. + +**Consigna** · Implementá `date_to_epoch(Date d, int hour, int min, int sec)`: armá una `struct tm` +cuidando las **tres trampas** (`year - 1900`, `month - 1`, `tm_isdst = -1`), completá hora/min/seg, +llamá a `mktime` y devolvé el `time_t`. Después comprobá la **ida y vuelta**: convertí ese `time_t` a +`struct tm` con `gmtime` y verificá que los campos coincidan (con `TZ=UTC`). + +**Entrada** · Una `Date` y hora/minuto/segundo. +**Salida** · El `time_t` correspondiente. + +**Escenario de prueba** + +```text +(con TZ = UTC) +date_to_epoch(1970-01-01, 0, 0, 0) → 0 +date_to_epoch(2000-01-01, 0, 0, 0) → 946684800 +date_to_epoch(2024-01-01, 0, 0, 0) → 1704067200 +``` + +> **Si tus números difieren** por un múltiplo de la hora (por ejemplo, ±3 h), es tu **huso horario** +> metiéndose: `mktime` interpreta la `struct tm` como **local**. Para el escenario exacto, corré con +> `TZ=UTC` (`set TZ=UTC` en Windows, `TZ=UTC ./prog` en Linux/macOS). Es justamente el punto de 4.4. + +**Identificadores sugeridos** · `date_to_epoch`, `t`, `d`, `sec` +**Criterios de aceptación** +- [ ] `tm_year = d.year - 1900` y `tm_mon = d.month - 1` (¡las trampas!). +- [ ] `tm_isdst = -1`. +- [ ] Con `TZ=UTC`, los tres valores del escenario dan exactos. +- [ ] Se rellena hora, minuto y segundo del parámetro. + +--- + +### Ejercicio 19.12 — El reloj de ahora (`date_today` + `date_now_local`) + +**Conceptos** · `time(NULL)`, `gmtime`, `localtime`, `strftime`, diferencia UTC/local + +**Contexto** · Un log, un turno o un mensaje de bienvenida necesitan "el ahora". Simplificación: +mostrar la fecha y hora actual en **UTC** y en tu **huso local**, y calcular la diferencia. + +**Consigna** · Implementá: + +1. `date_today()` que devuelva **hoy** como `Date` (usá `time(NULL)` y `localtime`, cuidando las + trampas de `struct tm`); +2. `date_now_local(struct tm *out)` que llene `out` con el "ahora" local. + +En el `main`, mostrá el instante actual formateado en **UTC** y en **local** (con `strftime`), y la +**diferencia** entre ambos (el *offset* de tu huso) en horas. + +**Entrada** · Ninguna (lee el reloj). +**Salida** · La fecha/hora UTC, la local, y el offset. + +**Escenario de prueba** + +```text +UTC : 2026-10-14 12:00:00 +local : 2026-10-14 09:00:00 (UTE−3, por ejemplo) +offset: -3 horas +``` + +> **No es determinista** (depende de cuándo lo corras). Lo que **sí** es verificable: la diferencia +> entre UTC y local debe ser un número **entero de horas** (o media hora en algunos países), y el +> `time_t` que usás para las dos vistas debe ser **el mismo**. + +**Identificadores sugeridos** · `date_today`, `date_now_local`, `now`, `utc`, `local` +**Criterios de aceptación** +- [ ] Ambas vistas parten del **mismo** `time_t` (`time(NULL)` una sola vez). +- [ ] `localtime` se usa para el huso; `gmtime` para UTC. +- [ ] Las trampas de `struct tm` se manejan al convertir a `Date`. +- [ ] La diferencia UTC−local queda expresada en horas. + +--- + +# PARTE 5 — Formatear y parsear: del número al texto y de vuelta + +Última pieza: **mostrar** la fecha (para un humano) y **leerla** (de un archivo o del teclado). Es la +frontera entre tu mundo interno (números, estructuras) y el mundo externo (texto). + +## 5.1 `strftime`: la plantilla de formato + +`strftime` toma una `struct tm` y una **plantilla** (una cadena de formato, como en `printf`) y escribe +el texto resultante en un búfer. Cada `%` y una letra significan un campo: + +| Formato | Qué produce | Ejemplo | +|---|---|---| +| `%Y` | año, 4 dígitos | `2024` | +| `%m` | mes, `01–12` | `02` | +| `%d` | día del mes, `01–31` | `29` | +| `%H` | hora, `00–23` | `13` | +| `%M` | minuto, `00–59` | `45` | +| `%S` | segundo, `00–60` | `07` | +| `%j` | día del año, `001–366` | `060` | +| `%w` | día de semana, `0–6` (dom = 0) | `4` | +| `%A` | nombre del día | `Thursday` | +| `%B` | nombre del mes | `February` | +| `%F` | atajo de `%Y-%m-%d` | `2024-02-29` | +| `%T` | atajo de `%H:%M:%S` | `13:45:07` | +| `%%` | un `%` literal | `%` | + +```c +#include +#include + +int main(void) { + /* Armamos una struct tm fija (OJO: año desde 1900, mes 0-based). */ + struct tm t = { 0 }; + t.tm_year = 2024 - 1900; + t.tm_mon = 2 - 1; + t.tm_mday = 29; + + /* mktime completa tm_wday y tm_yday a partir del año/mes/día. + Sin esto, %A y %j no tendrían de dónde salir. */ + mktime(&t); + + char buf[64]; + /* strftime(búfer, tamaño, plantilla, &tm): escribe el texto formateado. + Devuelve cuántos bytes escribió (sin contar el '\0'). */ + strftime(buf, sizeof buf, "%Y-%m-%d (%A)", &t); + printf("%s\n", buf); /* 2024-02-29 (Thursday) */ + return 0; +} +``` + +> **El detalle de `%A` y `%B`: dependen del *locale*.** Con la configuración por defecto (el *locale* +> `"C"`), los nombres salen en **inglés**: `Thursday`, `February`. Para que salgan en español +> (`jueves`, `febrero`) hay que **configurar el locale** con `setlocale(LC_TIME, "es_ES.UTF-8")` (o el +> que exista en la máquina). Y ojo: **no todos los sistemas tienen el mismo locale instalado**, así que +> los nombres traducidos pueden no aparecer. Para texto **estable**, usá números (`%Y-%m-%d`) y vos +> armás los nombres si los necesitás. + +## 5.2 Devolver el texto: el `const char *` prestado + +Escribir en un búfer ajeno (como hace `date_format`) está bien, pero es **verboso**: declarás el +arreglo, le pasás el tamaño, te acordás de que existe. A veces querés lo cómodo: **que la función te +devuelva el texto y listo**, para meterlo directo en un `printf`. + +```c +printf("Hoy es %s\n", date_to_string(today, "%d/%m/%Y")); +``` + +Para que eso funcione, `date_to_string` tiene que devolver un `const char *`. Y acá hay una pregunta +que en C **no se puede esquivar**: ¿**dónde vive** ese texto? La función retorna, su marco de pila se +destruye... y un puntero a una variable local quedaría apuntando a **tierra arrasada** (el *dangling +pointer* del capítulo `07`). + +La respuesta del C clásico es un **búfer estático interno**: una variable local declarada `static`, que +**no** vive en la pila sino que se crea una sola vez y **sobrevive** a todas las llamadas. + +```c +#include + +/* Ilustrativo: el patrón del "búfer prestado". No es la solución de un ejercicio. */ +const char *greeting(void) { + static char buf[32]; /* NO está en la pila: vive TODO el programa */ + snprintf(buf, sizeof buf, "hola"); + return buf; /* válido: 'buf' sigue vivo al volver */ +} +``` + +> **Pensalo como el pizarrón del aula.** El aula (programa) tiene **un solo** pizarrón (`static char +> buf`). Cualquiera lo usa y se va; el pizarrón queda. Cuando otra función (`ctime`, `localtime`, +> `date_to_string`) escribe en **su** pizarrón, **borra** lo que había antes. Si vos "te guardaste" el +> contenido anterior **copiándolo a tu carpeta** (tu propio arreglo), perfecto; si solo **apuntaste al +> pizarrón**, vas a leer lo nuevo, no lo viejo. + +Ese es el **contrato de "puntero prestado"**: la función te da una dirección válida, pero el contenido +es **de ella** y puede cambiarlo cuando quiera. Consecuencias, todas clásicas: + +```text +p1 = date_to_string(d1, "%Y"); /* p1 apunta al búfer interno → "2024" */ +p2 = date_to_string(d2, "%Y"); /* la MISMA dirección; el búfer ahora dice "2025" */ +printf("%s %s\n", p1, p2); /* sale: "2025 2025" ← ¡p1 también cambió! */ +``` + +- **No es *thread-safe*.** Dos hilos compartirían el mismo búfer y se pisarían entre sí. +- **No es reentrante.** No la podés llamar desde dentro de un `printf` que ya la está usando... bueno, + técnicamente sí compila, pero el resultado es impredecible. +- **El resultado dura hasta la próxima llamada.** Si lo necesitás más tiempo, **copialo**. + +> **No es una rareza: es exactamente lo que hace ``.** `ctime`, `asctime`, `localtime` y +> `gmtime` devuelven punteros a búferes internos (a veces **compartidos** entre varias de ellas). Por eso +> "guardar el `char *` de `localtime` y después llamarla de nuevo" es el bug clásico del horario que +> "se cambia solo". Tu `date_to_string` hereda el mismo contrato: **el texto es prestado**. + +> **¿Cuándo usar cada uno?** Regla práctica: **prestado** (`date_to_string`) para *usar y tirar* —un +> `printf`, una comparación al vuelo—; **tu búfer** (`date_format`) cuando necesités guardar el texto o +> tener varios vivos a la vez; **arena** (capítulo `12`) cuando sean muchos o de tamaño variable. Y si +> de verdad necesitás varios "prestados" simultáneos en un programa concurrente, existe el +> almacenamiento *thread-local* (`_Thread_local`), que es tema para más adelante. + +> **No te olvides del tope.** El búfer *static* tiene un tamaño **fijo** (como el de `greeting`). Si el +> texto formateado no entra, se **trunca** (o `strftime` devuelve `0` y no escribió nada). Un `%B` con +> un nombre de mes largo, más un `%A`, más hora, pueden pasarse. Elegí el tope con margen y **tratá el +> caso "no entró"**. + +## 5.3 Parsear: del texto a la fecha + +Parsear es lo inverso de formatear. Tenés el texto `"2024-02-29"` y querés una `Date`. Dos caminos: + +1. **`sscanf`** (capítulo `14`): el estándar, seguro y **portable**. Le indicás el formato con `%d` y + separadores literales: + + ```c + int y, m, d; + if (sscanf("2024-02-29", "%d-%d-%d", &y, &m, &d) == 3) { + /* los tres se leyeron bien */ + } + ``` + +2. **`strptime`** (POSIX): inversa de `strftime`, pero **no estándar** de ISO C. Existe en Linux y + macOS; en Windows suele requerir un reemplazo (`strptime` como función propia o `sscanf`). + +```text + "2024-02-29" ──parse(" %d-%d-%d")──► { 2024, 2, 29 } ──date_is_valid──► OK + "2023-02-29" ──parse(" %d-%d-%d")──► { 2023, 2, 29 } ──date_is_valid──► INVÁLIDA +``` + +> **Parsear NO es validar.** `sscanf` te da tres números; **no** sabe si el `2-29` existe. Podés parsear +> `"2023-02-29"` sin problema: los tres enteros entran. La **validación** (`date_is_valid`, Parte 1) es +> un paso **aparte** y **obligatorio**. Confundirlos es meter una fecha imposible en tu sistema. + +> **El infierno de las fechas ambiguas.** `"01/02/2024"` puede ser el **1 de febrero** o el **2 de +> enero**, según el país. Nunca adivines: **imponé un formato** (por ejemplo, el ISO `AAAA-MM-DD`, que +> ordena alfabéticamente y no se presta a confusión) y rechazá lo que no lo cumpla. + +## Práctica de la Parte 5 + +### Ejercicio 19.13 — Formatear en tu búfer (`date_format`) + +**Conceptos** · `strftime`, búfer acotado, armar un `struct tm` desde `Date`, valor de retorno + +**Contexto** · Mostrar fechas en reportes, logs y carteles. Simplificación: formatear una `Date` con una +plantilla. + +**Consigna** · Implementá `date_format(Date d, const char *fmt, char *out, size_t cap)`: convertí `d` a +un `struct tm` (cuidando las trampas; podés llamar a `mktime` para que se completen `tm_wday`/`tm_yday`), +llamá a `strftime` con la plantilla, y devolvé cuántos bytes se escribieron (`0` si no entró o hubo +error). Comprobá varias plantillas sobre la misma fecha. + +**Entrada** · Una `Date`, una plantilla y un búfer. +**Salida** · El texto formateado. + +**Escenario de prueba** + +```text +fecha = 2024-02-29 + fmt "%Y-%m-%d" → "2024-02-29" + fmt "%d/%m/%Y" → "29/02/2024" + fmt "%j" → "060" + fmt "%Y-%m-%d (%A)" → "2024-02-29 (Thursday)" (locale "C": en inglés) + fmt "%F %T" → "2024-02-29 00:00:00" +``` + +> **Sobre `%A`:** sale `Thursday` porque el locale por defecto es `"C"`. Si configurás `setlocale`, puede +> salir en español; si tu máquina no tiene el locale, no aparece. Por eso el escenario usa el inglés. + +**Identificadores sugeridos** · `date_format`, `t`, `written` +**Criterios de aceptación** +- [ ] Se arma el `struct tm` con `year - 1900` y `month - 1`. +- [ ] `strftime` recibe el **tamaño** del búfer (`cap`), no un número inventado. +- [ ] Devuelve cuántos bytes se escribieron (`0` si no entró). +- [ ] Las cinco plantillas del escenario dan lo esperado. + +--- + +### Ejercicio 19.14 — Devolver el texto (`date_to_string`) + +**Conceptos** · `static` (duración de almacenamiento), puntero prestado, `strftime`, `const char *` + +**Contexto** · Para imprimir una fecha formateada al vuelo querés escribir +`printf("%s\n", date_to_string(d, "%d/%m/%Y"))` sin declarar un arreglo. Simplificación: una variante +cómoda de `date_format` que devuelve un `const char *` en vez de escribir en un búfer tuyo. + +**Consigna** · Implementá `date_to_string(Date d, const char *fmt)`: convertí `d` a un `struct tm` +(cuidando las trampas), formatealo con `strftime` en un **búfer `static` interno** de tamaño fijo, y +devolvé el puntero a ese búfer. Si el texto **no entra** o hay error, devolvé un puntero a cadena vacía +(`""`), nunca `NULL`. Después, **demostrá en un mismo `main`** las consecuencias del contrato prestado: + +1. dos llamadas **seguidas** y tu `printf` mostrando que el primer puntero "cambió" (apunta al mismo + búfer que el segundo); +2. la salida de `date_format` **no** sufre ese efecto (dos búferes tuyos, dos resultados distintos). + +**Entrada** · Una `Date` y una plantilla. +**Salida** · Un `const char *` al texto formateado (válido hasta la próxima llamada). + +**Escenario de prueba** + +```text +date_to_string(2024-02-29, "%Y-%m-%d") → "2024-02-29" +date_to_string(2024-02-29, "%d/%m/%Y") → "29/02/2024" + +demostración del "se pisa": + const char *a = date_to_string(f1, "%Y"); /* a apunta al búfer interno */ + const char *b = date_to_string(f2, "%Y"); /* la MISMA dirección */ + printf("%s | %s\n", a, b); → "2025 | 2025" (¡a "cambió"!) + /* con date_format y dos búferes propios: */ + → "2024 | 2025" (bien) +``` + +> **Ojo con el tope del búfer.** Elegí un tamaño fijo con margen (por ejemplo `64`) y **tratá** el +> caso "no entró": `strftime` devuelve `0` y no escribió. Este es el precio del búfer *static*: un +> tamaño que no podés adaptar a la plantilla. + +**Identificadores sugeridos** · `date_to_string`, `buf`, `t`, `n` +**Restricciones / pistas** +- El búfer debe ser `static` **dentro** de la función; si fuera un arreglo local común, devolverías un + *dangling pointer* (capítulo `07`). +- No uses `malloc` (capítulo `09`): acá el contrato es "prestado", sin reserva dinámica. +- Documentá en un comentario, junto a la función, que el puntero **se pisa en la próxima llamada**. +- Diferenciá bien: `date_format` = **tu** búfer; `date_to_string` = **búfer interno prestado**. + +**Criterios de aceptación** +- [ ] El búfer es `static char` dentro de la función (no un local común). +- [ ] Se arma el `struct tm` con `year - 1900` y `month - 1`. +- [ ] Si `strftime` no entra (`0`), devuelve `""` (nunca `NULL` ni basura). +- [ ] Las dos primeras conversiones del escenario dan lo esperado. +- [ ] La demostración prueba que dos llamadas seguidas comparten el mismo búfer (el primero "se pisa"). +- [ ] La comparación con `date_format` (dos búferes propios) da resultados **distintos** y correctos. + +--- + +### Ejercicio 19.15 — Parsear con validación (`date_parse`) + +**Conceptos** · `sscanf`, valor de retorno, `date_is_valid`, formatos ambiguos + +**Contexto** · Leer fechas de un archivo (`fgets`, capítulo `15`) o de `argv` (capítulo `13`) y +convertirlas a `Date`, rechazando lo mal formado **y** lo inexistente. Simplificación: parser con +formato fijo ISO + validación. + +**Consigna** · Implementá `date_parse(const char *s, const char *fmt, Date *out)`: usá `sscanf` con el +formato, verificá que haya leído **los tres** valores (`== 3`), armá la `Date`, y **validala** con +`date_is_valid`. Devolvé `1` si todo salió bien (parseo **y** validación) y `0` si no. **No** modifiques +`*out` si falla. + +**Entrada** · Un texto, un formato y un puntero a `Date`. +**Salida** · `1`/`0`, y la `Date` llena si correspondía. + +**Escenario de prueba** + +```text +parse("2024-02-29", "%d-%d-%d") → 1, {2024, 2, 29} +parse("2023-02-29", "%d-%d-%d") → 0 (parsea, pero 2023-02-29 NO existe) +parse("2024-13-01", "%d-%d-%d") → 0 (mes 13) +parse("29/02/2024", "%d/%m/%Y") → 1, {2024, 2, 29} +parse("no es una fecha","%d-%d-%d") → 0 +parse("2024-02", "%d-%d-%d") → 0 (leyó solo dos) +``` + +> **El caso estrella:** `"2023-02-29"` **parsea bien** (tres enteros válidos) pero **valida mal**. Ese +> es exactamente el bug que este ejercicio previene: parsear no es validar. + +**Identificadores sugeridos** · `date_parse`, `y`, `m`, `d`, `tmp`, `n` +**Criterios de aceptación** +- [ ] Se comprueba que `sscanf` leyó **tres** valores. +- [ ] Después de parsear, se llama a `date_is_valid` (parsear no alcanza). +- [ ] `2023-02-29` da `0` (parsea pero se rechaza), y `2024-02-29` da `1`. +- [ ] Si falla, no se toca la `Date` de salida. + +--- + +# PARTE 6 — El catálogo completo: instantes, duraciones y el jefe + +Para cerrar, tres herramientas más (`difftime`, la duración entre instantes, y el manejo de la famosa +"bomba" de 2038) y un desafío final que integra **todo**. + +## 6.1 `difftime`: la resta de instantes + +`date_diff_days` te da **días** entre fechas. Cuando tenés **instantes** (`time_t`), la resta te da +**segundos**, y ahí entra `difftime`: + +```text + difftime(t2, t1) → la diferencia (t2 − t1) en segundos, como double +``` + +Es la forma **portable** de restar dos `time_t` (porque `time_t` puede no ser "segundos" en todas las +plataformas). Dividiendo, obtenés horas, días, etc. + +## 6.2 El fin de un `int` de 32 bits: el efecto 2038 + +Como en muchas plataformas `time_t` es un entero **con signo de 32 bits**, el máximo es `2147483647` +segundos, que corresponde a: + +```text + 2038-01-19 03:14:07 UTC → el último instante que entra en un time_t de 32 bits +``` + +Un segundo después, se **desborda** y (si sigue con signo) da un número **negativo**: la fecha salta a +**1901**. Ese es el **efecto 2038** (el "Y2K de Unix"). En las máquinas de 64 bits de hoy no es +problema (el rango se va a la estratósfera), pero en sistemas embebidos con `time_t` de 32 bits sigue +vivo. + +> **Cómo te protegés.** Usá `time_t` (deja que la plataforma elija su ancho) y **no** guardes el epoch en +> un `int`. Y cuando guardes en archivo, guardá `time_t`/64 bits, no un `int32`. + +## 6.3 El ejercicio jefe: el calendario + +Ahora juntá **todo**: años bisiestos, largos de mes, día de la semana y formato, para imprimir el +calendario de un mes como la vieja herramienta `cal`. + +```text + (ejemplo: febrero de 2024) + + febrero 2024 + do lu ma mi ju vi sa + 1 2 3 + 4 5 6 7 8 9 10 + 11 12 13 14 15 16 17 + 18 19 20 21 22 23 24 + 25 26 27 28 29 +``` + +## Práctica de la Parte 6 + +### Ejercicio 19.16 — El calendario de un mes (jefe) + +**Conceptos** · todo: `date_is_leap`, `date_days_in_month`, `date_day_of_week`, alineación con `printf` + +**Contexto** · `cal` es una de las herramientas más viejas de Unix y ejercita toda tu librería de una +sola pasada. Simplificación: imprimir el calendario de **un** mes de un año dado. + +**Consigna** · Escribí un programa que reciba un mes y un año (por `argv`, capítulo `13`, o fijos en el +`main`) e imprima el calendario de ese mes: + +1. el encabezado con el nombre del mes y el año; +2. la fila de días de la semana (por ejemplo `do lu ma mi ju vi sa`, con **domingo primero**, para + coincidir con `date_day_of_week`); +3. las semanas alineadas: la primera fila deja tantos huecos como indique el día de la semana del día + 1; cada número ocupa **dos columnas** (usá `%2d`), y las columnas se separan con un espacio; +4. un `\n` al final de cada semana; +5. validá el mes/año con tu librería antes de imprimir. + +Después, subí la apuesta: un `main` que imprima **los doce meses** del año en columnas (tres por fila), +como `cal 2024`. + +**Entrada** · Un mes (`1–12`) y un año. +**Salida** · El calendario del mes, alineado. + +**Escenario de prueba** + +```text +entrada: mes = 2, año = 2024 + + febrero 2024 + do lu ma mi ju vi sa + 1 2 3 + 4 5 6 7 8 9 10 + 11 12 13 14 15 16 17 + 18 19 20 21 22 23 24 + 25 26 27 28 29 +``` + +```text +entrada: mes = 9, año = 2026 (1 de septiembre de 2026 cae martes) + + septiembre 2026 + do lu ma mi ju vi sa + 1 2 3 4 5 6 7 + 8 9 10 11 12 13 14 + 15 16 17 18 19 20 21 + 22 23 24 25 26 27 28 + 29 30 +``` + +> **Cómo comprobar la alineación:** el día 1 de febrero de 2024 fue **jueves**. Con domingo = 0, jueves +> = 4, así que la primera fila lleva **4** huecos antes del `1`. Si tu calendario arranca el `1` en otra +> columna, revisá `date_day_of_week`. + +**Identificadores sugeridos** · `print_month`, `print_year`, `year`, `month`, `dow`, `day` +**Restricciones/pistas** +- Reusá **toda** la librería: `date_is_valid`, `date_days_in_month`, `date_day_of_week`, y `date_from...` + si te sirve para armar cada día. +- La alineación sale del `date_day_of_week(Date{year, month, 1})`: esa cantidad de **huecos** va antes + del primer número. +- Usá `%2d` para cada día (dos columnas) y un separador de **un** espacio entre columnas. +- Nombres de meses y días: un arreglo de cadenas en español (no dependas del locale, que puede no + existir). +- Validá mes (`1..12`) y año razonable; si no, avisá por `stderr` y no imprimas basura. + +**Criterios de aceptación** +- [ ] El 1 del mes cae en la columna correcta (según `date_day_of_week`). +- [ ] Febrero de un bisiesto muestra **29** días; de un no bisiesto, **28**. +- [ ] Cada número ocupa **dos** columnas y las semanas se cierran con `\n`. +- [ ] Los dos escenarios (feb 2024 y sep 2026) salen **idénticos** al ejemplo. +- [ ] Reusa las funciones de la librería (no repite la regla del bisiesto ni la del día de semana). +- [ ] Un mes o año inválido se reporta sin imprimir un calendario roto. +- [ ] (Extra) El año completo imprime los doce meses en columnas. + +--- + +## 19.17 Errores comunes + +- **Olvidar el bisiesto de los 400.** `1900` **no** es bisiesto, `2000` **sí**. "Cada 4 años" es + incorrecto. +- **La regla del 100 al revés.** Un año divisible por 100 **no** es bisiesto, **salvo** que sea divisible + por 400. No lo resumas a "divisible por 4". +- **`tm_year` sin el `-1900`/`+1900`.** `struct tm` guarda **años desde 1900**: `2026` es `126`. +- **`tm_mon` o `tm_yday` 0-based.** Enero es `0` (no `1`); el 1 de enero es `tm_yday == 0` (no `1`). +- **Módulo de negativos sin blindar.** `-1 % 7` da `-1` en C, no `6`. Usá `((x % 7) + 7) % 7` para el + día de la semana. +- **Creer que `time_t` es siempre "segundos".** El estándar **no** lo garantiza; en la práctica sí, pero + usá `difftime` para restar, no una resta cruda. +- **Mezclar local con UTC.** Guardá y calculá en **UTC**; convertí a local **solo al mostrar**. Guardar + "hora local sin huso" en un archivo es un bug esperando a mudarse de país. +- **Ignorar el DST.** La hora local puede **repetirse** o **no existir** el día del cambio de horario. + `tm_isdst = -1` y dejá que el sistema decida. +- **Parsear sin validar.** `"2023-02-29"` se parsea bien y **no** existe. Validá siempre después. +- **Confiar en los nombres de mes/día del locale.** Sin configurar `setlocale`, salen en inglés; y no + todos los sistemas tienen el locale en español. Para texto estable, usá números. +- **Asumir un ancho de `time_t`.** No guardes epoch en un `int` (efecto 2038). Dejá que `time_t` mande. +- **Escribir a `data[-1]` al armar días.** Al iterar meses/días, cuidá los límites (mes `1..12`, día + `1..n`): no leas "un día antes del 1". +- **Devolver un puntero a un local.** Si `date_to_string` usara un arreglo local **común** en vez de + `static`, devolverías un *dangling pointer* (capítulo `07`): memoria que ya no es tuya. +- **Creer que el `const char *` prestado es tuyo.** El de `date_to_string` (y el de `ctime`, + `asctime`, `localtime`) se **pisa** en la próxima llamada. Guardalo (copialo) si lo necesitás después; + no lo liberes con `free`. + +--- + +## 19.18 Cierre + +Si tuvieras que quedarte con **una sola idea**, que sea esta: **una fecha es una etiqueta irregular; +convertila a un número de día y todo se vuelve sumas y restas**. + +- Dejaste de pelear con meses y bisiestos al hacer aritmética: **linealizás** la fecha a un número de + día, operás con enteros, y **la volvés** a fecha. Sumar, restar, comparar y ubicar el día de la + semana salen de ahí. +- Aprendiste a **validar** antes de calcular: no toda etiqueta existe (`2023-02-29` no), y para saberlo + necesitás la regla **completa** del bisiesto (4 / 100 / 400). +- Separaste **dos mundos**: tu `Date` 1-based, sin husos, para la lógica de calendario; y + `time_t`/`struct tm` para hablar con el reloj del sistema, cuidando sus **tres trampas**. +- Entendiste que el tiempo se **guarda en UTC** y se **muestra en local**, y por qué eso evita la mitad + de los bugs de fechas del mundo. +- Distinguiste los **dos contratos de formateo**: `date_format` escribe en tu búfer (seguro); el + `const char *` de `date_to_string` es **prestado** (cómodo, pero se pisa) — el mismo trato de `ctime` + y `localtime`. +- Escribiste una **librería completa**: validación, linealización, aritmética, epoch, formato, parseo y + un impresor de calendarios. + +**Resumen de la caja de herramientas.** + +| Quiero… | Función | Idea en una frase | +|---|---|---| +| ¿Año bisiesto? | `date_is_leap` | Divisible por 4, no por 100, salvo por 400. | +| Largo del mes | `date_days_in_month` | Tabla + febrero según bisiesto. | +| ¿Fecha real? | `date_is_valid` | Mes `1..12` y día `1..días_del_mes`. | +| Linealizar | `date_to_day_number` | Fecha → número de día (origen 1970 = 0). | +| Vuelta atrás | `date_from_day_number` | Número de día → fecha (inversa de la anterior). | +| Sumar/restar días | `date_add_days` | `from(to(fecha) + n)`. | +| Días entre fechas | `date_diff_days` | Resta de números de día. | +| Comparar | `date_compare` | `-1`, `0`, `1`. | +| Día de la semana | `date_day_of_week` | `((día + 4) % 7 + 7) % 7` (dom = 0). | +| Día del año | `date_day_of_year` | Meses anteriores + día (`1..366`). | +| Instante absoluto | `date_to_epoch` | `struct tm` (local) → `mktime` → `time_t`. | +| Ahora | `date_today` / `date_now_local` | `time(NULL)` + `localtime`. | +| Mostrar (tu búfer) | `date_format` | `strftime` con plantilla, escribís en tu arreglo. | +| Mostrar (prestado) | `date_to_string` | `strftime` en búfer `static`; se pisa en la próxima llamada. | +| Leer | `date_parse` | `sscanf` **+** `date_is_valid`. | +| Duración entre instantes | `difftime` | Resta de `time_t` en segundos. | + +> **El puente a lo que sigue.** Ahora las fechas dejaron de ser un dolor. Con lo mismo (linealizar, +> operar, volver) podés resolver horarios, agendas, edades, plazos y reportes por rango de fechas. Y si +> algún día guardás miles de eventos, la combinación de este capítulo con el `18` (cadenas) y el `12` +> (arenas) te deja construir un sistema de turnos completo **sin reservar memoria de más** y sin +> sorpresas de huso horario. El calendario, ese edificio de cuartos irregulares, ahora lo recorrés vos. diff --git a/dist/19-fechas.pdf b/dist/19-fechas.pdf new file mode 100644 index 0000000..c53f686 Binary files /dev/null and b/dist/19-fechas.pdf differ