Requisitos del diseño de la clase calendar

Aquí se expone con más detalle como se ha diseñado este calendario. Para hacer pruebas ponemos un calendario que usamos en el tema anterior:

Ejemplo:




Formato:

Los requisitos de diseño para este calendario son los siguientes:

  • Objeto que almacena una fecha (día, mes y año) y que muestra en pantalla una clásica hoja de calendario de un mes y año dado.
  • El rango de años que cubre el calendario debe ser el máximo posible.
  • Ejecución sólo en el navegador del cliente con JavaScript, por lo que el servidor no participa en esa ejecución.
  • Presenta los días festivos nacionales y autonómicos de uso más frecuente con un estilo diferenciado. No se consideran las fiestas nacionales que se sustituyen o las que se trasladan de domingo a lunes. Avisa al usuario de esto.
  • Presenta el jueves y viernes santo como festivo.
  • La fecha actual aparece con un estilo diferenciado.
  • El usuario puede seleccionar un día del mes para almacenarlo en el objeto.
  • El usuario tiene facilidades para moverse a otra hoja del calendario.
  • Puede recoger una fecha y almacenarla así como devolver la fecha almacenada en varios formatos.
  • El programador que use el objeto tiene facilidades para cambiar el estilo de los días marcados (festivos, actual y seleccionado).

En base a estos requisitos se diseña la clase. Pero antes hay 2 problemas previos que hay que resolver:

  1. ¿Cómo se construye una hoja de calendario de un mes y año cualquiera?
  2. ¿Cómo se calcula cuando caen los jueves y viernes santos de cada año?

A continuación explicaremos esto mediante el uso de algunas funciones que están también en el archivo calendar.js pero que son funciones globales de la clase calendar. Esto quiere decir que no depende de cierta instancia de objeto sino que se aplica globalmente a la clase.

Si lo desea puede consultar el código de calendar.js completo.

Cómo se construye una hoja de calendario de un mes y año cualquiera

Para hacer una hoja cualquiera de un mes dado del calendario lo que tenemos que saber es que día de la semana le correspondía al día 1 de ese mes y año. De eso se encarga la siguiente función que se invoca como buscaDiaSemana(1, mes, año).

//Los días de la semana:
//  0 = lunes, 1 = martes, 2 = miércoles, 3 = jueves,
//  4 = viernes, 5 = sábado, 6 = domingo
//El 01/01/1601 fue lunes
var ANYO_INICIO = 1601
//Limitado por el cálculo de semana santa
var ANYO_FIN = 2299
//domingo 31/12/1600, día previo al 01/01/1601 que fue lunes
var DIA_INICIO = 6
//rectifica posición primer día en cada mes
var RECTIF = Array(0,3,3,6,1,4,6,2,5,0,3,5);  	
	
function buscaDiaSemana(dia, mes, anyo) {
    //Días del mes más día de la semana de inicio
    var diferDias = dia + DIA_INICIO;
    //Rectificar por desplazamiento de los días en los meses
    diferDias += RECTIF[mes - 1];
    //Rectificar por el desplazamiento de un dia por año
    diferDias += (anyo - ANYO_INICIO);
    //Rectificar por años bisiestos
    diferDias += parseInt((anyo - ANYO_INICIO) / 4);
    //Deducir los años divisibles por 100 que no lo sean
    por 400 no bisiestos
    var j = parseInt((anyo - ANYO_INICIO) / 100);
    var k = parseInt((anyo - ANYO_INICIO) / 400);
    diferDias -= (j-k);
    //Rectificar si el año actual es bisiesto
    if ((esBisiesto(anyo))&&(mes>2)) diferDias++;
    //Día de la semana a buscar
    diferDias = diferDias % 7;
    return diferDias;
}	
	

Si especificamos que el primer día de la semana es el lunes podemos darle el índice cero y al domingo el índice 6 y así tener la comodidad cuando usemos Array, aunque también hay otro motivo que explicaremos más abajo. Hemos de buscar un año cualquiera donde sepamos que el 31 de diciembre cayó en domingo, con lo que su orden de día será 6. Así el 1 de enero del año siguiente tendrá número de orden 0 en la semana por ser lunes. Una fecha así es el 1/1/1601. Decalaramos DIA_INICIO como una constante igual a 6, que es ese domingo 31/12/1600. Y de paso ya tenemos el rango inferior para los años que nos piden.

