Rrule: Confusión UTC-pero-no-UTC

Creado en 5 abr. 2019  ·  18Comentarios  ·  Fuente: jakubroztocil/rrule

Sospecho que esto no es un problema con rrule, sino con mi comprensión y / o su compatibilidad con la zona horaria. Ya leí secciones relacionadas en la documentación para rrule y luxon .

Muestra de código

const { RRule, RRuleSet } = require(`rrule`);

const dtstart = new Date();
console.log(`dtstart`, dtstart);

const firstHour = dtstart.getHours();
const secondHour = (firstHour + 2) % 24;

const getRRule = options => new RRule(Object.assign({}, options, {
    freq: RRule.DAILY,
    count: 1,
    dtstart,
    byminute: 0,
    bysecond: 0
}));

const showNextInstance = tzid => {
    const ruleSet = new RRuleSet();
    ruleSet.rrule(getRRule({ tzid, byhour: firstHour }));
    ruleSet.rrule(getRRule({ tzid, byhour: secondHour }));
    console.log(tzid, ruleSet.after(dtstart));
};

showNextInstance(undefined);
showNextInstance(`UTC`);
showNextInstance(`local`);
showNextInstance(`America/Chicago`);

Salida real

dtstart 2019-04-05T11:51:23.744Z
undefined 2019-04-06T06:00:00.744Z
UTC 2019-04-06T06:00:00.744Z
local 2019-04-06T06:00:00.744Z
America/Chicago 2019-04-06T06:00:00.744Z

Rendimiento esperado

Creo que la especificación de tzid tendría algún efecto en la salida, pero se genera la misma salida para cada zona horaria.

Tampoco puedo ver cuál es la correlación entre los tiempos proporcionados en las reglas y la salida para la siguiente instancia de evento. Esperaría que la siguiente instancia corresponda aproximadamente a una de las dos horas representadas por las reglas proporcionadas, dependiendo de cómo se interpreten o apliquen los valores dtstart y tzid especificados.

versión rrule

2.6.0

Sistema operativo

OS X 10.14.2, nodo 8.11.4

Zona horaria local

$ date
Fri Apr  5 06:32:33 CDT 2019

Comentario más útil

Creo que es fácil combinar los problemas con el objeto Date y los problemas para calcular las ocurrencias de RRULE , así que hablaré de ellos por separado antes de combinar los dos. Tuve que escribir esto principalmente para poder resolver mi comprensión del problema y pensé que publicaría aquí en caso de que ayude a otros.

Objeto de fecha de JavaScript

No estoy de acuerdo con que el objeto JS Date sea ​​irregular, pero existen malentendidos comunes con el objeto que hacen que parezca así. Es confuso por decir lo menos.

Como mencionó @ hlee5zebra , las fechas creadas con el constructor Date siempre se representan en UTC. Pueden mostrarse en la hora local, pero detrás de escena están representados en la época de Unix Epoch. Siempre podemos ejecutar .getTime() o .toISOString() en el objeto Date instanciado para recordarnos que estamos trabajando en UTC.

La implementación también sigue correctamente la convención de hacer todo el procesamiento en UTC y solo convertir a la hora local que se muestra al usuario. Da la casualidad de que nosotros, los desarrolladores, somos el usuario; pero luego tenemos en mente a nuestro propio usuario que enturbia el agua. Al desarrollar nuestras aplicaciones, debemos asegurarnos de seguir la convención y guardar las fechas como UTC en nuestro backend (JS ya procesa las fechas en UTC, por lo que estamos bien allí). Si almacena fechas como UTC en su almacén de datos, deberían llegar al cliente como cadenas ISO , ser analizadas correctamente por el objeto Date y mostrarse en la hora local del usuario, todo sin hacer ningún trabajo pesado.

Este artículo me ha ayudado a comprender el objeto Date y descubrir cuánto puedo hacer con él: https://www.toptal.com/software/definitive-guide-to-datetime-manipulation

RRULE

Calcular ocurrencias para un RRULE vuelve complicado porque existe una diferencia fundamental en la expectativa del usuario y el procesamiento de fechas en UTC, que se nota cuando estas dos representaciones de tiempo cruzan la barrera de un día.

Por ejemplo, si nuestro usuario desea que ocurra una repetición que ocurra todos los martes, esperará que todas las ocurrencias caigan en un martes en su hora local. Sin embargo, todos los cálculos se realizan en UTC, por lo que un RRULE como:

