Rrule: Confusion UTC mais pas UTC

Créé le 5 avr. 2019  ·  18Commentaires  ·  Source: jakubroztocil/rrule

Je soupçonne que ce n'est pas un problème avec rrule, mais avec ma compréhension et/ou sa prise en charge du fuseau horaire. J'ai déjà lu les sections connexes dans la documentation de rrule et luxon .

Exemple de code

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`);

Sortie réelle

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

Production attendue

Je pense que la spécification de tzid aurait un effet sur la sortie, mais la même sortie est générée pour chaque fuseau horaire.

Je ne vois pas non plus quelle est la corrélation entre les heures fournies dans les règles et la sortie de la prochaine instance d'événement. Je m'attendrais à ce que l'instance suivante corresponde à peu près à l'une des deux heures représentées par les règles fournies en fonction de la façon dont les valeurs spécifiées dtstart et tzid sont interprétées ou appliquées.

version de la règle

2.6.0

Système opérateur

OS X 10.14.2, nœud 8.11.4

Fuseau horaire local

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

Commentaire le plus utile

Je pense qu'il est facile de confondre les problèmes avec l'objet Date et les problèmes de calcul des occurrences pour RRULE , je vais donc en parler séparément avant de combiner les deux. J'ai dû écrire ceci principalement pour que je puisse trier ma compréhension du problème et j'ai pensé que je posterais ici au cas où cela aiderait les autres.

Objet Date JavaScript

Je ne suis pas d'accord pour dire que l'objet JS Date est irrégulier, mais il existe des malentendus courants avec l'objet qui le font sembler ainsi. C'est pour le moins déroutant.

Comme @hlee5zebra l'a mentionné, les dates créées avec le constructeur Date sont toujours représentées en UTC. Ils peuvent être affichés en heure locale, mais dans les coulisses, ils sont en fait représentés en temps Unix Epoch. Nous pouvons toujours exécuter .getTime() ou .toISOString() sur l'objet Date instancié pour nous rappeler que nous travaillons en UTC.

L'implémentation suit également correctement la convention d'effectuer tout le traitement en UTC et de ne convertir qu'en heure locale affichée à l'utilisateur. Il se trouve que nous, les développeurs, sommes l'utilisateur ; mais alors nous avons notre propre utilisateur à l'esprit qui brouille l'eau. Lors du développement de nos applications, nous devons nous assurer de suivre la convention et d'enregistrer les dates en UTC dans notre backend (JS traite déjà les dates en UTC, donc nous y sommes bien). Si vous stockez les dates au format UTC dans votre magasin de données, elles doivent arriver au client sous forme de chaînes ISO , être analysées correctement par l'objet Date et être affichées dans l'heure locale de l'utilisateur, le tout sans faire de gros travaux.

Cet article m'a aidé à comprendre l'objet Date et à découvrir tout ce que je peux faire avec : https://www.toptal.com/software/definitive-guide-to-datetime-manipulation

RÈGLE

Le calcul des occurrences pour un RRULE devient délicat car il existe une différence fondamentale dans les attentes des utilisateurs et le traitement des dates en UTC, visible lorsque ces deux représentations temporelles franchissent une barrière de jour.

Par exemple, si notre utilisateur souhaite que la récurrence se produise tous les mardis, il s'attendra à ce que toutes les occurrences tombent un mardi à son heure locale. Cependant, tous les calculs sont effectués en UTC, donc un RRULE comme :

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

renverrait les occurrences le mardi en UTC, mais pour tout utilisateur situé significativement à l'ouest du premier méridien, les occurrences apparaîtraient le lundi en heure locale, ce qui n'est évidemment pas le résultat espéré.

Faire les bons choix

Cela dit, ce que le développeur doit faire pour résoudre ce problème, c'est forcer JavaScript à penser que la date locale est en fait une date UTC, donner cette "fausse" date à rrule.js , et demander à la bibliothèque de faire ses calculs dans cette heure UTC locale. Lorsque vous obtenez les occurrences de la méthode .all() , vous instanciez ensuite de nouveaux objets Date utilisant les parties "UTC" de ces dates. Cela rend effectivement les résultats conformes aux attentes.

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

Ou en bref :

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

Autant que je sache, Chrome, Firefox et Safari affichent tous les dates sous forme de chaînes en heure locale lors de la connexion à la console, et Node.js sous forme de chaînes ISO en UTC.

Tous les 18 commentaires

Je suis aussi terriblement confus.

Ok, j'ai compris, il y avait ce paragraphe clé dans le readme :

image

Vous devez utiliser ces deux fonctions d'assistance pour le fuseau horaire local. Vous devrez modifier le décalage pour obtenir le décalage du fuseau horaire X si vous souhaitez le modifier pour prendre en charge 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;
}

Ensuite, votre code deviendrait :

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`);