En la variable diferDias vamos acumulando todos los días que hay desde ese día 31/12/1600, día 6 de la semana, hasta el día de la fecha en la que vamos a presentar la hoja del calendario, es decir, hasta el día 1/mes/año. Veámos estas operaciones una a una:

  • diferDias = dia + DIA_INICIO;

    El desplazamiento de dias se acumula desde la posición 6 del domingo 31/12/1600 más el día del mes donde se va a buscar el día de la semana. Recordemos que los días de la semana son índices (0,1,2,3,4,5,6) para lunes a domingo. Por ejemplo, el día 17/1/1601 cayó un miércoles porqué 17+6=23 y esto en partes de 7 días por semana son 23/7=3 semanas y queda un resto de 2. Así que este índice en (0,1,2,3,4,5,6) le corresponde al tercer día de la semana, el miércoles. Realmente se trata de aplicar el operador módulo que nos da el resto de la división entera entre 7 días de la semana. Cualquier número entero dividido por 7 tiene un resto entero que debe ser alguno de los números del conjunto {0, 1, 2, 3, 4, 5, 6}. Por eso conviene aplicar la base del índice cero a los días de la semana.

  • diferDias += RECTIF[mes - 1];

    Cómo todos los meses no tienen los mismos días, disponemos de otra constante RECTIF = Array(0,3,3,6,1,4,6,2,5,0,3,5); donde tenemos el desplazamiento de los días de la semana en cada mes, suponiendo que empezamos el año un lunes y que febrero tiene 28 días. Los array de JavaScript suelen tener índice base cero, por lo que hacemos RECTIF[mes-1] dado que los meses los disponemos en el rango [1..12]. De todas formas tenemos la función construyeRECTIF() que, aunque no la usamos pues es más rápido tener la constante ya fijada, nos sirve para hallar los términos de ese array. Si llamamos a esa función con un script que está a continuación, podemos obtener esa misma lista: . Puede ver los detalles en el código (he puesto una copia de esa función en el script de la cabecera de esta página).

  • diferDias += (anyo - ANYO_INICIO);

    Es fácil ver que para los años no bisiestos, que tienen 365 días, la operación 365 MOD 7 nos da 1, es decir, 365/7=52 semanas y nos sobra un resto de 1 día. Por lo tanto este resto es lo que se va desplazando en cada año.

  • diferDias += parseInt((anyo - ANYO_INICIO) / 4);
    var j = parseInt((anyo - ANYO_INICIO) / 100);
    var k = parseInt((anyo - ANYO_INICIO) / 400);
    diferDias -= (j-k);

    Agregamos un día por cada año bisiesto sin contar el año en que se está haciendo el calendario, que se considera en el siguiente paso. Los años bisiestos (con 29 días en febrero) son los divisibles por 4, excepto el último de cada siglo (aquel divisible por 100), salvo que este último sea divisible por 400. En definitiva agregamos los divisibles por 4, deducimos los divisibles por 100 y volvemos a agregar los divisibles por 400.

  • if ((esBisiesto(anyo))&&(mes>2)) diferDias++;

    Si el año en el que estamos construyendo el calendario es bisiesto y estamos en un mes posterior a febrero, sumamos un día.

  • diferDias = diferDias % 7;

    Por último hallamos el resto de la división entre 7 y el valor será un número entre 0 (lunes) a 6 (domingo).

Cómo se calcula cuando caen los Jueves y Viernes Santos de cada año