DTSTART:2019-10-29T00:02:26Z
RRULE:FREQ=WEEKLY;BYWEEKDAY=TU

devolvería ocurrencias los martes en UTC, pero para cualquier usuario ubicado significativamente al oeste del primer meridiano, las ocurrencias aparecerían los lunes en hora local, que obviamente no es el resultado que nadie espera.

Entendiéndolo

Dicho todo esto, lo que el desarrollador debe hacer para resolver esto es forzar a JavaScript a pensar que la fecha local es en realidad una fecha UTC, dar esa fecha "falsa" a rrule.js y hacer que la biblioteca haga sus cálculos en esta hora UTC local. Cuando obtiene las ocurrencias del método .all() , entonces crea una instancia de nuevos objetos Date usando las partes "UTC" de esas fechas. Esto efectivamente hace que los resultados coincidan con las expectativas.

import { RRule } from 'rrule'

const dtstart = new Date('2019-10-29T00:02:26Z') // same as: `new Date(Date.UTC(2019, 9, 29, 0, 2, 26))`
console.log(dtstart.toISOString()) // 2019-10-29T00:02:26.000Z
console.log(dtstart) // Mon Oct 28 2019 17:02:26 GMT-0700 (Pacific Daylight Time)

const fakeDateStart = setPartsToUTCDate(dtstart)
console.log(fakeDateStart.toISOString()) // 2019-10-28T17:02:26.000Z
console.log(fakeDateStart) // Mon Oct 28 2019 10:02:26 GMT-0700 (Pacific Daylight Time)

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.TU],
  count: 2,
  dtstart: fakeDateStart
})

const localUTCOccurrences = rule.all()
console.log(localUTCOccurrences.map(toISOString)) // ["2019-10-29T17:02:26.000Z", "2019-11-05T17:02:26.000Z"]
console.log(localUTCOccurrences) // [Tue Oct 29 2019 10:02:26 GMT-0700 (Pacific Daylight Time), Tue Nov 05 2019 09:02:26 GMT-0800 (Pacific Standard Time)]

const occurrences = localUTCOccurrences.map(setUTCPartsToDate)
console.log(occurrences.map(toISOString)) // ["2019-10-30T00:02:26.000Z", "2019-11-06T01:02:26.000Z"]
console.log(occurrences) // [Tue Oct 29 2019 17:02:26 GMT-0700 (Pacific Daylight Time), Tue Nov 05 2019 17:02:26 GMT-0800 (Pacific Standard Time)]

function setPartsToUTCDate(d) {
  return new Date(
    Date.UTC(
      d.getFullYear(),
      d.getMonth(),
      d.getDate(),
      d.getHours(),
      d.getMinutes(),
      d.getSeconds()
    )
  )
}

function setUTCPartsToDate(d) {
  return new Date(
    d.getUTCFullYear(),
    d.getUTCMonth(),
    d.getUTCDate(),
    d.getUTCHours(),
    d.getUTCMinutes(),
    d.getUTCSeconds()
  )
}

function toISOString(d) {
  return d.toISOString()
}

https://codesandbox.io/s/rrule-localutc-conversion-zxlki

O en resumen:

import { RRule } from 'rrule'

const date = new Date('2019-10-29T00:02:26Z')

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.TU],
  count: 2,
  dtstart: setPartsToUTCDate(date)
})

const occurrences = rule.all().map(setUTCPartsToDate)

https://codesandbox.io/s/rrule-localutc-conversion-in-short-ez7g0

Por lo que puedo ver, Chrome, Firefox y Safari muestran todas las fechas como cadenas en la hora local al iniciar sesión en la consola, y Node.js como cadenas ISO en UTC.

Todos 18 comentarios

También estoy terriblemente confundido.

Ok, lo tengo, había este párrafo clave en el archivo Léame:

image

Debe utilizar estas dos funciones auxiliares para la zona horaria local. Debería modificar el desplazamiento para obtener el desplazamiento de la zona horaria X si desea modificar esto para admitir tzid.

/**
 * Mutates date. Add timezone to the date.
 *
 * <strong i="9">@param</strong> {Date} [date]
 */
