Rrule: Путаница с UTC, но не с UTC

Созданный на 5 апр. 2019  ·  18Комментарии  ·  Источник: jakubroztocil/rrule

Я подозреваю, что это проблема не в rrule, а в моем понимании этого правила и / или его поддержке часовых поясов. Я уже читал соответствующие разделы в документации для rrule и luxon .

Пример кода

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

Фактический выход

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

Ожидаемый результат

Я бы подумал, что спецификация tzid окажет некоторое влияние на вывод, но один и тот же вывод создается для каждого часового пояса.

Я также не могу увидеть, какова корреляция между временем, указанным в правилах, и выводом для следующего экземпляра события. Я ожидал, что следующий экземпляр будет примерно соответствовать одному из двух часов, представленных предоставленными правилами, в зависимости от того, как указанные значения dtstart и tzid интерпретируются или применяются.

Правильная версия

2.6.0

Операционная система

OS X 10.14.2, узел 8.11.4

Местный часовой пояс

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

Самый полезный комментарий

Я думаю, что легко объединить проблемы с объектом Date и проблемами вычисления вхождений для RRULE , поэтому я собираюсь поговорить о них отдельно, прежде чем объединить их. Мне пришлось написать это в основном, чтобы я мог разобраться в своем понимании проблемы, и решил, что опубликую здесь, если это поможет другим.

Объект даты JavaScript

Я не согласен с тем, что объект JS Date является неправильным, но есть распространенные недоразумения с объектом, из-за которых он кажется таким. Это, мягко говоря, сбивает с толку.

Как упоминалось в @ hlee5zebra , даты, созданные с помощью конструктора Date , всегда представлены в формате UTC. Они могут отображаться в местном времени, но за кадром они фактически представлены во времени Unix Epoch. Мы всегда можем запустить .getTime() или .toISOString() на созданном экземпляре объекта Date чтобы напомнить себе, что мы работаем в формате UTC.

Реализация также правильно следует соглашению о выполнении всей обработки в формате UTC и преобразовании только в местное время, отображаемое для пользователя. Так уж получилось, что мы, разработчики, являемся пользователем; но тогда у нас есть собственный пользователь, который мутит воду. При разработке наших приложений мы должны следить за соблюдением соглашения и сохранять даты в формате UTC в нашем бэкэнде (JS уже обрабатывает даты в формате UTC, так что у нас все хорошо). Если вы храните даты в формате UTC в своем хранилище данных, они должны поступать к клиенту в виде строк ISO , правильно анализироваться объектом Date и отображаться в локальном времени пользователя, и все это без каких-либо сложных действий.

Эта статья помогла мне понять объект Date и узнать, как много я могу с ним сделать: https://www.toptal.com/software/definitive-guide-to-datetime-manipulation

RRULE

Вычисление вхождений для RRULE становится сложным, потому что существует фундаментальная разница в ожиданиях пользователей и обработке дат в формате UTC, заметная, когда эти два представления времени пересекают дневной барьер.

Например, если наш пользователь хочет повторения, которое происходит каждый вторник, он будет ожидать, что все вхождения будут приходиться на вторник по их местному времени. Однако все расчеты выполняются в формате UTC, поэтому RRULE выглядит так:

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

будет возвращать вхождения по вторникам по всемирному координированному времени, но для любого пользователя, расположенного значительно западнее нулевого меридиана, вхождения будут появляться по понедельникам по местному времени, что, очевидно, не является тем результатом, на который кто-либо надеется.

Правильно

Все, что было сказано, что разработчик должен сделать, чтобы решить эту проблему, - это заставить JavaScript думать, что локальная дата на самом деле является датой в формате UTC, присвоить эту «фальшивую» дату rrule.js и попросить библиотеку выполнять свои вычисления в это местное время в формате UTC. Когда вы получаете экземпляры из метода .all() , вы затем создаете экземпляры новых объектов Date используя части этих дат в формате «UTC». Это фактически приводит к тому, что результаты соответствуют ожиданиям.

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

Или вкратце:

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

Насколько я понимаю, все Chrome, Firefox и Safari отображают даты в виде строк по местному времени при входе в консоль, а Node.js - в виде строк ISO в UTC.

Все 18 Комментарий

Я тоже ужасно запутался.

