Análisis de cadena de fecha de Javascript

Una de mis funciones favoritas de PHP es el strtotime función. Esta función le permite pasar una cadena de fecha y devolver una marca de tiempo de Unix

    $time = strtotime('2021-04-01');
    echo date('c',$time),"\n";
    // outputs
    // 2021-04-01T00:00:00-07:00

Lo bueno de esto es que funciona con una variedad de formatos de fecha.

    $time = strtotime('04/01/2021');
    echo date('c',$time),"\n";
    // outputs
    // 2021-04-01T00:00:00-07:00

Y no se preocupe:si son todos objetos todo el tiempo, el mismo comportamiento de análisis de cadenas funciona con DateTime de PHP. clase.

    $date = new DateTime('April 1, 2020');
    echo $date->format('c'),"\n";
    // outputs
    // 2020-04-01T00:00:00-07:00

Con strtotime si está trabajando con datos incompletos (en otras palabras, datos reales), tiene un poco más de confianza en que su código seguirá funcionando cuando/si su código encuentra un formato de fecha inesperado.

Fecha.parse de Javascript

Javascript tiene una funcionalidad similar integrada en su Date objeto. Desafortunadamente, hay algunos casos extraños en torno a las zonas horarias que lo hacen poco confiable. Todos los siguientes ejemplos usan un REPL de Node.js 14.2, pero deberían aplicarse en general a las versiones modernas de javascript.

En javascript, puede usar el Date.parse para analizar automáticamente una cadena de fecha y recuperar una marca de tiempo de Unix, o puede pasar una cadena directamente al Date función constructora del objeto.

    $ node
    Welcome to Node.js v14.2.0.
    Type ".help" for more information.
    > Date.parse('April 1, 2021')
    1617260400000
    > new Date('April 1, 2021')
    2021-04-01T07:00:00.000Z

Inmediatamente vemos algunas pequeñas diferencias con respecto a strtotime . Primero, javascript informa sus marcas de tiempo de época de Unix en milisegundos, no en segundos. En segundo lugar, el formato de fecha ISO de javascript (el 'c' formato en PHP stringToTime ) siempre informa utilizando la hora UTC (indicada por el Z final) ), donde PHP informa el desplazamiento de la zona horaria desde UTC. Así que estas dos cadenas de fecha ISO

2021-04-01T00:00:00-07:00
2021-04-01T07:00:00.000Z

representan la misma hora.

Nota :Todos los ejemplos de este artículo se ejecutaron en una computadora configurada para la hora de la costa oeste de EE. UU. durante el horario de verano; es posible que vea una diferencia de siete horas según cuándo y dónde ejecute los ejemplos de código.

Hasta ahora, estas son diferencias importantes, pero pequeñas. La mayor diferencia surge cuando comienza a usar cadenas de fecha que parecen ser parte de una cadena de fecha ISO 8601

    > new Date('2021-04-01')
    2021-04-01T00:00:00.000Z

Verás que, como antes, javascript usa un Z para indicar que la fecha está en hora UTC. Sin embargo, también notará que la hora no 07:00:00 -- es 00:00:00 . En nuestros ejemplos anteriores, javascript asume una hora de medianoche usando el actual zona horaria configurada. Sin embargo, cuando usamos 2021-04-01 como una cadena de fecha, javascript asumió una hora de medianoche con una zona horaria UTC. Porque 2021-04-01 parece una fecha ISO 8601 incompleta, javascript asumió que era una fecha ISO 8601 con una zona horaria faltante y la zona horaria predeterminada es UTC.

Si no lo sabe, este comportamiento puede causar errores en su programa. Me encontré con esto cuando estaba procesando algunos archivos CSV de los bancos. Algunas transacciones aparecieron en el día equivocado porque un archivo CSV usaba YYYY-MM-DD formato y otro usó el MM/DD/YYYY formato.

Este no es el único problema con el análisis de cadenas en el Date clase. La documentación de MDN sobre el formato de cadena de fecha y hora de javascript cubre algunos otros casos extremos que podrían interesarle.

Bibliotecas de fechas

El Date object es uno de los objetos originales de javascript y no es probable que cambie su comportamiento. Si algún proveedor de javascript "arregló" esto para que fuera más consistente, es casi seguro que rompería una gran cantidad de código en el mundo. Debido a esto, la mayoría de los programadores de JavaScript confían en una biblioteca de terceros para manejar las fechas.

