1406 lines
64 KiB
Markdown
1406 lines
64 KiB
Markdown
# 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.
|