Rrule: Confusão UTC-mas-não-UTC

Criado em 5 abr. 2019  ·  18Comentários  ·  Fonte: jakubroztocil/rrule

Suspeito que isso não seja um problema com a regra, mas com meu entendimento dela e / ou seu suporte de fuso horário. Já li seções relacionadas na documentação para regra e luxon .

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

Saída 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

Saída esperada

Eu acho que a especificação de tzid teria algum efeito na saída, mas a mesma saída é gerada para cada fuso horário.

Também não consigo ver qual é a correlação entre os tempos fornecidos nas regras e a saída para a próxima instância de evento. Eu esperaria que a próxima instância corresponderia aproximadamente a uma das duas horas representadas pelas regras fornecidas, dependendo de como os valores especificados de dtstart e tzid são interpretados ou aplicados.

versão da regra

2.6.0

Sistema operacional

OS X 10.14.2, Nó 8.11.4

Fuso horário local

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

Comentários muito úteis

Acho que é fácil confundir problemas com o objeto Date e problemas de cálculo de ocorrências para RRULE , então vou falar sobre eles separadamente antes de combinar os dois. Eu tive que escrever isso principalmente para que eu pudesse entender meu entendimento do problema e resolvi postar aqui, caso isso ajude outras pessoas.

Objeto JavaScript Date

Não concordo que o objeto JS Date seja irregular, mas há mal-entendidos comuns com o objeto que o fazem parecer assim. É no mínimo confuso.

Como @ hlee5zebra mencionou, as datas criadas com o construtor Date são sempre representadas em UTC. Eles podem ser exibidos no horário local, mas, nos bastidores, eles são de fato representados no horário Unix Epoch. Sempre podemos executar .getTime() ou .toISOString() no objeto Date instanciado para nos lembrar de que estamos trabalhando em UTC.

A implementação também segue corretamente a convenção de fazer todo o processamento em UTC e apenas converter para a hora local no display para o usuário. Acontece que nós, os desenvolvedores, somos os usuários; mas temos nosso próprio usuário em mente, o que turva a água. Ao desenvolver nossos aplicativos, devemos nos certificar de seguir a convenção e salvar as datas como UTC em nosso backend (JS já processa datas em UTC, então estamos bem lá). Se você armazenar datas como UTC em seu armazenamento de dados, elas devem chegar ao cliente como strings ISO , ser analisadas corretamente pelo objeto Date e exibidas na hora local do usuário, tudo sem fazer nenhum trabalho pesado.

Este artigo me ajudou a entender o objeto Date e descobrir o quanto posso fazer com ele: https://www.toptal.com/software/definitive-guide-to-datetime-manipulation

RRULE

Calcular ocorrências para um RRULE fica complicado porque há uma diferença fundamental na expectativa do usuário e no processamento de datas em UTC, perceptível quando essas duas representações de tempo cruzam a barreira de um dia.

Por exemplo, se nosso usuário deseja que a recorrência ocorra toda terça-feira, ele espera que todas as ocorrências caiam em uma terça-feira em seu horário local. No entanto, todos os cálculos são feitos em UTC, então RRULE como:

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

retornaria ocorrências às terças-feiras no UTC, mas para qualquer usuário localizado significativamente a oeste do meridiano principal, as ocorrências apareceriam às segundas-feiras no horário local, o que obviamente não é o resultado que todos esperam.

Fazendo certo

Dito isso, o que o desenvolvedor deve fazer para resolver isso é forçar o JavaScript a pensar que a data local é na verdade uma data UTC, dar essa data "falsa" para rrule.js e fazer com que a biblioteca faça seus cálculos em esta hora UTC local. Quando você obtém as ocorrências do método .all() , instancia novos objetos Date usando as partes "UTC" dessas datas. Isso efetivamente faz com que os resultados correspondam às 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

Ou resumindo:

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

Tanto quanto posso ver, Chrome, Firefox e Safari exibem datas como strings na hora local ao fazer login no console, e Node.js como strings ISO em UTC.

Todos 18 comentários