Veamos cuatro bibliotecas populares de manejo de fechas (date-fns , dayjs , luxon y moment ) y vea cómo manejan YYYY-MM-DD caso. Los siguientes ejemplos suponen que tiene estas bibliotecas de fechas instaladas a través de npm.

$ npm install date-fns dayjs luxon moment

Momento

La biblioteca de momentos es una de las bibliotecas de fechas más populares para javascript, incluso si sus desarrolladores se han alejado de ella y la consideran "terminada". Veamos cómo maneja cadenas de fecha ISO abreviadas.

    > moment= require('moment')
    //...
    > moment('2021-04-01')
    Moment<2021-04-01T00:00:00-07:00>

¡Éxito! A diferencia del Date nativo objeto, el momento no asume una zona horaria UTC. En su lugar, asume la zona horaria del sistema configurada actualmente.

Sin embargo, sucederá algo interesante si tratamos de analizar una cadena de fecha que no tiene formato ISO.

    > moment('04/01/2021')
    Deprecation warning: value provided is not in a recognized RFC2822 or
    ISO format. moment construction falls back to js Date(), which is not
    reliable across all browsers and versions. Non RFC2822/ISO date formats
    are discouraged.

    Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.
    /* ... */
    Moment<2021-04-01T00:00:00-07:00>

El moment la función aún devuelve una fecha, pero recibimos una advertencia de que nuestra fecha está en un formato que ese momento no reconoce, y ese momento está recurriendo al uso de javascript integrado en Date . Entonces, aunque obtuvimos la respuesta que queríamos para nuestra fecha ISO 8601 (Moment<2021-04-01T00:00:00-07:00> ), es posible que no tengamos tanta suerte si usáramos una versión diferente de javascript o un formato de cadena que no estuviera basado en ISO 8601.

Luxón

La biblioteca de fechas de luxon (creada por uno de los mantenedores de moment) tiene un enfoque diferente.

Luxon puede manejar una variedad de formatos de fecha, pero no intenta detectar automáticamente qué formato es cuál.

    const {DateTime} = require('luxon')

    DateTime.fromISO(...)
    DateTime.fromRFC2822(...)
    DateTime.fromSQL(...)
    DateTime.fromMillis(...)
    DateTime.fromSeconds(...)
    DateTime.fromJsDate(...)

La filosofía de Luxon es que depende de usted, el programador usuario final, saber con qué tipo de fechas está tratando. Si llama a uno de estos métodos con un formato de fecha no válido, Luxon devolverá un DateTime objeto, pero ese objeto se considerará inválido

    > DateTime.fromISO('04/01/2021')
    DateTime {
      /* ... */
      invalid: Invalid {
        reason: 'unparsable',
        explanation: `the input "04/01/2021" can't be parsed as ISO 8601`
      },
      /* ... */
    }

Día.js

El siguiente es Day.js, una biblioteca que se enorgullece de su pequeño tamaño y una API similar a Moment.js.

Day.js parece capaz de analizar una variedad de formatos de fecha, y no queda atrapado en el problema ISO 8601 UTC.

    > const dayjs = require('dayjs')
    undefined
    > dayjs('2021-04-01')
    d {
      /* ... */
      '$d': 2021-04-01T07:00:00.000Z,
      /* ... */
    }
    > dayjs('4/01/2021')
    d {
      /* ... */
      '$d': 2021-04-01T07:00:00.000Z,
      /* ... */
    }

Sin embargo, su página de documentos contiene esta vaga advertencia.

Esto sugiere que, detrás de escena, Day.js está realizando una validación y análisis de datos adicionales, pero en última instancia solo usa un Date objeto para su análisis. Dado que Day.js es de código abierto, podemos echar un vistazo entre bastidores y confirmar que esto es cierto.

Esto significa que si está utilizando Day.js y desea un análisis coherente de las fechas que no son ISO, deberá usar su CustomParseFormat enchufar. El complemento le permite definir un formato de cadena que analizará una cadena de fecha específica.

    > const dayjs = require('dayjs')
    /* ... */
    > const customParseFormat = require('dayjs/plugin/customParseFormat')
    /* ... */
    > dayjs.extend(customParseFormat)
    /* ... */
    > dayjs('04/01/2021', 'MM/DD/YYYY')
    d {
      /* ... */
      '$d': 2021-04-01T07:00:00.000Z,
      /* ... */
    }

Si su fecha tiene un formato conocido y usa uno de los tokens de análisis de Day.js, estará en buena forma.

fecha-fns