Je soupçonne qu'il n'y aura pas un bon moyen de contourner cela dans cette bibliothèque, construite telle qu'elle est sur l'implémentation confuse et irrégulière de Date JS. Je travaille actuellement sur une réécriture complète, mais cela prendra un certain temps pour bien faire les choses.

Merci @davidgoli votre travail est tellement apprécié !!!

Je pense que nous avons besoin de quelqu'un qui comprend pour écrire un article pour aider les autres à comprendre la mise en œuvre des dates irrégulières. Honnêtement, je ne comprends toujours pas, j'ai juste peaufiné les choses jusqu'à ce que cela fonctionne.

Ce serait parce que la date renvoyée que vous imprimez sur la console est une date JavaScript qui est convertie en une chaîne, et JavaScript imprime automatiquement les dates sur la console en UTC (voir le « Z » à la fin de vos horodatages qui indique décalage zéro). Étant donné que les prochaines occurrences dans chacun de vos quatre cas se produisent exactement au même moment (bien que dans des fuseaux horaires différents), il imprime exactement le même horodatage quatre fois.

Notez également que l'objet JavaScript Date garde une trace du temps en utilisant le nombre de millisecondes qui se sont écoulées depuis 00:00:00 UTC, jeudi 1er janvier 1970 (Notez le "UTC" là-bas !), pas par certains sorte d'interprétation de chaîne de "12:34:56pm" ou quelque chose du genre.

@davidgoli

Je travaille actuellement sur une réécriture complète, mais cela prendra un certain temps pour bien faire les choses.

Vous voudrez peut-être consulter la source de rSchedule . Toutes les itérations sont effectuées avec un objet spécial DateTime (pas de Luxon) qui prend une entrée et la convertit en une date utc flottante qui semble équivalente à l'entrée. L'itération est effectuée avec cette date-heure UTC, donc l'heure d'été n'est pas un problème. Une fois qu'une occurrence est trouvée, avant qu'elle ne soit transmise à l'utilisateur, elle est reconvertie en une date normale dans le fuseau horaire approprié.

Par exemple : cette date

import { DateTime as LuxonDateTime } from 'luxon';

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

serait converti à cette date UTC et le fuseau horaire serait enregistré

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

Notez que cette date UTC ressemble à la date Luxon (elles ressemblent toutes les deux à 2019/1/1 ) mais, comme elles se trouvent dans des fuseaux horaires différents, elles ne représentent pas réellement la même heure.

Cela étant dit, l'itération avec cette date spéciale "UTC" nous permet d'itérer sans nous soucier de l'heure d'été. Une fois la date correcte trouvée, nous pouvons la convertir en une date luxon.

Par exemple:

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

Cela maintient la logique d'itération agréable et conviviale UTC tout en prenant en charge des fuseaux horaires arbitraires.

@thefliik j'ai obtenu les primitives; ce sont les semaines qui sont le vrai casse-tête ici. Bien que je vois que rschedule a également fait un botté (jusqu'à présent) sur le support byweekno & bysetpos ...

Ah ouais. byweekno est très ennuyeux.

des nouvelles?

Je pense qu'il est facile de confondre les problèmes avec l'objet Date et les problèmes de calcul des occurrences pour RRULE , je vais donc en parler séparément avant de combiner les deux. J'ai dû écrire ceci principalement pour que je puisse trier ma compréhension du problème et j'ai pensé que je posterais ici au cas où cela aiderait les autres.

Objet Date JavaScript

Je ne suis pas d'accord pour dire que l'objet JS Date est irrégulier, mais il existe des malentendus courants avec l'objet qui le font sembler ainsi. C'est pour le moins déroutant.

Comme @hlee5zebra l'a mentionné, les dates créées avec le constructeur Date sont toujours représentées en UTC. Ils peuvent être affichés en heure locale, mais dans les coulisses, ils sont en fait représentés en temps Unix Epoch. Nous pouvons toujours exécuter .getTime() ou .toISOString() sur l'objet Date instancié pour nous rappeler que nous travaillons en UTC.

