“Los años bisiestos han sido un dolor de cabeza a lo largo de la historia. También para los sistemas informáticos y las bases de datos.
En este artículo comprendemos el origen de estos problemas y le damos un vistazo a bugs sobre fechas que aun siguen presentes en Oracle23c”.
Este año 2024 es año bisiesto.
SQL> SELECT LAST_DAY(TO_DATE(’01/02/2024′,’DD/MM/YYYY’)) FECHA FROM DUAL;
FECHA
—————————–
Jueves, 29 Febrero 2024
En principio, cada año divisible por cuatro lo es, pero a lo largo de la historia sucedieron muchos ajustes en las fechas, que dieron lugar a varios calendarios. Nuestro calendario estándar y el de la mayor parte del mundo es el calendario gregoriano.
SQL> show parameters calendar
NAME TYPE VALUE
————————— ———– ——————————
nls_calendar string Gregorian
Para entender cómo manejar fechas históricas en Oracle y comprender la adaptación de cómo se implementan los años bisiestos en la base de datos, he creado el siguiente PL/SQL que muestra por cada año en la historia si fue bisiesto o no.
declare
anno_juliano varchar2(50);
fecha_juliana date;
begin
dbms_output.enable(1000000);
for x in 0 .. 2100
loop
if x<>0 then
anno_juliano:=’01/02/’||x;
fecha_juliana:=last_day(to_date(anno_juliano,’DD/MM/SYYYY’));
dbms_output.put(to_char(fecha_juliana,’Day, DD MM SYYYY’));
if to_char(fecha_juliana,’DD’)=’28’
then
dbms_output.put_line(‘ – No es bisiesto’);
else
dbms_output.put_line(‘ – Es bisiesto’);
end if;
else
dbms_output.put (‘–> Saltamos el año 0 – No existe.’);
dbms_output.put_line(”);
end if;
end loop;
end;
/
Los años bisiestos fueron incluidos por Julio César en el año 46 a.C. para sincronizar los años con las estaciones, es decir, que los años coincidieran con el giro completo de la tierra alrededor del sol.
Antes del mandato de Julio César, se seguía el calendario romano. Éste contaba los días a partir de la fundación de la ciudad de Roma (“Ab Urbe condita”), los años tenían diez meses y éstos eran de 29 días para seguir la referencia de los ciclos lunares. Los años empezaban en marzo y cada dos años se añadía un mes extra llamado “mercedonius” para añadir los días pendientes de ajustar al giro del sol.
Sin embargo, Julio César conocía por el astrónomo Sisógenes de Alejandría que los egipcios intentaron, sin conseguirlo, reformar su calendario dos siglos antes. Habían calculado con bastante precisión que el año solar constaba de 365 días y 6 horas, y redactaron estos avances en un documento conocido como el Decreto de Canopo (grabado en piedra, por cierto, escrito en egipcio y griego, y expuesto en la actualidad en el Museo del Cairo, en Egipto).
Con lo cual estaba claro que, para hacer coincidir los meses con los años, éstos debían ser de 365 días “y cuarto”.
Pero si los egipcios no vieron claro cómo hacer el cambio, a Julio César tampoco le resultó fácil.
Para empezar, el descuadre que llevaban de bisiestos era importante.
Julio César tuvo que decretar un año con 445 días, al que se llamó año de la confusión (annus confusionis). Se trata del año 46 a.C., y se pretendía así compensar la gran acumulación de días pendientes de ajustar.
Por lo tanto, a partir de ese año 45 antes de Cristo (año primero del calendario juliano), los años deberían empezar en enero, tener 12 meses tal y como los conocemos, y cada cuatro años, el 23 de febrero se repetiría dos veces “bis sextus dies ante calendas martii” (“repetido el sexto día antes del primer día del mes de marzo”).
Pese a todo, el uso del año bisiesto fue bastante irregular durante los primeros años de la era juliana. Se conoce que los pontífices romanos aplicaron los bisiestos cada tres años y hay discrepancias sobre cuales años fueron realmente tratados como bisiestos o como años comunes. Fue Cesar Augusto quien estabilizó el año bisiesto cada cuatro años. De ahí que los meses quintilis (quinto), sextilisse (sexto) rebautizaran a julio y agosto, en homenaje a los césares y a esas mejoras que llevaron a cabo en el calendario.
Históricamente se considera que, a partir del año 4 d.C., disponemos de los años bisiestos correctamente desde entonces hasta la actualidad, pero veamos qué sucede durante esa época a ojos de la base de datos.
Modificando el PL/SQL un poco, cambiando el bucle FOR para que evalúe los años desde el 46 a.C. hasta el 46 d.C. y sólo visualice los bisiestos. El resultado es el siguiente:
Lunes , 29 Febrero -0044 – Es bisiesto
Sábado , 29 Febrero -0040 – Es bisiesto
Jueves , 29 Febrero -0036 – Es bisiesto
Martes , 29 Febrero -0032 – Es bisiesto
Domingo , 29 Febrero -0028 – Es bisiesto
Viernes , 29 Febrero -0024 – Es bisiesto
Miércoles, 29 Febrero -0020 – Es bisiesto
Lunes , 29 Febrero -0016 – Es bisiesto
Sábado , 29 Febrero -0012 – Es bisiesto
Jueves , 29 Febrero -0008 – Es bisiesto
Martes , 29 Febrero -0004 – Es bisiesto
–> Saltamos el año 0 – No existe.
Viernes , 29 Febrero 0004 – Es bisiesto
Miércoles, 29 Febrero 0008 – Es bisiesto
Lunes , 29 Febrero 0012 – Es bisiesto
Sábado , 29 Febrero 0016 – Es bisiesto
Jueves , 29 Febrero 0020 – Es bisiesto
Martes , 29 Febrero 0024 – Es bisiesto
Domingo , 29 Febrero 0028 – Es bisiesto
Viernes , 29 Febrero 0032 – Es bisiesto
Miércoles, 29 Febrero 0036 – Es bisiesto
Lunes , 29 Febrero 0040 – Es bisiesto
Sábado , 29 Febrero 0044 – Es bisiesto
Procedimiento PL/SQL terminado correctamente.
Aunque la salida muestra un año bisiesto cada cuatro, resulta bastante complicado, debido a un gran número de imprecisiones históricas, fechar correctamente los bisiestos en el inicio de la transición entre el calendario romano y el juliano. Si bien disponemos de los años bisiestos correctamente establecidos desde el año 4d.C. hasta la actualidad, para los años anteriores Oracle aplica una variante del calendario gregoriano para fechar el calendario juliano llamado calendario gregoriano proléptico.
Debido a este calendario gregoriano proléptico, no existe en la base de datos un “año de la confusión”, ni podemos insertar fechas correspondientes a los desajustes con los bisiestos hasta la época de Cesar Augusto.
Todo esto se recoge en la convención estándar de manejo de fechas de la ISO-8601:2004.
Gracias a ese estándar, vemos en la ejecución del PL/SQL ciclos de años bisiestos cada cuatro años hasta el 44a.C (inicio de la era juliana).
Aparentemente todo perfecto.
No obstante, podemos observar una anomalía importante en el tratamiento de los años bisiestos alrededor del año 0.
Repitiendo la ejecución del PL/SQL para los años comprendidos entre el -4 y el 4, obtenemos la siguiente relación de años bisiestos y comunes.
Martes , 29 Febrero -0004 – Es bisiesto
Miercoles, 28 Febrero -0003 – No es bisiesto
Jueves , 28 Febrero -0002 – No es bisiesto
Viernes , 28 Febrero -0001 – No es bisiesto
–> Saltamos el año 0 – No existe.
Lunes , 28 Febrero 0001 – No es bisiesto
Martes , 28 Febrero 0002 – No es bisiesto
Miercoles, 28 Febrero 0003 – No es bisiesto
Viernes , 29 Febrero 0004 – Es bisiesto
Procedimiento PL/SQL terminado correctamente.
Entonces, según el calendario gregoriano proléptico, existen seis años bisiestos seguidos, dado que el año cero no existe.
Pero tanto en los calendarios gregorianos y julianos prolépticos, el año -1 debería ser bisiesto, aunque con ello se rompería la regla general de la división por cuatro.
¿Qué está sucediendo en realidad?
Oracle adopta en esa implementación de la ISO-8601:2004 una mezcla entre el calendario gregoriano proléptico y la numeración astronómica. En realidad, internamente sí está gestionando un año cero.
Ese año cero no existe en los calendarios históricos, pero sí existe en el calendario que se usa en astrofísica y astronomía.
Este calendario astronómico tiene el nombre de “juliano astronónico”, maneja las fechas en un formato llamado “Fecha juliana”, que nada tiene que ver con el calendario Juliano de Julio César. La fecha juliana contabiliza los días desde el mediodía del 1 de enero de -4713 hasta la actualidad, incluyendo el año cero en el cómputo para manejar con una precisión de “cuarto de día” la posición de la tierra respecto al sol en fechas antes de Cristo.
Gracias a este calendario, Fred Espenak, astrofísico de la NASA, pudo predecir con gran precisión tanto los eclipses como las fases de la luna producidos desde los años -1999 hasta la actualidad, y los eclipses futuros hasta el año 3000.
Oracle permite consultar la fecha juliana para cada fecha desde la función TO_CHAR.
Basta con usar el formato de máscara ‘J’.
SQL> select to_char(sysdate,’J’);
TO_CHAR
——-
2460306 –> Días desde el 1 de enero de -4713
Aquí nos encontramos con un conflicto importante.
Es necesario mantener la precisión del calendario juliano astronómico para fechas anteriores al año cero y a la vez poder gestionar igualmente el calendario gregoriano proléptico.
Aquí el año cero supone un problema.
Si bien en Oracle no podemos manejar fechas en formato gregoriano para el año cero (obtenemos el error ORA-01841), este año sí existe en el cómputo de días juliano.
Y aquí empiezan los bugs.
Si consultamos el día 1 de enero del año 1, nos figura como sábado, pero el día anterior, el 31 de diciembre del año 1a.C. nos lo muestra como miércoles.
¿Cómo es eso posible? Pasamos de miércoles a sábado de un día para otro.
SQL> alter session set nls_date_format=’Day DD/MM/SYYYY –> J’;
Sesion modificada.
SQL> select to_date(’01/01/0001′,’DD-MM-SYYYY’);
TO_DATE(’01/01/0001′,’DD-MM-SYYYY
———————————
Sabado 01/01/ 0001 –> 1721424
SQL> select to_date(’01/01/0001′,’DD-MM-SYYYY’)-1;
TO_DATE(’01/01/0001′,’DD-MM-SYYYY
———————————
Miercoles 31/12/-0001 –> 1721057
Además, calculando con una SELECT los días que hay entre esas dos fechas, el día 1 de la era actual y un día antes, Oracle contabiliza 367 días. Es decir, 1 día más los 366 días de ese año bisiesto cero que no existe en el calendario gregoriano proléptico, pero sí en el astronómico.
SQL> select to_date(’01/01/0001′,’DD-MM-SYYYY’) – (to_date(’01/01/0001′,’DD-MM-SYYYY’)-1) from dual;
TO_DATE(’01/01/0001′,’DD-MM-SYYYY’)-(TO_DATE(’01/01/0001′,’DD-MM-SYYYY’)-1)
—————————————————————————
367
Por lo tanto, obtendremos este error lógico cuando Oracle intente invisibilizar ese año juliano astronómico cero en el manejo de fechas del calendario gregoriano.
Oracle Soporte advierte que manejar operaciones aritméticas que conmuten de años antes de cero y posteriores a cero, puede dar lugar a ciertas corrupciones lógicas, incorrecciones y bugs, aun presentes en Oracle23c.
Vamos a ver con más detalle uno de estos bugs de fechas manipulando la fecha “bisiesta” de este año cero invisible. Versión Oracle23c.
El 29 de febrero del año -1 no es contemplado como bisiesto en el calendario gregoriano proléptico, como hemos visto anteriormente. Si intentamos visualizar esa fecha, obtendremos el error de que no es válida para el mes especificado.
SQL> select to_date(’29/02/-0001′,’DD/MM/SYYYY’);
select to_date(’29/02/-0001′,’DD/MM/SYYYY’)
*
ERROR at line 1:
ORA-01839: date not valid for month specified
Help: https://docs.oracle.com/error-help/db/ora-01839/
No obstante, si restamos 307 días al 1 de enero del año 1 d.C. Oracle debería situarnos en el 28 de febrero del año 1 a.C., pues Oracle lo considera un año común no bisiesto, pero al consultarlo nos topamos con una fecha que nunca existió.
SQL> select to_date(’01/01/0001′,’DD-MM-SYYYY’)-307;
TO_DATE(’01/01/0001′,’
———————-
Saturday 29 Feb -0001
De algún modo es una fecha que sí debería existir, como he comentado anteriormente, pero incluso la fecha generada por to_date(’01/01/0001′,’DD-MM-SYYYY’)-307 ni siquiera es evaluable por TO_CHAR para visualizar el día de la semana, la fecha completa o a qué día corresponde en formato día juliano.
SQL> select to_char(to_date(’01/01/0001′,’DD-MM-SYYYY’)-307);
TO_CHAR(TO_DATE(’01/01/0001′,’
——————————
000000000, 00 0000000000 00000
SQL> select to_char(to_date(’01/01/0001′,’DD-MM-SYYYY’)-307,’J’);
TO_CHAR
——-
0000000
Es decir, el tratamiento de fechas a partir de literales nos indica que el 28 de febrero del año -1 es un viernes, y que el 1 de marzo del año -1 es un sábado, lo cual es correcto.
El error sucede al manejar operaciones sobre fechas que atraviesen ese año cero, porque supone una conversión contradictoria entre los dos calendarios.
Basta con realizar las consultas de esas fechas manejando literales simples, para que tanto para el cálculo de su día de la semana como su día en fecha juliana, ambos se visualicen correctamente.
SQL> select to_char(to_date(’28/02/-0001′,’DD/MM/SYYYY’),’Day, DD Month SYYYY ” dia juliano: ” J’);
TO_CHAR(TO_DATE(’28/02/-0001′,’DD/MM/SYYYY’),’DAY,DDM
—————————————————–
Viernes , 28 Febrero -0001 dia juliano: 1720751
SQL> select to_char(to_date(’01/03/-0001′,’DD/MM/SYYYY’),’Day, DD Month SYYYY ” dia juliano: ” J’);
TO_CHAR(TO_DATE(’01/03/-0001′,’DD/MM/SYYYY’),’DAY,DDMO
——————————————————
Sabado , 01 Marzo -0001 dia juliano: 1720752