La última biblioteca de fechas que veremos es date-fns , que se describe a sí mismo como

El date-fns La biblioteca se enorgullece de su tamaño, con más de 200 funciones en su GitHub README. Cuando se trata del análisis de fechas, date-fns tiene un parseISO función que es explícitamente para analizar cadenas de fecha ISO completas y parciales.

    > const datefns = require('date-fns')
    //...
    > datefns.parseISO('2021-04-01')
    2021-04-01T07:00:00.000Z

Al igual que las otras soluciones basadas en bibliotecas, esta función utilizará la zona horaria actual si no se proporciona una.

Si tu cita es no una cadena similar a ISO, datefns proporciona una solución basada en cadenas de formato a través de parse método. Similar a Day.js, el parse El método te permite decirle a datefns cómo debe analizar una cadena de fecha.

    > foo = datefns.parse('04/01/2021','MM/dd/yyyy', (new Date))
    2021-04-01T07:00:00.000Z

Ese tercer parámetro requerido es un Date objeto -- según los documentos, parse usará este objeto para

Lo que esto significa en la práctica lo dejaremos como ejercicio para el lector; para el caso general, esto significa pasar un nuevo Date instancia.

Otra cosa a tener en cuenta aquí:esos tokens de formato no son los mismos tokens que se usan en otras bibliotecas.

Cambio de responsabilidad

Como puede ver, hay una variedad de bibliotecas y enfoques disponibles para que un desarrollador de javascript evite el comportamiento predeterminado no ideal del Date de javascript. objeto. Sin embargo, también puede haber notado que ninguna de sus bibliotecas intenta resolver el problema del análisis de cadenas de fecha genéricas. En su lugar, ofrecen al programador-usuario final una variedad de opciones para manejar cadenas de fechas, pero es responsabilidad del programador del cliente identificar qué formato están usando sus fechas.

Dicho de otra manera, si tiene un archivo CSV bancario que incluye fechas en el formato

04/01/2021

escribirá una cadena de formato para analizar este formato de fecha específico, o analizará usted mismo su cadena de fecha en sus partes de mes/día/año. Si tiene una fuente de datos donde el formato de fecha varía, estará escribiendo código para identificar qué formato es ese.

Esto encaja con la tendencia general del código fuente abierto durante los últimos 5 a 10 años. La mayoría de las veces, los creadores y mantenedores de bibliotecas de software intentan limitar el alcance de lo que hace el código que publican en el mundo para limitar el alcance de lo que necesitan respaldar en el futuro.

¿Portar strtotime?

Después de hacer toda esta investigación, tenía una última pregunta:¿por qué no simplemente transferir strtotime? a otros idiomas? Fui a buscar y encontré dos cosas que vale la pena mencionar.

Primero, la implementación de strtotime es un estudio de libro de texto sobre por qué el código C de otras personas no es donde usted quiere pasar el tiempo. Puede ver las agallas de la lógica de implementación aquí. Este no es un código C estándar, es un código para un sistema llamado re2c. Este sistema le permite escribir expresiones regulares en un DSL personalizado (lenguaje específico de dominio) y luego transformar/compilar esas expresiones regulares en programas C (también C++ y Go) que ejecutarán esas expresiones regulares. Algo en el archivo make de PHP usa este parse_date.re archivo para generar parse_date.c . Si no te das cuenta parse_date.c es un archivo generado, esto puede ser extremadamente difícil. Si no está familiarizado con re2c Puede ser regular y rudo. Dejamos la exploración adicional como un ejercicio para el lector, un ejercicio que no hemos realizado nosotros mismos.

Por lo tanto, migrar esta función no es una tarea sencilla, pero hay un paquete de código abierto impulsado por la comunidad llamado locutus que lo está intentando. En sus propias palabras

Este paquete incluye una implementación de strtotime de PHP función. Si bien no es un puerto directo del re2c Expresiones regulares de PHP, parece manejar los formatos de fecha que hemos usado en este artículo. Un programa como este

    const strtotime = require('locutus/php/datetime/strtotime')
    console.log(new Date(strtotime('April 1, 2021') * 1000))
    console.log(new Date(strtotime('4/1/2021') * 1000))
    console.log(new Date(strtotime('2021-04-01') * 1000))

da como resultado una salida como esta

2021-04-01T07:00:00.000Z
2021-04-01T07:00:00.000Z
2021-04-01T07:00:00.000Z

Fechas idénticas, creadas con una fecha de medianoche en la zona horaria local, representada como una fecha UTC.