# 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.