Também estou terrivelmente confuso.

Ok, entendi, havia este parágrafo principal no leia-me:

image

Você deve usar essas duas funções auxiliares para o fuso horário local. Você teria que modificar o deslocamento para obter o deslocamento para o fuso horário X se quiser modificá-lo para suportar 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;
}

Então, seu código mudaria para:

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

Suspeito que não haverá uma boa maneira de contornar isso nesta biblioteca, construída como está na implementação Date confusa e irregular de JS. No momento, estou trabalhando em uma reescrita completa, mas levará algum tempo para acertar.

Obrigado @davidgoli seu trabalho é super apreciado !!!

Acho que precisamos de alguém que entenda de escrever um artigo para ajudar outras pessoas a entender a implementação de datas irregulares. Sinceramente, ainda não entendi, apenas mexi nas coisas até que funcionasse.

Seria porque a data retornada que você está imprimindo no console é uma data JavaScript que está sendo convertida em uma string, e o JavaScript imprime automaticamente as datas para o console em UTC (Veja o 'Z' no final de seus carimbos de data / hora que denota deslocamento zero). Como as próximas ocorrências em cada um dos quatro casos estão acontecendo exatamente ao mesmo tempo (embora em fusos horários diferentes), ele está imprimindo exatamente o mesmo carimbo de data / hora quatro vezes.

Observe também que o objeto JavaScript Date mantém o controle do tempo usando o número de milissegundos decorridos desde 00:00:00 UTC, quinta-feira, 1 de janeiro de 1970 (observe o "UTC" aqui!), Não por alguns tipo de interpretação de string de "12:34:56 pm" ou qualquer coisa do tipo.

@davidgoli

No momento, estou trabalhando em uma reescrita completa, mas levará algum tempo para acertar.

Você pode querer verificar a fonte do rSchedule . Toda a iteração é feita com um objeto especial parece equivalente à entrada. A iteração é feita com essa data e hora UTC, portanto, o DST não é um problema. Depois que uma ocorrência é encontrada, antes de ser fornecida ao usuário, ela é convertida de volta para uma data normal no fuso horário apropriado.

Por exemplo: esta data

import { DateTime as LuxonDateTime } from 'luxon';

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

seria convertido para esta data UTC e o fuso horário seria salvo

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

Observe que essa data UTC é semelhante à data Luxon (ambas se parecem com 2019/1/1 ), mas, como estão em fusos horários diferentes, na verdade não representam a mesma hora.

Dito isso, iterar com essa data "UTC" especial nos permite iterar sem nos preocupar com o horário de verão. Depois que a data correta for encontrada, podemos converter de volta para uma data luxon.

Por exemplo:

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

Isso mantém a lógica de iteração agradável e amigável com o UTC, ao mesmo tempo em que oferece suporte a fusos horários arbitrários.

@thefliik Baixei os primitivos; são as semanas que são a verdadeira dor de cabeça aqui. Embora eu veja que rschedule também tem direcionado (até agora) para o suporte de byweekno e bysetpos ...

Ah, claro. byweekno é muito chato.

qualquer notícia?

Acho que é fácil confundir problemas com o objeto Date e problemas de cálculo de ocorrências para RRULE , então vou falar sobre eles separadamente antes de combinar os dois. Eu tive que escrever isso principalmente para que eu pudesse entender meu entendimento do problema e resolvi postar aqui, caso isso ajude outras pessoas.

Objeto JavaScript Date

Não concordo que o objeto JS Date seja irregular, mas há mal-entendidos comuns com o objeto que o fazem parecer assim. É no mínimo confuso.

Como @ hlee5zebra mencionou, as datas criadas com o construtor Date são sempre representadas em UTC. Eles podem ser exibidos no horário local, mas, nos bastidores, eles são de fato representados no horário Unix Epoch. Sempre podemos executar .getTime() ou .toISOString() no objeto Date instanciado para nos lembrar de que estamos trabalhando em UTC.