Хорошо, я понял, в ридми был такой ключевой абзац:

image

Вы должны использовать эти две вспомогательные функции для местного часового пояса. Вам нужно будет изменить смещение, чтобы получить смещение для часового пояса X, если вы хотите изменить его для поддержки 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;
}

Тогда ваш код изменится на:

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

Я подозреваю, что в этой библиотеке, построенной на запутанной и нестандартной реализации Date , не будет хорошего способа обойти это. В настоящее время я работаю над полным переписыванием, но потребуется некоторое время, чтобы все исправить.

Спасибо @davidgoli, твоя работа очень ценится !!!

Я думаю, нам нужен кто-то, кто понимает, чтобы написать статью, чтобы помочь другим понять реализацию нестандартной даты. Честно говоря, я до сих пор не понимаю, я просто поправлял вещи, пока это не сработало.

Это было бы потому, что возвращаемая дата, которую вы печатаете на консоли, является датой JavaScript, которая преобразуется в строку, а JavaScript автоматически печатает даты на консоли в формате UTC (см. 'Z' в конце ваших временных меток, который обозначает нулевое смещение). Поскольку следующие события в каждом из ваших четырех случаев происходят в одно и то же время (хотя и в разных часовых поясах), он печатает одну и ту же метку времени четыре раза.

Также обратите внимание, что объект JavaScript Date отслеживает время, используя количество миллисекунд, прошедших с 00:00:00 UTC, четверг, 1 января 1970 г. (обратите внимание на "UTC"!), А не некоторыми своего рода строковая интерпретация "12:34:56 pm" или что-нибудь в этом роде.

@davidgoli

В настоящее время я работаю над полным переписыванием, но потребуется некоторое время, чтобы все исправить.

Возможно, вы захотите проверить исходный код rSchedule . Вся итерация выполняется с помощью специального объекта DateTime (не от Luxon), который принимает входные данные и преобразует их в плавающую дату в формате utc, которая выглядит эквивалентной входной. Итерация выполняется с этой датой и временем в формате UTC, поэтому переход на летнее время не является проблемой. После обнаружения вхождения, прежде чем оно будет передано пользователю, оно преобразуется обратно в обычную дату в соответствующем часовом поясе.

Например: эта дата

import { DateTime as LuxonDateTime } from 'luxon';

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

будет преобразован в эту дату в формате UTC, а часовой пояс будет сохранен

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

Обратите внимание, что эта дата в формате UTC похожа на дату Luxon (они оба выглядят как 2019/1/1 ), но, поскольку они находятся в разных часовых поясах, они фактически не представляют одно и то же время.

При этом итерация с этой специальной датой «UTC» позволяет нам выполнять итерацию, не беспокоясь о переходе на летнее время. После того, как правильная дата найдена, мы можем преобразовать ее обратно в дату люксона.

Например:

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

Это сохраняет логику итераций, удобную для UTC, при этом поддерживая произвольные часовые пояса.

@thefliik Я достал примитивы; это недели, которые являются настоящей головной болью здесь. Хотя я вижу, что rschedule также (до сих пор) указывал на поддержку byweekno и bysetpos ...

Ага. byweekno очень раздражает.

любые новости?

Я думаю, что легко объединить проблемы с объектом Date и проблемами вычисления вхождений для RRULE , поэтому я собираюсь поговорить о них отдельно, прежде чем объединить их. Мне пришлось написать это в основном, чтобы я мог разобраться в своем понимании проблемы, и решил, что опубликую здесь, если это поможет другим.

Объект даты JavaScript

Я не согласен с тем, что объект JS Date является неправильным, но есть распространенные недоразумения с объектом, из-за которых он кажется таким. Это, мягко говоря, сбивает с толку.

Как упоминалось в @ hlee5zebra , даты, созданные с помощью конструктора Date , всегда представлены в формате UTC. Они могут отображаться в местном времени, но за кадром они фактически представлены во времени Unix Epoch. Мы всегда можем запустить .getTime() или .toISOString() на созданном экземпляре объекта Date чтобы напомнить себе, что мы работаем в формате UTC.

