要以稳健的方式执行此操作有点挑战性。
总结
- 使用
RRULE 避免使您的 ics 臃肿,并支持长时间运行或无限制的重复事件。
-
moment-timezone 不会以任何方式公开底层 zoneinfo 数据,这使得为给定区域构建 RRULE 变得容易(据我所知)。
- 对于具有固定日期的一次性活动,您可以根据活动日期从
moment.tz.zone('America/New_York').untils 中选择正确的时间间隔以包含在 ics 中。
详情
例如:moment.tz.zone('America/New_York').untils 包括从 1918 年到 2037 年的 235 个区间(DAYLIGHT 或 STANDARD 多年来)。
您不想将它们全部包含在您的 ics 中。
如果您只在 VTIMEZONE 中包含前两个,它将无效,除了 1918/1919 中的某些事件。
var timezoneName = 'America/New_York',
{untils, abbrs, offsets} = moment.tz.zone(timezone);
console.log(untils.length);
// 236
console.log(moment.tz(untils[0], timezoneName).format('YYYY-MM-DD HH:mm:ss'));
// 1918-03-31 03:00:00
console.log(moment.tz(untils[untils.length-2], timezoneName).format('YYYY-MM-DD HH:mm:ss'));
// 2037-11-01 01:00:00
console.log(untils[untils.length-1]);
// Infinity
您可以将所有 235 个这些间隔放入 ICS,但它会非常臃肿。
RFC section on VTIMEZONE 包含一些示例...
This is an example showing time zone information for New York City
using only the "DTSTART" property. Note that this is only
suitable for a recurring event that starts on or later than March
11, 2007 at 03:00:00 EDT (i.e., the earliest effective transition
date and time) and ends no later than March 9, 2008 at 01:59:59 EST (i.e., latest valid date and time for EST in this scenario).
For example, this can be used for a recurring event that occurs
every Friday, 8:00 A.M.-9:00 A.M., starting June 1, 2007, ending
December 31, 2007,
BEGIN:VTIMEZONE
TZID:America/New_York
LAST-MODIFIED:20050809T050000Z
BEGIN:STANDARD
DTSTART:20071104T020000
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:20070311T020000
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
END:DAYLIGHT
END:VTIMEZONE
关键是示例中的VTIMEZONE 是using only the "DTSTART" property...在这种情况下,VTIMEZONE 仅对STANDARD 和DAYLIGHT 中明确列出的时间间隔所涵盖的事件日期有效VTIMEZONE.
RFC 中的另一个示例...
This is a simple example showing the current time zone rules for
New York City using a "RRULE" recurrence pattern. Note that there
is no effective end date to either of the Standard Time or
Daylight Time rules. This information would be valid for a
recurring event starting today and continuing indefinitely.
BEGIN:VTIMEZONE
TZID:America/New_York
LAST-MODIFIED:20050809T050000Z
TZURL:http://zones.example.com/tz/America-New_York.ics
BEGIN:STANDARD
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
END:DAYLIGHT
END:VTIMEZONE
请注意,在这种情况下,RRULE 的存在解释了这些 STANDARD 和 DAYLIGHT 间隔何时再次出现意味着我们不必多年来明确添加所有特定间隔。您只需要RRULE 更改的最近(在您的事件之前)间隔。如果您的事件重复发生并且跨越规则更改,那么您必须在相应规则中包含更多间隔,以涵盖规则更改之前的事件以及规则更改之后的事件。
实际上,检查 Apple 的 macOS 日历应用程序生成的 ICS 以查找 2021 年 8 月 19 日时区 Europe/Berlin 的事件包括以下 VTIMEZONE(为了便于阅读而缩进)...
BEGIN:VTIMEZONE
TZID:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
DTSTART:19810329T020000
TZNAME:GMT+2
TZOFFSETTO:+0200
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
DTSTART:19961027T030000
TZNAME:GMT+1
TZOFFSETTO:+0100
END:STANDARD
END:VTIMEZONE
请注意,STANDARD 在 1996 年有一个 DTSTART,DAYLIGHT 在 1981 年有一个DTSTART,尽管事件发生在 2021 年。RRULE 的存在使他们能够避免包括更多标准/日光间隔。
最佳解决方案
...可能是生成RRULE。这使您可以最大限度地减小 ics 文件的大小,同时支持远在未来的重复事件。
缺点:我找不到任何简单的方法来生成 RRULE 和 moment-timezone... 但周围似乎有一些其他库可能会有所帮助(尚未使用它们)。
如果有人有一些提示/经验生成RRULEs,很高兴听到您的经验。
选项 2:针对特定用例的解决方法
如果您要为单个或重复事件动态生成 ICS 文件,并且您知道事件日期(或重复事件的日期范围),那么您只需过滤 moment.tz.zone('America/New_York').untils 以确保您拥有所有 @ 987654355@ 和 DAYLIGHT 间隔,您需要涵盖您的活动日期/范围。
缺点:对于长时间运行或开放式重复事件,这可能不是一个好的选择,因为 ics 文件中必须包含太多间隔(膨胀)。
但是对于单一的、固定日期的事件,这可能是一个不错的选择。
选项 2 的快速示例...
我只是对 RFC 进行了粗略的扫描,为了安全起见,我在结束日期之后包含了转换,因此即使您在单个时间戳上有一个事件,您也将始终至少有 2 个转换。一个发生在事件日期之前的转换,一个发生在事件日期之后的转换。这可能没有必要。
function generateVTimezone (timezoneName, tsRangeStart, tsRangeEnd) {
var zone = moment.tz.zone(timezoneName),
{untils, abbrs, offsets} = zone,
i, dtStart, utcOffsetBefore, utcOffsetDuring, periodType,
vtz = [
`BEGIN:VTIMEZONE`,
`TZID:${timezoneName}`,
];
tsRangeStart = tsRangeStart || 0;
tsRangeEnd = tsRangeEnd || Math.pow(2,31)-1;
// https://momentjs.com/timezone/docs/#/data-formats/unpacked-format/
// > between `untils[n-1]` and `untils[n]`, the `abbr` should be
// > `abbrs[n]` and the `offset` should be `offsets[n]`
for (i=0; i<untils.length - 1; i++) {
// filter to intervals that include our start/end range timestamps
if (untils[i+1] < tsRangeStart) continue; // interval ends before our start, skip
if (i>0 && untils[i-1] > tsRangeEnd) break; // interval starts after interval we end in, break
utcOffsetBefore = formatUtcOffset(offsets[i]); // offset BEFORE dtStart
dtStart = moment.tz(untils[i], timezoneName).format('YYYYMMDDTHHmmss');
utcOffsetDuring = formatUtcOffset(offsets[i+1]); // offset AFTER dtStart
periodType = offsets[i+1] < offsets[i] ? 'DAYLIGHT' : 'STANDARD'; // spring-forward, DAYLIGHT, fall-back: STANDARD.
vtz.push(`BEGIN:${periodType}`);
vtz.push(`DTSTART:${dtStart}`); // local date-time when change
vtz.push(`TZOFFSETFROM:${utcOffsetBefore}`); // utc offset BEFORE DTSTART
vtz.push(`TZOFFSETTO:${utcOffsetDuring}`); // utc offset AFTER DTSTART
vtz.push(`TZNAME:${abbrs[i+1]}`);
vtz.push(`END:${periodType}`);
}
vtz.push(`END:VTIMEZONE`);
return vtz.join('\r\n'); // rfc5545 says CRLF
}
function formatUtcOffset(minutes) {
var hours = Math.floor(Math.abs(minutes) / 60).toString(),
mins = (Math.abs(minutes) % 60).toString(),
sign = minutes > 0 ? '-' : '+', // sign inverted, see https://momentjs.com/timezone/docs/#/zone-object/offset/
output = [sign];
// zero-padding
if (hours.length < 2) output.push('0');
output.push(hours);
if (mins.length < 2) output.push('0');
output.push(mins);
return output.join('');
}
function test() {
var timezone = 'America/New_York',
startTS = moment.tz('2013-11-18 11:55', timezone).unix()*1000,
endTS = moment.tz('2013-11-18 11:55', timezone).unix()*1000;
console.log(generateVTimezone(timezone, startTS, endTS));
}
test();
产生输出...
BEGIN:VTIMEZONE
TZID:America/New_York
BEGIN:STANDARD
DTSTART:20131103T010000
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:20140309T030000
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
END:DAYLIGHT
END:VTIMEZONE