L'implémentation suit également correctement la convention d'effectuer tout le traitement en UTC et de ne convertir qu'en heure locale affichée à l'utilisateur. Il se trouve que nous, les développeurs, sommes l'utilisateur ; mais alors nous avons notre propre utilisateur à l'esprit qui brouille l'eau. Lors du développement de nos applications, nous devons nous assurer de suivre la convention et d'enregistrer les dates en UTC dans notre backend (JS traite déjà les dates en UTC, donc nous y sommes bien). Si vous stockez les dates au format UTC dans votre magasin de données, elles doivent arriver au client sous forme de chaînes ISO , être analysées correctement par l'objet Date et être affichées dans l'heure locale de l'utilisateur, le tout sans faire de gros travaux.

Cet article m'a aidé à comprendre l'objet Date et à découvrir tout ce que je peux faire avec : https://www.toptal.com/software/definitive-guide-to-datetime-manipulation

RÈGLE

Le calcul des occurrences pour un RRULE devient délicat car il existe une différence fondamentale dans les attentes des utilisateurs et le traitement des dates en UTC, visible lorsque ces deux représentations temporelles franchissent une barrière de jour.

Par exemple, si notre utilisateur souhaite que la récurrence se produise tous les mardis, il s'attendra à ce que toutes les occurrences tombent un mardi à son heure locale. Cependant, tous les calculs sont effectués en UTC, donc un RRULE comme :

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

renverrait les occurrences le mardi en UTC, mais pour tout utilisateur situé significativement à l'ouest du premier méridien, les occurrences apparaîtraient le lundi en heure locale, ce qui n'est évidemment pas le résultat espéré.

Faire les bons choix

Cela dit, ce que le développeur doit faire pour résoudre ce problème, c'est forcer JavaScript à penser que la date locale est en fait une date UTC, donner cette "fausse" date à rrule.js , et demander à la bibliothèque de faire ses calculs dans cette heure UTC locale. Lorsque vous obtenez les occurrences de la méthode .all() , vous instanciez ensuite de nouveaux objets Date utilisant les parties "UTC" de ces dates. Cela rend effectivement les résultats conformes aux attentes.

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

Ou en bref :

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

Autant que je sache, Chrome, Firefox et Safari affichent tous les dates sous forme de chaînes en heure locale lors de la connexion à la console, et Node.js sous forme de chaînes ISO en UTC.

@tim-phillips Je souffre également de cela en ce moment et je cherche une sorte de patch de singe pour le contourner.

J'ai un événement récurrent à 20 h 00 HNE qui fonctionne bien, mais 21 h 00 HNE le repousse à la veille.

J'ai hâte de voir si vous pouvez trouver une réponse à cela.

@tim-phillips c'est une excellente panne. Je suis toujours en train de le parcourir, mais il semble que cela m'aidera enfin à comprendre, merci!

@tim-phillips Merci pour votre analyse.

Cependant, pouvez-vous expliquer ce qui suit ? Il semble que votre solution puisse toujours revenir mercredi en fonction du fuseau horaire local de votre ordinateur.

image

@tonylau ne regarde pas la sortie stringifiée dans votre console, qui sera toujours convertie dans votre fuseau horaire local. Vous devez utiliser les méthodes getUTC*() pour obtenir les parties de la date, qui seront les mêmes quel que soit votre fuseau horaire local.

@davidgoli

J'ai défini mon fuseau horaire sur UTC+14 (Kiritimati Island/Line Islands Time) et j'ai obtenu ce qui suit, même lorsque j'utilise getUTCDay(). Le jour est toujours revenu comme mercredi alors qu'il devrait être mardi ?

Bac à sable : https://codesandbox.io/s/rrule-localutc-conversion-in-short-0uzt1

image

Potentiellement pertinent pour cette discussion : https://github.com/tc39/proposal-temporal

@tim-phillips merci !!!!!!!!!! c'était tellement utile !!!!!!! Savez-vous comment faire les ics avec la règle ? J'ai à nouveau le problème du jour lorsque j'utilise le package ics dans node et lorsque je reçois les invitations du calendrier, elles sont un jour de congé. (Mais je suis capable de tout mettre dans ma base de données correctement grâce à votre code !! merci beaucoup.)

Cette page vous a été utile?
0 / 5 - 0 notes