export function localizeDate(date = new Date()) {
  const offset = new Date().getTimezoneOffset() * 60 * 1000; // get offset for local timezone (can modify here to support tzid)
  date.setTime(date.getTime() - offset);
  return date;
}

/**
 * Mutates a date. De-timezones a date.
 *
 * <strong i="10">@param</strong> {Date} [date]
 */
export function utcifyDate(date = new Date()) {
  const offset = new Date().getTimezoneOffset() * 60 * 1000; // get offset for local timezone (can modify here to support tzid)
  date.setTime(date.getTime() + offset);
  return date;
}

Entonces su código cambiaría a:

const { RRule, RRuleSet } = require(`rrule`);

const dtstart = localizeDate(new Date());
console.log(`dtstart`, utcifyDate(new Date(dtstart))); // even though it is utc string, this is the local time

const firstHour = dtstart.getUTCHours();
const secondHour = (firstHour + 2) % 24;

const getRRule = options => new RRule(Object.assign({}, options, {
    freq: RRule.DAILY,
    count: 1,
    dtstart,
    byminute: 0,
    bysecond: 0
}));

const showNextInstance = tzid => {
    const ruleSet = new RRuleSet();
    ruleSet.rrule(getRRule({ byhour: firstHour })); // remove tzid as i dont support this
    ruleSet.rrule(getRRule({ byhour: secondHour })); // removed tzid as i dont support this
    console.log('local:', utcifyDate(ruleSet.after(dtstart));
};

showNextInstance(undefined);
showNextInstance(`UTC`);
showNextInstance(`local`);
showNextInstance(`America/Chicago`);

Sospecho que no habrá una buena forma de evitar esto en esta biblioteca, construida como está en la implementación confusa e irregular de JS Date . Actualmente estoy trabajando en una reescritura completa, pero tomará algún tiempo hacerlo bien.

Gracias @davidgoli, tu trabajo es muy apreciado !!!

Creo que necesitamos a alguien que comprenda cómo escribir un artículo para ayudar a otros a comprender la implementación de fechas irregulares. Honestamente, todavía no lo entiendo, solo modifiqué las cosas hasta que funcionó.

Sería porque la fecha devuelta que está imprimiendo en la consola es una fecha de JavaScript que se está convirtiendo en una cadena, y JavaScript imprime automáticamente Fechas en la consola en UTC (vea la 'Z' al final de sus marcas de tiempo que denota desplazamiento cero). Dado que las siguientes ocurrencias en cada uno de sus cuatro casos están sucediendo exactamente al mismo tiempo (aunque en diferentes zonas horarias), se imprime exactamente la misma marca de tiempo cuatro veces.

También tenga en cuenta que el objeto JavaScript Date realiza un seguimiento del tiempo utilizando la cantidad de milisegundos que han transcurrido desde las 00:00:00 UTC, jueves 1 de enero de 1970 (¡tenga en cuenta el "UTC" allí!), No por algunos tipo de interpretación de cadena de "12:34:56 pm" o algo por el estilo.

@davidgoli

Actualmente estoy trabajando en una reescritura completa, pero tomará algún tiempo hacerlo bien.

Es posible que desee consultar la fuente de rSchedule . Toda la iteración se realiza con un objeto especial DateTime (no de Luxon) que toma una entrada y la convierte en una fecha utc flotante que parece equivalente a la entrada. La iteración se realiza con esta fecha y hora UTC, por lo que el horario de verano no es un problema. Una vez que se encuentra una ocurrencia, antes de cederla al usuario, se vuelve a convertir a una fecha normal en la zona horaria adecuada.

Por ejemplo: esta fecha

import { DateTime as LuxonDateTime } from 'luxon';

const date = LuxonDateTime.fromObject({
  year: 2019,
  month: 1,
  day: 1,
  zone: 'America/Los_Angeles'
})

se convertiría a esta fecha UTC y la zona horaria se guardaría

const fakeDate = Date.UTC(2019, 0, 1)
const fakeDateZone = 'America/Los_Angeles'

Tenga en cuenta que esta fecha UTC se parece a la fecha de Luxon (ambas se ven como 2019/1/1 ) pero, dado que están en diferentes zonas horarias, en realidad no representan la misma hora.

Dicho esto, iterar con esta fecha especial "UTC" nos permite iterar sin preocuparnos por el horario de verano. Una vez que se encuentra la fecha correcta, podemos convertirla de nuevo a una fecha luxon.

Por ejemplo:

LuxonDateTime.fromObject({
  year: fakeDate.year,
  month: fakeDate.month,
  day: fakeDate.day,
  zone: fakeDateZone,
})

Esto mantiene la lógica de iteración agradable y compatible con UTC y al mismo tiempo admite zonas horarias arbitrarias.

@thefliik Tengo los primitivos abajo; Son semanas las que son el verdadero dolor de cabeza aquí. Aunque veo que rschedule también ha despejado (hasta ahora) en el soporte de byweekno y bysetpos ...

Ah ya. byweekno es muy molesto.

¿hay noticias?

Creo que es fácil combinar los problemas con el objeto Date y los problemas para calcular las ocurrencias de RRULE , así que hablaré de ellos por separado antes de combinar los dos. Tuve que escribir esto principalmente para poder resolver mi comprensión del problema y pensé que publicaría aquí en caso de que ayude a otros.

Objeto de fecha de JavaScript

No estoy de acuerdo con que el objeto JS Date sea ​​irregular, pero existen malentendidos comunes con el objeto que hacen que parezca así. Es confuso por decir lo menos.

Como mencionó @ hlee5zebra , las fechas creadas con el constructor Date siempre se representan en UTC. Pueden mostrarse en la hora local, pero detrás de escena están representados en la época de Unix Epoch. Siempre podemos ejecutar .getTime() o .toISOString() en el objeto Date instanciado para recordarnos que estamos trabajando en UTC.

La implementación también sigue correctamente la convención de hacer todo el procesamiento en UTC y solo convertir a la hora local que se muestra al usuario. Da la casualidad de que nosotros, los desarrolladores, somos el usuario; pero luego tenemos en mente a nuestro propio usuario que enturbia el agua. Al desarrollar nuestras aplicaciones, debemos asegurarnos de seguir la convención y guardar las fechas como UTC en nuestro backend (JS ya procesa las fechas en UTC, por lo que estamos bien allí). Si almacena fechas como UTC en su almacén de datos, deberían llegar al cliente como cadenas ISO , ser analizadas correctamente por el objeto Date y mostrarse en la hora local del usuario, todo sin hacer ningún trabajo pesado.

Este artículo me ha ayudado a comprender el objeto Date y descubrir cuánto puedo hacer con él: https://www.toptal.com/software/definitive-guide-to-datetime-manipulation

RRULE

Calcular ocurrencias para un RRULE vuelve complicado porque existe una diferencia fundamental en la expectativa del usuario y el procesamiento de fechas en UTC, que se nota cuando estas dos representaciones de tiempo cruzan la barrera de un día.

Por ejemplo, si nuestro usuario desea que ocurra una repetición que ocurra todos los martes, esperará que todas las ocurrencias caigan en un martes en su hora local. Sin embargo, todos los cálculos se realizan en UTC, por lo que un RRULE como:

DTSTART:2019-10-29T00:02:26Z
RRULE:FREQ=WEEKLY;BYWEEKDAY=TU

devolvería ocurrencias los martes en UTC, pero para cualquier usuario ubicado significativamente al oeste del primer meridiano, las ocurrencias aparecerían los lunes en hora local, que obviamente no es el resultado que nadie espera.

Entendiéndolo

Dicho todo esto, lo que el desarrollador debe hacer para resolver esto es forzar a JavaScript a pensar que la fecha local es en realidad una fecha UTC, dar esa fecha "falsa" a rrule.js y hacer que la biblioteca haga sus cálculos en esta hora UTC local. Cuando obtiene las ocurrencias del método .all() , entonces crea una instancia de nuevos objetos Date usando las partes "UTC" de esas fechas. Esto efectivamente hace que los resultados coincidan con las expectativas.

import { RRule } from 'rrule'

const dtstart = new Date('2019-10-29T00:02:26Z') // same as: `new Date(Date.UTC(2019, 9, 29, 0, 2, 26))`
console.log(dtstart.toISOString()) // 2019-10-29T00:02:26.000Z
console.log(dtstart) // Mon Oct 28 2019 17:02:26 GMT-0700 (Pacific Daylight Time)

const fakeDateStart = setPartsToUTCDate(dtstart)
console.log(fakeDateStart.toISOString()) // 2019-10-28T17:02:26.000Z
console.log(fakeDateStart) // Mon Oct 28 2019 10:02:26 GMT-0700 (Pacific Daylight Time)

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.TU],
  count: 2,
  dtstart: fakeDateStart
})

