Hace unas semanas me encontré con una tarea en el trabajo que consistía en validar campos de formularios con JavaScript. Ya ya, existe el atributo pattern de HTML que hace esta tarea mucho más fácil, pero en este caso era, más que un requisito, una exigencia del cliente. No quería validarlo con estos atributos. Prefería hacerlo con JavaScript, así que hubo que remangarse. En ese momento pensé que ¡¡la validación de formularios con JavaScript siempre vuelve!!

Uno de las validaciones más costosos fue la de la fecha de nacimiento. Siguiendo la recomendación de un montón de artículos que leí (¡¡mentira!!, fue el diseñador quien lo propuso así) para introducir la fecha de nacimiento, es mejor dividir cada elemento de la fecha en los tres elementos que forman dicha fecha: día, mes y año. Así que tampoco podía usar un elemento de entrada tipo date. Así que, desde este punto, creé un componente HTML con tres elementos HTML select para cada elemento de la fecha.

Imagen del componente fecha de nacimiento

Con esto en mente, para validarlo, debía obtener el valor de los tres elementos y formar una fecha, formato cadena, y que esta tuviera sentido. Comprobar que esa fecha se corresponde con una fecha real del pasado.

Explicado así, no parecía una tarea demasiado compleja. Pues resultó ser algo muuuucho más coñazo. Me encontré con varios problemas a la hora de validar la fecha y quiero dejar aquí escrito ese testimonio. ¿Quereís saber cuales fueron? Pues vamos allá.

Validación con Date Object e isNaN

La primera opción que contemplé para la validación es la de crear una fecha instanciando un objeto con la clase Date nativa de JavaScript. Para ello, recibiendo una cadena de texto, creaba una fecha y comprobaba que es un número con isNaN. Podemos ver una comprobación tal que así:

isNaN(new Date('2018-02-28').getTime())

isNaN es una función que intenta convertir el parámetro pasado a un número. En caso de no poder convertirlo nos devuelve true. Por otro lado, la función o método getTime() de la clase Date nos devuelve un número, exactamente nos devuelve el número de milisegundos desde la fecha 01/01/1970

Con la comprobación anterior nos debe devolver false. Hasta aquí todo bien, al menos aparentemente. Al probarlo en los distintos navegadores nos devuelve el mismo valor. Pero ¿y si probamos con un valor no real, por ejemplo el 31 de febrero de cualquier año? ¿qué ocurrirá?

isNaN(new Date('2018-02-31').getTime())

Pues aquí dependemos de como el navegador interpreta dicha fecha. Mientras que la sentencia anterior Firefox nos devuelve true (recordad que en este caso no es un número) tanto Chrome como Edge nos dice que si es un número. ¿Y cuál es la diferencia entre ellos? Principalmente tanto Chrome como Edge nos dicen que la fecha 2018-02-31 en cadena de texto, es realmente el 3 de marzo de ese mismo año en milisegundos. Tal cual.

Pues bien, algo que parecía bastante trivial, como es validar si una fecha es correcta, ya comienza a dar sus problemas. Podemos ver que si probamos a instanciar simplemente la fecha en consola:

new Date('2018-02-31')

Firefox nos da un resultado, en este caso Invalid Date, y Chrome y Edge otro distinto, por ejemplo:

[date] Sat Mar 03 2018 01:00:00 GMT+0100 (Hora estándar romance): 

Es más, no solo ocurre con los años bisiestos (leap year en inglés). Podemos probar con otras opciones como:

new Date('2018-11-31')

Curiosamente no ocurre lo mismo cuando desbordamos el año. En el caso de incluir un 32 de diciembre todos los navegadores probados devuelven un Invalid Date. Ejemplo:

new Date('2018-12-32')

Cambiando el formato de fecha

Vale, en este punto, leo en algún post o blog (No tengo la referencia. Si la encuentro la incluiré en el artículo) que también influye el formato de la fecha pasado al instanciar Date. Comentan que se use el formato siguiente:

new Date('31 February 2018')

Este formato no es nada práctico. En este caso hasta Firefox nos comenta que es una fecha válida. Al igual que este otro:

new Date('2018/02/31')

Esto comienza a provocar dolor. ¿Por qué ocurre esto? ¿no existe un estándard sobre las fechas? ¿por qué no lo implementan de igual manera los navegadores? ¿debemos usar un polyfill para una solución cross-browser? ¡¡Voy a mirar en internet a ver que dicen!!

El artículo A Bad Date With Internet Explorer 11: Trouble With New Unicode Characters in Javascript Date Strings me da algunas pistas de lo que ocurre. Junto con uno de documentación más básica Date and Time in JavaScript tengo información bastante buena al respecto, pero no la solución al problema. Toca seguir buscando una solución, porque pienso, esto debe haberle pasado a alguien antes que a mi. Digo yo.

Usar una librería de terceros: Moment.js

Bien, me pongo a invertigar si existe una librería para esto. Todo el mundo habla de Moment.js. Da un montón de posibilidades y parece que funciona correctamente, pero considero que el tamaño que tiene para la simple validación de una fecha es demasiado grande, incluso no usando el locale. Para este contexto, me ofrece un herramienta potente para algo demasiado pequeño. De momento no queda descartada, pero prefiero buscar una solución más liviana.

Stack Overflow al rescate. ¡Larga vida a Stack Overflow!

En este caso encuentro una solución que me funciona correctamente. Un script que cumple con los requisitos deseados: nos dice si un año es bisiesto o no, asigna el número de días que tiene cada mes, lo cual es perfecto para este asunto y además es lo suficientemente liviano en tamaño:

function daysInMonth(m, y) { // m is 0 indexed: 0-11
    switch (m) {
        case 1 :
            return (y % 4 == 0 && y % 100) || y % 400 == 0 ? 29 : 28;
        case 8 : case 3 : case 5 : case 10 :
            return 30;
        default :
            return 31
    }
}

function isValid(d, m, y) {
    return m >= 0 && m < 12 && d > 0 && d <= daysInMonth(m, y);
}

Y el dolor acabó, por lo menos para este componente. A ver si el siguiente me da menos problemas.