A implementação também segue corretamente a convenção de fazer todo o processamento em UTC e apenas converter para a hora local no display para o usuário. Acontece que nós, os desenvolvedores, somos os usuários; mas temos nosso próprio usuário em mente, o que turva a água. Ao desenvolver nossos aplicativos, devemos nos certificar de seguir a convenção e salvar as datas como UTC em nosso backend (JS já processa datas em UTC, então estamos bem lá). Se você armazenar datas como UTC em seu armazenamento de dados, elas devem chegar ao cliente como strings ISO , ser analisadas corretamente pelo objeto Date e exibidas na hora local do usuário, tudo sem fazer nenhum trabalho pesado.

Este artigo me ajudou a entender o objeto Date e descobrir o quanto posso fazer com ele: https://www.toptal.com/software/definitive-guide-to-datetime-manipulation

RRULE

Calcular ocorrências para um RRULE fica complicado porque há uma diferença fundamental na expectativa do usuário e no processamento de datas em UTC, perceptível quando essas duas representações de tempo cruzam a barreira de um dia.

Por exemplo, se nosso usuário deseja que a recorrência ocorra toda terça-feira, ele espera que todas as ocorrências caiam em uma terça-feira em seu horário local. No entanto, todos os cálculos são feitos em UTC, então RRULE como:

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

retornaria ocorrências às terças-feiras no UTC, mas para qualquer usuário localizado significativamente a oeste do meridiano principal, as ocorrências apareceriam às segundas-feiras no horário local, o que obviamente não é o resultado que todos esperam.

Fazendo certo

Dito isso, o que o desenvolvedor deve fazer para resolver isso é forçar o JavaScript a pensar que a data local é na verdade uma data UTC, dar essa data "falsa" para rrule.js e fazer com que a biblioteca faça seus cálculos em esta hora UTC local. Quando você obtém as ocorrências do método .all() , instancia novos objetos Date usando as partes "UTC" dessas datas. Isso efetivamente faz com que os resultados correspondam às 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

Ou resumindo:

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

Tanto quanto posso ver, Chrome, Firefox e Safari exibem datas como strings na hora local ao fazer login no console, e Node.js como strings ISO em UTC.

@ tim-phillips Eu também estou sofrendo por isso agora e procurando algum tipo de patch de macaco para contornar isso.

Tenho um evento recorrente às 20h EST que funciona bem, mas 21h EST está adiando para o dia anterior ao que deveria.

Estou ansioso para ver se você pode encontrar uma resposta para isso.

@ tim-phillips que é um excelente colapso. Ainda estou passando por isso, mas parece que vai me ajudar finalmente a entender, obrigado!

@ tim-phillips Obrigado por sua análise.

No entanto, você pode explicar o seguinte? Parece que sua solução ainda pode retornar quarta-feira, dependendo do fuso horário local do seu computador.

image

@tonylau não olha para a saída stringificada em seu console, que sempre será convertida em seu fuso horário local. Você deve usar os métodos getUTC*() para obter as partes da data, que serão as mesmas independentemente do seu fuso horário local.

@davidgoli

Eu configurei meu fuso horário para UTC + 14 (Kiritimati Island / Line Islands Time) e obtive o seguinte, mesmo quando estou usando getUTCDay (). O dia ainda é retornado como quarta-feira, quando deveria ser terça-feira?

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

image

Potencialmente relevante para esta discussão: https://github.com/tc39/proposal-temporal

@ tim-phillips obrigado !!!!!!!!!! isso foi tão útil !!!!!!! Você sabe como fazer o ics com a regra? Estou tendo o problema do dia novamente quando uso o pacote ics no node e quando recebo os convites do calendário, é um dia de folga. (Mas consigo colocar tudo no meu banco de dados corretamente por causa do seu código !! Muito obrigado.)

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

kirrg001 picture kirrg001  ·  5Comentários

spurreiter picture spurreiter  ·  3Comentários

berardo picture berardo  ·  9Comentários

Prinzhorn picture Prinzhorn  ·  15Comentários

fatshotty picture fatshotty  ·  5Comentários