const localUTCOccurrences = rule.all()
console.log(localUTCOccurrences.map(toISOString)) // ["2019-10-29T17:02:26.000Z", "2019-11-05T17:02:26.000Z"]
console.log(localUTCOccurrences) // [Tue Oct 29 2019 10:02:26 GMT-0700 (Pacific Daylight Time), Tue Nov 05 2019 09:02:26 GMT-0800 (Pacific Standard Time)]

const occurrences = localUTCOccurrences.map(setUTCPartsToDate)
console.log(occurrences.map(toISOString)) // ["2019-10-30T00:02:26.000Z", "2019-11-06T01:02:26.000Z"]
console.log(occurrences) // [Tue Oct 29 2019 17:02:26 GMT-0700 (Pacific Daylight Time), Tue Nov 05 2019 17:02:26 GMT-0800 (Pacific Standard Time)]

function setPartsToUTCDate(d) {
  return new Date(
    Date.UTC(
      d.getFullYear(),
      d.getMonth(),
      d.getDate(),
      d.getHours(),
      d.getMinutes(),
      d.getSeconds()
    )
  )
}

function setUTCPartsToDate(d) {
  return new Date(
    d.getUTCFullYear(),
    d.getUTCMonth(),
    d.getUTCDate(),
    d.getUTCHours(),
    d.getUTCMinutes(),
    d.getUTCSeconds()
  )
}

