Rrule: Kebingungan UTC-tapi-bukan-UTC

Dibuat pada 5 Apr 2019  ·  18Komentar  ·  Sumber: jakubroztocil/rrule

Saya menduga ini bukan masalah dengan aturan, tetapi dengan pemahaman saya tentangnya dan/atau dukungan zona waktunya. Saya sudah membaca bagian terkait dalam dokumentasi untuk rrule dan luxon .

Contoh kode

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

Keluaran sebenarnya

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

Keluaran yang diharapkan

Saya akan berpikir spesifikasi tzid akan memiliki beberapa efek pada output, tetapi output yang sama dihasilkan untuk setiap zona waktu.

Saya juga tidak bisa melihat apa korelasi antara waktu yang disediakan dalam aturan dan output untuk contoh acara berikutnya. Saya berharap bahwa contoh berikutnya secara kasar akan sesuai dengan salah satu dari dua jam yang diwakili oleh aturan yang disediakan tergantung pada bagaimana nilai dtstart dan tzid yang ditentukan ditafsirkan atau diterapkan.

versi aturan

2.6.0

Sistem operasi

OS X 10.14.2, Node 8.11.4

Zona waktu lokal

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

Komentar yang paling membantu

Saya pikir mudah untuk menggabungkan masalah dengan objek Date dan masalah menghitung kemunculan untuk RRULE , jadi saya akan membicarakannya secara terpisah sebelum menggabungkan keduanya. Saya harus menulis ini sebagian besar sehingga saya bisa memilah pemahaman saya tentang masalah ini dan berpikir saya akan memposting di sini kalau-kalau itu membantu orang lain.

Objek Tanggal JavaScript

Saya tidak setuju bahwa objek JS Date tidak teratur, tetapi ada kesalahpahaman umum dengan objek yang membuatnya tampak seperti itu. Ini membingungkan untuk sedikitnya.

Seperti yang disebutkan @hlee5zebra , tanggal yang dibuat dengan konstruktor Date selalu direpresentasikan dalam UTC. Mereka mungkin ditampilkan dalam waktu lokal, tetapi di balik layar mereka sebenarnya diwakili dalam waktu Unix Epoch. Kita selalu dapat menjalankan .getTime() atau .toISOString() pada objek Date diinstansiasi untuk mengingatkan diri kita sendiri bahwa kita sedang bekerja di UTC.

Implementasinya juga dengan benar mengikuti konvensi melakukan semua pemrosesan dalam UTC dan hanya mengonversi ke waktu lokal yang ditampilkan kepada pengguna. Kebetulan kami, para pengembang, adalah pengguna; tapi kemudian kami memikirkan pengguna kami sendiri yang membuat air menjadi keruh. Saat mengembangkan aplikasi kami, kami harus memastikan untuk mengikuti konvensi dan menyimpan tanggal sebagai UTC di backend kami (JS sudah memproses tanggal dalam UTC jadi kami baik-baik saja di sana). Jika Anda menyimpan tanggal sebagai UTC di penyimpanan data Anda, tanggal tersebut akan tiba di klien sebagai string ISO , diuraikan dengan benar oleh objek Date , dan ditampilkan dalam waktu lokal pengguna, semuanya tanpa melakukan pekerjaan berat apa pun.

Tulisan ini telah membantu saya memahami objek Date dan menemukan seberapa banyak yang dapat saya lakukan dengannya: https://www.toptal.com/software/definitive-guide-to-datetime-manipulation

RULE

Menghitung kejadian untuk RRULE menjadi rumit karena ada perbedaan mendasar dalam ekspektasi pengguna dan pemrosesan tanggal dalam UTC, terlihat ketika dua representasi waktu ini melewati batas hari.

Misalnya, jika pengguna kami menginginkan pengulangan yang terjadi setiap hari Selasa, mereka akan mengharapkan semua kejadian jatuh pada hari Selasa dalam waktu lokal mereka. Namun, semua perhitungan dilakukan dalam UTC, jadi RRULE seperti:

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

akan mengembalikan kemunculan pada hari Selasa di UTC, tetapi untuk setiap pengguna yang terletak secara signifikan di sebelah barat meridian utama, kemunculan akan muncul pada hari Senin dalam waktu setempat, yang jelas bukan hasil yang diharapkan siapa pun.

Melakukannya dengan benar

Semua yang dikatakan, apa yang harus dilakukan pengembang untuk menyelesaikannya adalah memaksa JavaScript untuk berpikir bahwa tanggal lokal sebenarnya adalah tanggal UTC, berikan tanggal "palsu" itu ke rrule.js , dan minta perpustakaan melakukan perhitungannya waktu UTC lokal ini. Saat Anda mendapatkan kemunculan dari metode .all() , Anda kemudian membuat instance objek Date menggunakan bagian "UTC" dari tanggal tersebut. Ini secara efektif membuat hasil sesuai dengan harapan.

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

Atau singkatnya:

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