La Semana Santa no cae siempre en las mismas fechas, pero si existe una norma establecida para hallar cuando cae. Buscando información en Internet encontré en una página de Wikipedia una extensa descripción del algoritmo de Gauss para calcular cuando cae el Domingo de Pascua de un año dado. En esa página de Wikipedia también hay un enlace a varias implementaciones para distintos lenguajes de programación, aunque ninguna para JavaScript (al menos en junio 2010). Así que hay que implementarlo para JavaScript partiendo de las fórmulas de Gauss, pero antes se debe realizar un esquema previo del algoritmo, donde Año es el año dado y PASCUA es el domingo a buscar:

  • a = Año mod 19
  • b = Año mod 4
  • c = Año mod 7
  • d = (19a + f) mod 30
  • e = (2b + 4c + 6d + g) mod 7
  • Siendo f, g unas constantes de la tabla siguiente
    • 1583 - 1699 --> f=22 g=2
    • 1700 - 1799 --> f=23 g=3
    • 1800 - 1899 --> f=23 g=4
    • 1900 - 2099 --> f=24 g=5
    • 2100 - 2199 --> f=24 g=6
    • 2200 - 2299 --> f=25 g=0
  • Si d + e < 10 --> PASCUA = (d + e + 22) Marzo sino PASCUA = (d + e - 9) Abril
  • Tener en cuenta las excepciones:
    • Si PASCUA == 26 de Abril --> PASCUA = 19 de Abril
    • Si PASCUA == 25 de Abril, d==28, e==6, a>10 --> PASCUA = 18 Abril

El código no tiene más complicación que la señalada, implementándolo en la función calculaPascua(anyo) que nos devuelve un Array(dia, mes) con el día y el mes (marzo o abril) del Domingo de Pascua. Pero como necesitamos el Jueves y Viernes Santo para el calendario, a la hora de dar estilo de día festivo a éstos hemos de restar días al domingo. Se debe tener en cuenta si estamos en los límites entre Marzo y Abril, pues alguna semana santa puede caer en una que comparte ambos meses como la del año 1994. Este código se ejecuta en el método construyeCalendario() para saber que día y mes cae el Jueves y el Viernes Santo partiendo del Domingo de Pascua que nos da esa función calculaPascua(anyo):

var juevesSanto = 999
var mesJuevesSanto = 999
var viernesSanto = 999
var mesViernesSanto = 999
if ((this.mes == 3)||(this.mes == 4)) {
    var pasc = calculaPascua(this.anyo)
    if ((pasc[0] > 0)&&(pasc[1] > 0)) {
        juevesSanto = pasc[0] - 3
        mesJuevesSanto = pasc[1]
        if (juevesSanto < 1) {
            juevesSanto = 31 + juevesSanto
            mesJuevesSanto = 3
        }
        viernesSanto = pasc[0] - 2
        mesViernesSanto = pasc[1]
        if (viernesSanto < 1){
            viernesSanto = 31 + viernesSanto
            mesViernesSanto = 3
        }

    }
}

    

La tabla de Gauss nos da un límite superior al rango de fechas permitido por el calendario, se trata del año 2299. Así que el rango permitido será de 1601-2299.

Un problema con this en los objetos de JavaScript

En el tema anterior hacíamos referencia a un problema con la palabra reservada this de JavaScript, cuando hablabamos del argumento nombreInstancia en el constructor

var x = new calendar(nombreInstancia,
    dondeTabla[, dondeTitulo][, fecha][, conSeleccionDia])

tal que debíamos pasar el string "x" igual que el nombre de la variable del calendario creado en el argumento nombreInstancia.

El caso es que necesitamos que las celdas del calendario, que son elementos <td> construidos dinámicamente, dispongan de un evento onclick para que el usuario pueda seleccionar un día del mes. Se trata de adjudicar el método del calendario seleccionarDia(celda):

this.seleccionarDia = function seleccionarDia(celda){
    if (this.tablaConstruida){
        this.borrarSeleccionDia();
        this.dia = parseInt(celda.innerText);
        celda.style.cssText += "; " + this.estiloDiaSeleccionado;
    }
}
    

En este método el argumento apunta a un determinado elemento <td> que contiene el día a seleccionar. Así luego dentro de ese método podemos obtener el día con celda.innerText por ejemplo, llamar a otro método para borrar la selección anterior y dar un valor a una propiedad. Como tenemos que construir una cadena HTML para luego pegarla con innerHTML, podemos usar varias técnicas pero algunas pueden darnos problemas. Para entenderlo mejor es recomendable leer el artículo sobre este tema donde expongo esas técnicas y los pormenores de cada una.