Реализация также правильно следует соглашению о выполнении всей обработки в формате UTC и преобразовании только в местное время, отображаемое для пользователя. Так уж получилось, что мы, разработчики, являемся пользователем; но тогда у нас есть собственный пользователь, который мутит воду. При разработке наших приложений мы должны следить за соблюдением соглашения и сохранять даты в формате UTC в нашем бэкэнде (JS уже обрабатывает даты в формате UTC, так что у нас все хорошо). Если вы храните даты в формате UTC в своем хранилище данных, они должны поступать к клиенту в виде строк ISO , правильно анализироваться объектом Date и отображаться в локальном времени пользователя, и все это без каких-либо сложных действий.

Эта статья помогла мне понять объект Date и узнать, как много я могу с ним сделать: https://www.toptal.com/software/definitive-guide-to-datetime-manipulation

RRULE

Вычисление вхождений для RRULE становится сложным, потому что существует фундаментальная разница в ожиданиях пользователей и обработке дат в формате UTC, заметная, когда эти два представления времени пересекают дневной барьер.

Например, если наш пользователь хочет повторения, которое происходит каждый вторник, он будет ожидать, что все вхождения будут приходиться на вторник по их местному времени. Однако все расчеты выполняются в формате UTC, поэтому RRULE выглядит так:

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

будет возвращать вхождения по вторникам по всемирному координированному времени, но для любого пользователя, расположенного значительно западнее нулевого меридиана, вхождения будут появляться по понедельникам по местному времени, что, очевидно, не является тем результатом, на который кто-либо надеется.

Правильно

Все, что было сказано, что разработчик должен сделать, чтобы решить эту проблему, - это заставить JavaScript думать, что локальная дата на самом деле является датой в формате UTC, присвоить эту «фальшивую» дату rrule.js и попросить библиотеку выполнять свои вычисления в это местное время в формате UTC. Когда вы получаете экземпляры из метода .all() , вы затем создаете экземпляры новых объектов Date используя части этих дат в формате «UTC». Это фактически приводит к тому, что результаты соответствуют ожиданиям.

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

Или вкратце:

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

Насколько я понимаю, все Chrome, Firefox и Safari отображают даты в виде строк по местному времени при входе в консоль, а Node.js - в виде строк ISO в UTC.

@ tim-phillips Я тоже сейчас страдаю из-за этого и ищу какой-то обезьяний патч, чтобы обойти это.

У меня есть повторяющееся событие в 8 вечера EST, которое работает нормально, но 9 PM EST отодвигает его на день раньше, чем он должен быть.

С нетерпением жду ответа, сможете ли вы найти ответ на этот вопрос.

@ tim-phillips, это отличная поломка. Я все еще прохожу через это, но, похоже, это поможет мне наконец понять, спасибо!

@ tim-phillips Спасибо за ваш анализ.

Однако можно ли объяснить следующее? Кажется, ваше решение все еще может вернуться в среду в зависимости от местного часового пояса вашего компьютера.

image

@tonylau не смотрит на строковый вывод в вашей консоли, который всегда будет преобразован в ваш местный часовой пояс. Вы должны использовать методы getUTC*() чтобы получить части даты, которые будут одинаковыми независимо от вашего местного часового пояса.

@davidgoli

Я установил свой часовой пояс на UTC + 14 (время острова Киритимати / острова Лайн) и получил следующее, даже когда я использую getUTCDay (). День по-прежнему возвращается как среда, а должен быть вторник?

Песочница: https://codesandbox.io/s/rrule-localutc-conversion-in-short-0uzt1

image

Потенциально актуально для этого обсуждения: https://github.com/tc39/proposal-temporal

@ tim-phillips спасибо !!!!!!!!!! это было так полезно !!!!!!! Вы случайно не знаете, как делать картинки с рулем? У меня снова возникает дневная проблема, когда я использую пакет ics в узле, и когда я получаю приглашения от календаря, это выходной день. (Но я могу правильно поместить все в свою базу данных из-за вашего кода !! Большое вам спасибо.)

Была ли эта страница полезной?
0 / 5 - 0 рейтинги

Смежные вопросы

shorlbeck picture shorlbeck  ·  21Комментарии

jimmywarting picture jimmywarting  ·  9Комментарии

kirrg001 picture kirrg001  ·  5Комментарии

zeluspudding picture zeluspudding  ·  11Комментарии

mapidemic picture mapidemic  ·  7Комментарии