Sejauh yang saya bisa lihat, Chrome, Firefox, dan Safari semua menampilkan tanggal sebagai string dalam waktu lokal saat masuk ke konsol, dan Node.js sebagai string ISO di UTC.

Semua 18 komentar

Saya juga sangat bingung.

Oke saya mengerti, ada paragraf kunci ini di readme:

image

Anda harus menggunakan dua fungsi pembantu ini untuk zona waktu lokal. Anda harus memodifikasi offset untuk mendapatkan offset untuk zona waktu X jika Anda ingin memodifikasi ini untuk mendukung 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;
}

Maka kode Anda akan berubah menjadi:

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

Saya menduga tidak akan ada cara yang baik untuk mengatasi ini di perpustakaan ini, dibangun seperti pada implementasi Date JS yang membingungkan dan tidak teratur. Saat ini saya sedang mengerjakan penulisan ulang yang lengkap, tetapi akan membutuhkan waktu untuk memperbaikinya.

Terima kasih @davidgoli karya Anda sangat dihargai!!!

Saya pikir kita membutuhkan seseorang yang mengerti untuk menulis artikel untuk membantu orang lain memahami penerapan tanggal yang tidak teratur. Sejujurnya saya masih tidak mengerti, saya hanya men-tweak hal-hal sampai berhasil.

Itu karena tanggal kembali yang Anda cetak ke konsol adalah tanggal JavaScript yang sedang diubah menjadi string, dan JavaScript secara otomatis mencetak Tanggal ke konsol di UTC (Lihat 'Z' di akhir stempel waktu Anda yang menunjukkan offset nol). Karena kejadian berikutnya di masing-masing dari empat kasus Anda terjadi pada waktu yang sama persis (walaupun di zona waktu yang berbeda), itu mencetak stempel waktu yang sama persis empat kali.

Perhatikan juga bahwa objek JavaScript Date melacak waktu menggunakan jumlah milidetik yang telah berlalu sejak 00:00:00 UTC, Kamis, 1 Januari 1970 (Perhatikan "UTC" di sana!), bukan oleh beberapa jenis interpretasi string "12:34:56" atau semacamnya.

@davidgoli

Saat ini saya sedang mengerjakan penulisan ulang yang lengkap, tetapi akan membutuhkan waktu untuk memperbaikinya.

Anda mungkin ingin memeriksa source rSchedule . Semua iterasi dilakukan dengan objek DateTime (bukan dari Luxon) yang mengambil input dan mengubahnya menjadi tanggal utc mengambang yang terlihat setara dengan input. Iterasi dilakukan dengan datetime UTC ini sehingga DST tidak menjadi masalah. Setelah kejadian ditemukan, sebelum diberikan kepada pengguna, kejadian tersebut diubah kembali ke tanggal normal di zona waktu yang sesuai.

Misalnya: tanggal ini

import { DateTime as LuxonDateTime } from 'luxon';

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

akan dikonversi ke tanggal UTC ini dan zona waktu akan disimpan

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

Perhatikan bahwa tanggal UTC ini terlihat mirip dengan tanggal Luxon (keduanya terlihat seperti 2019/1/1 ) tetapi, karena mereka berada di zona waktu yang berbeda, mereka sebenarnya tidak mewakili waktu yang sama.

Dikatakan demikian, iterasi dengan tanggal "UTC" khusus ini memungkinkan kita melakukan iterasi tanpa mengkhawatirkan waktu musim panas. Setelah tanggal yang benar ditemukan, kita dapat mengonversinya kembali ke tanggal luxon.

Sebagai contoh:

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

Ini membuat logika iterasi tetap ramah UTC sambil tetap mendukung zona waktu sewenang-wenang.

@thefliik saya mendapatkan primitif; itu minggu yang sakit kepala nyata di sini. Meskipun saya melihat rschedule juga telah menyepak (sejauh ini) pada dukungan byweekno & bysetpos ...

Oh, ya. byweekno sangat mengganggu.

ada berita?

Saya pikir mudah untuk menggabungkan masalah dengan objek Date dan masalah menghitung kemunculan untuk RRULE , jadi saya akan membicarakannya secara terpisah sebelum menggabungkan keduanya. Saya harus menulis ini sebagian besar sehingga saya bisa memilah pemahaman saya tentang masalah ini dan berpikir saya akan memposting di sini kalau-kalau itu membantu orang lain.

Objek Tanggal JavaScript

Saya tidak setuju bahwa objek JS Date tidak teratur, tetapi ada kesalahpahaman umum dengan objek yang membuatnya tampak seperti itu. Ini membingungkan untuk sedikitnya.

Seperti yang disebutkan @hlee5zebra , tanggal yang dibuat dengan konstruktor Date selalu direpresentasikan dalam UTC. Mereka mungkin ditampilkan dalam waktu lokal, tetapi di balik layar mereka sebenarnya diwakili dalam waktu Unix Epoch. Kita selalu dapat menjalankan .getTime() atau .toISOString() pada objek Date diinstansiasi untuk mengingatkan diri kita sendiri bahwa kita sedang bekerja di UTC.