function toISOString(d) {
  return d.toISOString()
}

https://codesandbox.io/s/rrule-localutc-conversion-zxlki

O en resumen:

import { RRule } from 'rrule'

const date = new Date('2019-10-29T00:02:26Z')

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.TU],
  count: 2,
  dtstart: setPartsToUTCDate(date)
})

const occurrences = rule.all().map(setUTCPartsToDate)

https://codesandbox.io/s/rrule-localutc-conversion-in-short-ez7g0

Por lo que puedo ver, Chrome, Firefox y Safari muestran todas las fechas como cadenas en la hora local al iniciar sesión en la consola, y Node.js como cadenas ISO en UTC.

@ tim-phillips También estoy sufriendo por esto en este momento y estoy buscando algún tipo de parche para evitarlo.

Tengo un evento recurrente a las 8 p.m. EST que funciona bien, pero a las 9 p.m. EST lo está retrasando al día anterior al que debería ser.

Espero ver si puede encontrar una respuesta a esto.

@ tim-phillips que es un excelente desglose. Todavía lo estoy pasando, pero parece que finalmente me ayudará a entender, ¡gracias!

@ tim-phillips Gracias por su análisis.

Sin embargo, ¿puede explicar lo siguiente? Parece que su solución aún puede regresar el miércoles dependiendo de la zona horaria local de su computadora.

image

@tonylau no mira la salida en cadena en su consola, que siempre se convertirá en su zona horaria local. Debe usar los métodos getUTC*() para obtener las partes de la fecha, que serán las mismas independientemente de su zona horaria local.

@davidgoli

Configuré mi zona horaria en UTC + 14 (Kiritimati Island / Line Islands Time) y obtuve lo siguiente, incluso cuando estoy usando getUTCDay (). ¿El día todavía se devuelve como miércoles cuando debería ser martes?

Sandbox: https://codesandbox.io/s/rrule-localutc-conversion-in-short-0uzt1

image

Potencialmente relevante para esta discusión: https://github.com/tc39/proposal-temporal

@ tim-phillips gracias !!!!!!!!!! eso fue muy útil !!!!!!! ¿Sabes cómo hacer las ics con la regla? Vuelvo a tener el problema del día cuando uso el paquete ics en el nodo y cuando recibo las invitaciones del calendario, tienen un día libre. (¡Pero puedo poner todo en mi base de datos correctamente debido a su código! Muchas gracias).

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

shavenwalrus picture shavenwalrus  ·  7Comentarios

jimmywarting picture jimmywarting  ·  9Comentarios

shorlbeck picture shorlbeck  ·  21Comentarios

Prinzhorn picture Prinzhorn  ·  15Comentarios

marcoancona picture marcoancona  ·  22Comentarios