Files
c-programming/book/19-fechas.md
T

1406 lines
64 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <stddef.h> /* size_t */
#include <time.h> /* 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 `<time.h>`, 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 <stdio.h>
#include <time.h>
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 <stdio.h>
#include <time.h>
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 <stdio.h>
/* 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 `<time.h>`.** `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.