Implementasinya juga dengan benar mengikuti konvensi melakukan semua pemrosesan dalam UTC dan hanya mengonversi ke waktu lokal yang ditampilkan kepada pengguna. Kebetulan kami, para pengembang, adalah pengguna; tapi kemudian kami memikirkan pengguna kami sendiri yang membuat air menjadi keruh. Saat mengembangkan aplikasi kami, kami harus memastikan untuk mengikuti konvensi dan menyimpan tanggal sebagai UTC di backend kami (JS sudah memproses tanggal dalam UTC jadi kami baik-baik saja di sana). Jika Anda menyimpan tanggal sebagai UTC di penyimpanan data Anda, tanggal tersebut akan tiba di klien sebagai string ISO , diuraikan dengan benar oleh objek Date , dan ditampilkan dalam waktu lokal pengguna, semuanya tanpa melakukan pekerjaan berat apa pun.

Tulisan ini telah membantu saya memahami objek Date dan menemukan seberapa banyak yang dapat saya lakukan dengannya: https://www.toptal.com/software/definitive-guide-to-datetime-manipulation

RULE

Menghitung kejadian untuk RRULE menjadi rumit karena ada perbedaan mendasar dalam ekspektasi pengguna dan pemrosesan tanggal dalam UTC, terlihat ketika dua representasi waktu ini melewati batas hari.

Misalnya, jika pengguna kami menginginkan pengulangan yang terjadi setiap hari Selasa, mereka akan mengharapkan semua kejadian jatuh pada hari Selasa dalam waktu lokal mereka. Namun, semua perhitungan dilakukan dalam UTC, jadi RRULE seperti:

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

akan mengembalikan kemunculan pada hari Selasa di UTC, tetapi untuk setiap pengguna yang terletak secara signifikan di sebelah barat meridian utama, kemunculan akan muncul pada hari Senin dalam waktu setempat, yang jelas bukan hasil yang diharapkan siapa pun.

Melakukannya dengan benar

Semua yang dikatakan, apa yang harus dilakukan pengembang untuk menyelesaikannya adalah memaksa JavaScript untuk berpikir bahwa tanggal lokal sebenarnya adalah tanggal UTC, berikan tanggal "palsu" itu ke rrule.js , dan minta perpustakaan melakukan perhitungannya waktu UTC lokal ini. Saat Anda mendapatkan kemunculan dari metode .all() , Anda kemudian membuat instance objek Date menggunakan bagian "UTC" dari tanggal tersebut. Ini secara efektif membuat hasil sesuai dengan harapan.

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

Atau singkatnya:

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

Sejauh yang saya bisa lihat, Chrome, Firefox, dan Safari semua menampilkan tanggal sebagai string dalam waktu lokal saat masuk ke konsol, dan Node.js sebagai string ISO di UTC.

@tim-phillips Saya juga menderita untuk ini sekarang dan mencari semacam tambalan monyet untuk menyiasatinya.

Saya memiliki acara berulang pada jam 8 malam EST yang berfungsi dengan baik, tetapi jam 9 malam EST mendorongnya kembali ke hari sebelumnya.

Menantikan untuk melihat apakah Anda dapat menemukan jawaban untuk ini.

@tim-phillips itu gangguan yang luar biasa. Saya masih melaluinya, tetapi sepertinya itu akan membantu saya akhirnya mengerti, terima kasih!

@tim-phillips Terima kasih atas analisis Anda.

Namun, dapatkah Anda menjelaskan hal berikut? Tampaknya solusi Anda masih dapat kembali pada hari Rabu tergantung pada zona waktu lokal komputer Anda.

image

@tonylau jangan melihat output string di konsol Anda, yang akan selalu dikonversi ke zona waktu lokal Anda. Anda harus menggunakan metode getUTC*() untuk mendapatkan bagian dari tanggal, yang akan sama terlepas dari zona waktu lokal Anda.

@davidgoli

Saya mengatur zona waktu saya ke UTC+14 (Kiritimati Island/Line Islands Time) dan mendapatkan yang berikut, bahkan ketika saya menggunakan getUTCDay(). Hari itu masih dikembalikan seperti Rabu padahal seharusnya Selasa?

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

image

Berpotensi relevan dengan diskusi ini: https://github.com/tc39/proposal-temporal

@tim-phillips terima kasih!!!!!!!!!! itu sangat membantu!!!!!!! Apakah Anda tahu cara membuat ics dengan aturan? Saya mengalami masalah hari lagi ketika saya menggunakan paket ics di node dan ketika saya menerima undangan kalender, itu adalah hari libur. (Tapi saya bisa memasukkan semuanya ke dalam db saya dengan benar karena kode Anda!! terima kasih banyak.)

Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

zeluspudding picture zeluspudding  ·  11Komentar

anthwinter picture anthwinter  ·  11Komentar

Prinzhorn picture Prinzhorn  ·  15Komentar

mapidemic picture mapidemic  ·  7Komentar

espen picture espen  ·  11Komentar