En resumen podemos decir que si usamos this dentro de una function y luego creamos un objeto con new, JavaScript entiende que esa función será una clase y por lo tanto deberá crear un objeto, por lo que ese this se refiere al propio objeto. Por otro lado si tratamos eventos en una función (de objetos o no) obtenemos automáticamente un argumento que hace referencia al elemento que causó ese evento. Podemos acceder a ese elemento con window.event.srcElement o bien usando esa misma palabra this pero en otro contexto.

Pero que pasa si estamos dentro de una clase y lo que queremos es el this de la clase y no el del elemento. En lo que yo he visto, un método de una clase que viene como respuesta a la ejecución de un evento trae el argumento this como referencia a ese elemento que causó el evento. Y no encuentro manera de poder referirme a la clase.

Como solución paso el nombre de la variable para luego obtener una referencia a al objeto calendario instanciado, porque la verdad tampoco se como obtenerlo directamente desde dentro de la clase. Entonces los eventos se ponen literalmente en la cadena de texto donde se construye el HTML de cada celda (elementos <td> de la tabla construida dinámicamente):

" onclick = \"" + this.nombre + ".seleccionarDia(this);\" "
    

Así en this.nombre está el valor recogido del argumento nombreInstancia y realmente lo anterior quedará, una vez insertado con innerHTML así onclick = "calendario4.seleccionarDia(this);" (siendo calendario4 el nombre del objeto instanciado). El this del método seleccionarDia(this) si es ahora el que se refiere al propio elemento <td> donde se produce el evento. Además la clase no puede detectarlo porque está dentro de un literal de texto.

Otra ventaja adicional de tener el nombre de la instancia dentro del objeto es que podemos ofrecer mensajes de error y mostrar que nombre de variable lo ha producido. Quizás haya alguna forma en JavaScript para obtener este nombre, pero mientras tanto usaré esta técnica.

Un problema con referencias al DOM en objetos JavaScript e Internet Explorer

En Junio 2010 implementé la clase calendar y en el mes siguiente subí toda esta documentación a este sitio. Luego estaba trabajando en otros objetos de JavaScript: los mensajes emergentes. Se trata de construir ventanas emergentes con HTML dinámico y JavaScript. Quise hacer un ejemplo de aplicación ubicando un objeto calendario dentro de un objeto emergente. Pero surgió un problema no previsto en relación con referencias a elementos HTML que se pasan a un objeto. Se manifiesta en Internet Explorer 8 y aunque en Firefox 3.6, Opera 10.6 y Safari 4.0 no parece evidenciarse, aún no se el alcance que puede tener, por lo que ese problema me hizo reconsiderar el constructor del calendario que inicialmente era:

new calendar(elDia, elMes, elAnyo, dondeTabla,
dondeTitulo[, conSeleccionDia, nombreInstancia])

Aquí se pasaban los argumentos dondeTabla y dondeTitulo como referencias a elementos HTML, por supuesto, después de que la página se hubiera cargado. Esas referencias servían luego dentro del objeto para modificar el DOM mediante el uso de innerHTML creando o reemplazando elementos. El caso es que al hacer esto se perdía la referencia a aquellos elementos. Mejor dicho, seguía manteniendo la referencia al elemento HTML pero desvinculado del DOM. Reconozco que soy un aprendiz en todo esto, pero dado que no halle otra solución, me decidí por cambiar el constructor así:

new calendar(nombreInstancia, dondeTabla
[, dondeTitulo][, fecha][, conSeleccionDia])

Ahora dondeTabla y dondeTitulo siguen siendo argumentos del constructor, pero se pasan como tipos string con los identificadores id de los elementos que referenciábamos. Luego dentro del objeto hacemos document.getElementById() para obtener la referencia actualizada del elemento.

He escrito un breve artículo con ejemplos exponiendo este caso sobre problemas al pasar referencias al DOM en un objeto de JavaScript con Internet Explorer. También hay otro sobre paso de argumentos a funciones de JScript y VBScript.