La aritmética de fechas es más difícil de lo que parece
Publicado el 6/6/2025 · 17 min de lectura · Calculadoras del día a día
Lena Hoffmann — Redactora de Ciencia y Educación en Allin
Matemáticas · Física
Verificado con 5 fuentes
Pregunta cuánto es el 31 de enero más un mes y no hay respuesta que las matemáticas te impongan. La mayoría de los sistemas recortan al final del mes más corto y devuelven el 28 de febrero de 2026 — o el 29 de febrero en un año bisiesto como 2024. La aritmética ingenua en días devuelve otra cosa: sumar 31 días da el 3 de marzo, sumar 30 días da el 2 de marzo. Las tres son defendibles, y ahí está el problema. Ese recorte tiene una consecuencia que casi nadie nota: sumar meses no es invertible ni asociativo. El 31 de marzo menos un mes es el 28 de febrero, y volver a sumar un mes da el 28 de marzo, no el 31 de marzo. El 31 de enero más un mes más un mes es el 28 de marzo, pero el 31 de enero más dos meses es el 31 de marzo. La aritmética horaria se rompe en otra dirección. En Europe/Paris, el 29 de marzo de 2026 dura 23 horas y el 25 de octubre dura 25, así que sumar 86 400 segundos a una cita de las 9:00 del 28 de marzo aterriza a las 10:00 del día siguiente. Y la edad es una comparación de calendario, no una división: sobre todos los cumpleaños de 1930 a 2020, días ÷ 365 da la edad equivocada el 3,49 % de las veces. La regla que lo resuelve todo: haz la aritmética de calendario en campos de calendario, la de instantes en UTC, y nunca las mezcles.
«Un mes después» no tiene una única respuesta correcta, y cada biblioteca de fechas ha tenido que elegir una. Sumar meses no es asociativo ni invertible, un día no siempre dura 24 horas y la edad no son los días divididos entre 365,25.
No hay aritmética que imponga una respuesta
Sumar uno a un número no tiene ambigüedad. Sumar un mes a una fecha sí, porque los meses no son una unidad — son etiquetas de longitud desigual, de 28 a 31 días, y la longitud de aquel en el que aterrizas depende de aquel del que partiste. El 31 de enero más un mes tiene que aterrizar en algún punto de febrero, y febrero no tiene día 31. Algo tiene que ceder. La elección casi universal es recortar: conservar el mes, conservar el año y retroceder el día hasta el último válido. Eso da el 28 de febrero de 2026 y el 29 de febrero de 2024. java.time en Java, la API Temporal de ECMAScript, la suma de intervalos de PostgreSQL y dateutil en Python se comportan todos así, y lo hacen porque la alternativa es peor.
La alternativa es tratar un mes como un número fijo de días. Elige 30 y el 31 de enero más un mes pasa a ser el 2 de marzo de 2026; elige 31 y pasa a ser el 3 de marzo. Ambas se saltan febrero entero, que es justo lo que no quiere el usuario que pide «un mes después». La tabla de arriba aplica las tres definiciones a cinco fechas de partida, y solo coinciden cuando el día del mes es lo bastante pequeño como para existir en todas partes. Esa es la lección práctica: todos los métodos son idénticos del 1 al 28, y todos difieren en algún punto los días 29, 30 y 31. Alrededor de una fecha de cada diez está en la zona de peligro, y por eso el fallo sobrevive tan fácilmente a las pruebas.
Recortar te cuesta la asociatividad y la invertibilidad
Ejecuta esto y mira desaparecer una propiedad que dabas por sentada. El 31 de marzo de 2026 menos un mes se recorta al 28 de febrero. Vuelve a sumar un mes y obtienes el 28 de marzo — tres días antes de donde partiste. Sumar meses no es invertible: restar y luego sumar no es la identidad. El mismo defecto aparece como fallo de la asociatividad. El 31 de enero de 2026 más un mes más un mes es el 28 de marzo, porque el valor intermedio se recortó al 28 de febrero y el recorte es permanente. El 31 de enero más dos meses, calculado de una vez, es el 31 de marzo. Dos expresiones que deberían valer lo mismo difieren en tres días.
Esto no es un fallo de ninguna biblioteca concreta — es una consecuencia del calendario, y toda biblioteca que recorta lo hereda. La regla práctica que se deriva merece una nota adhesiva: nunca construyas una serie mensual sumando un mes al resultado anterior. Suma siempre n meses a la fecha de anclaje original. Una suscripción que empieza el 31 de enero y se renueva por iteración derivará al día 28 y se quedará ahí para siempre; la misma suscripción anclada al 31 de enero y calculada como inicio + n meses cae el 28 de febrero, el 31 de marzo, el 30 de abril, el 31 de mayo — que es lo que el cliente espera y lo que la pasarela de pago cobrará. El fallo es invisible once meses al año y luego llega de golpe.
Un día no son 24 horas — Europe/Paris, marzo y octubre de 2026
Toma las reglas reales de la base tz en lugar de una suposición. En 2026 Europe/Paris pasa de UTC+1 a UTC+2 el 29 de marzo y vuelve el 25 de octubre. Mide la distancia entre la medianoche local del 29 de marzo y la medianoche local del 30 de marzo: es de 2026-03-28T23:00Z a 2026-03-29T22:00Z, o sea 23 horas. Haz lo mismo alrededor del 25 de octubre y obtienes de 2026-10-24T22:00Z a 2026-10-25T23:00Z, o sea 25 horas. El día del calendario y el día de 86 400 segundos son objetos distintos, y dos veces al año se separan de forma visible. America/New_York hace lo mismo en otras fechas — 23 horas el 8 de marzo de 2026 y 25 horas el 1 de noviembre.
La consecuencia cae sobre citas reales. Una franja de las 9:00 del 28 de marzo de 2026 en París es el instante 2026-03-28T08:00Z. Suma exactamente 24 horas transcurridas y obtienes 2026-03-29T08:00Z, que en París se lee como las 10:00 — la reunión se ha movido una hora más tarde. Hazlo en octubre y se mueve una hora antes: las 9:00 del 24 de octubre más 24 horas son las 8:00 del 25 de octubre. Ninguna de las dos es lo que significa «a la misma hora mañana». «A la misma hora mañana» es una operación de calendario: incrementar el campo fecha, conservar el campo de reloj de pared y volver a resolver el instante contra la zona. Sumar una duración es una operación de física. Coinciden 363 días al año, que es justo lo suficiente para que el fallo parezca aleatorio.
El desbordamiento silencioso: el 30 de febrero no lanza ningún error
En JavaScript, new Date(2026, 1, 30) no lanza excepción. Devuelve el 2 de marzo de 2026. El constructor acepta cualquier entero y normaliza arrastrando el exceso al mes siguiente, así que un campo de día 30 en un febrero de 28 días se convierte calladamente en el día 2 del mes siguiente. La misma normalización convierte el índice de mes 12 en enero del año siguiente y un día 0 en el último día del mes anterior — que es el truco tras el modismo habitual para «días de este mes», new Date(y, m, 0).getDate(). Es un comportamiento útil cuando lo quieres y una corrupción silenciosa de datos cuando no, y nada en el valor devuelto te dice en cuál de los dos casos estás.
Por eso validar una fecha es más que comprobar que el analizador no se ha quejado. Un formulario que acepta 30/02/2026 y guarda 2026-03-02 ha perdido el error del usuario en lugar de señalarlo, y el registro dice ahora algo que el usuario nunca escribió. La comprobación defensiva cabe en una línea: construye la fecha y luego verifica que el año, el mes y el día que recibes son los tres que pusiste. Si no lo son, la entrada no era una fecha real. La otra mitad de la defensa es conocer la longitud de cada mes antes siquiera de construir la entrada — 31, 28 o 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 — que es lo que la calculadora de días del mes responde para el año que indiques.
La edad es una comparación, no una división
El atajo tentador es contar los días transcurridos y dividir entre 365,25, con el argumento de que el año medio dura 365,25 días. Ponlo a prueba. Alguien nacido el 23 de agosto de 2000, consultado el 22 de agosto de 2026, ha vivido 9 495 días. Divide entre 365 y obtienes 26,0137, cuya parte entera es 26 — pero la persona tiene 25 y su cumpleaños es mañana. Divide entre 365,25 y obtienes 25,9959, cuya parte entera es 25, y ahí acierta. Así que recorre todo el espacio en lugar de un ejemplo: cada fecha de nacimiento del 1 de enero de 1930 al 1 de enero de 2020, evaluada el 22 de agosto de 2026, son 32 873 fechas. Días ÷ 365 da la edad equivocada en 1 148 de ellas, un 3,49 %. Días ÷ 365,25 va mucho mejor pero aún falla en 44, un 0,13 %.
Los fallos residuales son de la peor clase: caen justo en el cumpleaños. Alguien nacido el 22 de agosto de 1932 ha vivido 34 333 días al 22 de agosto de 2026, y 34 333 ÷ 365,25 = 93,9986, así que la división dice 93 la mañana en que cumple 94. Ningún afinado del divisor arregla esto, porque ningún divisor único puede hacerlo, y esa es toda la cuestión — el calendario no es una escala uniforme. El algoritmo correcto no lleva ninguna división. Resta el año de nacimiento del año actual, y luego resta uno más si el mes y el día actuales aún no han alcanzado el mes y el día de nacimiento. Tres comparaciones de enteros, exactas en todas partes, y nunca necesita saber cuánto dura un año.
La regla: campos de calendario para el calendario, UTC para los instantes
Casi todos los fallos de fechas son uno de dos errores. O bien una pregunta de calendario se respondió con tiempo transcurrido — «un mes» convertido en 30 días, «mañana» en 86 400 segundos, «la edad» en una división — o bien una pregunta de instante se respondió en campos locales, de modo que una marca de tiempo almacenada se desplazó al cambiar el desfase. El remedio es decidir, antes de escribir una línea, qué tipo de magnitud manejas. Fechas de renovación, cumpleaños, plazos, periodos de facturación y horarios de apertura son magnitudes de calendario: guárdalas como año, mes, día y hora de reloj de pared con una zona nombrada, y haz la aritmética sobre esos campos. Tiempos de espera, orden de registros, caducidad de caché, límites de tasa y duraciones son magnitudes de instante: guárdalas como marcas de tiempo UTC y suma segundos.
Donde ambos deben encontrarse — un recordatorio a las 9:00 hora local, enviado por un servidor que solo entiende instantes — convierte en la frontera, y solo en la frontera. Haz primero el paso de calendario en la zona nombrada del usuario, resuelve el resultado a un instante UTC una sola vez y entrega ese instante al planificador. Guardar el desfase en lugar del nombre de la zona se rompe en cuanto cambian las reglas políticas, cosa que ocurre varias veces al año; la base tz publica versiones precisamente porque los gobiernos no dejan de mover sus transiciones. Y nunca guardes una cita local futura como un instante UTC solo, porque si las reglas de esa zona se modifican antes de que llegue la fecha, el instante que guardaste ya no corresponderá a las 9:00 de la mañana de nadie.
| Fecha de partida | + 1 mes, recortado | + 30 días | + 31 días |
|---|---|---|---|
| 31 de enero de 2026 | 28 de febrero de 2026 | 2 de marzo de 2026 | 3 de marzo de 2026 |
| 31 de enero de 2024 (año bisiesto) | 29 de febrero de 2024 | 1 de marzo de 2024 | 2 de marzo de 2024 |
| 31 de marzo de 2026 | 30 de abril de 2026 | 30 de abril de 2026 | 1 de mayo de 2026 |
| 31 de agosto de 2026 | 30 de septiembre de 2026 | 30 de septiembre de 2026 | 1 de octubre de 2026 |
| 30 de noviembre de 2026 | 30 de diciembre de 2026 | 30 de diciembre de 2026 | 31 de diciembre de 2026 |
Preguntas frecuentes
- ¿Cuánto es el 31 de enero más un mes?
- Depende de la definición que use tu sistema, y no hay respuesta que las matemáticas impongan. El recorte — el comportamiento de java.time, la API Temporal, los intervalos de PostgreSQL y dateutil en Python — conserva el mes y el año y retrocede el día hasta el último válido, dando el 28 de febrero de 2026 o el 29 de febrero de 2024. La aritmética de días fijos da otra cosa: más 30 días es el 2 de marzo de 2026 y más 31 días es el 3 de marzo. El recorte casi siempre es la elección correcta para fechas de cara al usuario, porque quien pide «un mes después» quiere la fecha correspondiente del mes siguiente, no un número fijo de días. Si facturas, contratas o planificas, indica en las condiciones qué regla usas, porque los clientes sí notan que una suscripción del 31 de enero se renueve el 28 de febrero.
- ¿Por qué restar un mes y volver a sumarlo no devuelve la fecha original?
- Porque el recorte destruye información y nada puede restaurarla. El 31 de marzo de 2026 menos un mes tiene que aterrizar en febrero, febrero no tiene día 31, así que el resultado se recorta al 28 de febrero. Ese resultado ya no recuerda que venía de un día 31. Sumar un mes al 28 de febrero da, por tanto, el 28 de marzo, y estás tres días antes de donde empezaste. El mismo mecanismo te cuesta la asociatividad: el 31 de enero más un mes más un mes es el 28 de marzo, mientras que el 31 de enero más dos meses de una vez es el 31 de marzo. La consecuencia práctica es una regla que conviene aplicar siempre: construye los calendarios recurrentes sumando n meses a la fecha de anclaje original, nunca iterando mes a mes desde el resultado anterior. La iteración deja que un solo recorte se propague para siempre.
- ¿Un día dura siempre 24 horas?
- No, no como día natural local. En Europe/Paris en 2026, el 29 de marzo dura 23 horas y el 25 de octubre dura 25, porque la zona pasa de UTC+1 a UTC+2 y vuelve. Medidos como instantes, la medianoche local del 29 de marzo es 2026-03-28T23:00Z y la del 30 de marzo es 2026-03-29T22:00Z — 23 horas de diferencia. America/New_York hace lo mismo el 8 de marzo y el 1 de noviembre de 2026. Algunas transiciones ni siquiera son de horas enteras; la isla Lord Howe se desplaza 30 minutos. Las zonas sin horario de verano tienen días de 24 horas todo el año, pero no puedes suponer que tus usuarios estén en una. La costumbre segura es tratar «un día» en sentido de calendario como un incremento del campo fecha, resuelto contra una zona nombrada, y reservar los 86 400 segundos para trabajo real de tiempo transcurrido, hecho en UTC.
- ¿Cómo debo calcular la edad de alguien?
- Con comparaciones, nunca con una división. Resta el año de nacimiento del año actual y luego resta uno más si el mes actual es anterior al mes de nacimiento, o si los meses coinciden y el día actual es anterior al día de nacimiento. Eso es exacto para cualquier fecha. Las divisiones fallan de forma medible: sobre todos los cumpleaños de 1930 a 2020 evaluados el 22 de agosto de 2026 — 32 873 fechas — días transcurridos ÷ 365 da la edad equivocada el 3,49 % de las veces y ÷ 365,25 el 0,13 %. Peor aún, los fallos residuales se concentran justo en el cumpleaños, el único día que la gente comprueba. Alguien nacido el 22 de agosto de 1932 ha vivido 34 333 días al 22 de agosto de 2026, y 34 333 ÷ 365,25 = 93,9986, así que la división informa 93 la mañana en que cumple 94. Los nacidos el 29 de febrero requieren una decisión de política aparte, porque las jurisdicciones difieren sobre si el cumpleaños legal en año común es el 28 de febrero o el 1 de marzo.
- ¿Por qué mi formulario acepta el 30 de febrero sin quejarse?
- Porque la mayoría de los constructores de fechas normalizan en lugar de validar. En JavaScript, new Date(2026, 1, 30) devuelve el 2 de marzo de 2026 sin error: el campo de día se desborda y el exceso se arrastra al mes siguiente. La misma regla convierte un índice de mes 12 en enero del año siguiente y un día 0 en el último día del mes anterior, y por eso new Date(y, m, 0).getDate() es el modismo habitual para la longitud de un mes. Nada en el valor devuelto distingue un desbordamiento deliberado de una errata. La defensa es una comprobación de ida y vuelta: construye la fecha y luego afirma que el año, el mes y el día que lees de vuelta son los tres que escribiste. Si difieren, rechaza la entrada. Hacerlo en la frontera sale mucho más barato que descubrir después que una columna de la base de datos contiene una fecha que nadie escribió nunca.
- ¿Debo guardar las fechas en UTC o en hora local?
- Depende de lo que signifique el valor, y responder «siempre UTC» causa tantos fallos como evita. Todo lo que registra cuándo ocurrió algo — una línea de registro, un pago, una caducidad de caché, una ventana de límite de tasa — es un instante, y los instantes van en UTC. Todo lo que registra cuándo debe ocurrir algo en el día de alguien — un recordatorio a las 9:00, una franja de entrega, el horario de una tienda, una reunión recurrente — es un valor de calendario, y guardarlo como instante UTC pelado es un fallo esperando un cambio de reglas. Los gobiernos modifican las reglas del horario de verano varias veces al año, y la base tz publica versiones para seguirlas; una cita futura congelada como instante se desviará de la hora de reloj prevista si se modifica su zona. Guarda esas como fecha local, hora local y nombre de zona IANA, y resuelve a instante solo en el momento de actuar.
Artículos que podrían interesarte
Todas las guías →Herramientas relacionadas
Fuentes
- IANA — Time Zone Database (tz database)
- ISO — ISO 8601-1:2019 — Date and time: representations for information interchange
- IETF — RFC 3339 — Date and Time on the Internet: Timestamps
- IETF — RFC 5545 — Internet Calendaring and Scheduling Core Object Specification (iCalendar)
- Ecma International — ECMAScript Temporal — calendar and duration arithmetic
¿Has detectado un error en este artículo?