以稳健的方式执行此操作有点挑战性。
概括
- 用于
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,但它会非常臃肿。
VTIMEZONE 上的RFC 部分包含一些示例...
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
发生在 2021 年,但DTSTART
在 1996 年和1981 年DAYLIGHT
都有。存在允许他们避免包括更多的标准/日光间隔。DTSTART
RRULE
最佳解决方案
...可能是生成RRULE。这使您可以最小化 ics 文件的大小,同时支持远在未来的重复事件。
缺点:我找不到任何简单的方法来生成RRULE
...moment-timezone
但似乎还有一些其他的库可能会有所帮助(还没有玩过它们)。
如果有人有一些提示/经验生成RRULE
,很高兴听到您的经验。
选项 2:针对特定用例的解决方法
如果您正在为单个或重复事件动态生成 ICS 文件,并且您知道事件日期(或重复事件的日期范围),那么您只需过滤moment.tz.zone('America/New_York').untils
以确保您拥有涵盖您的所有时间STANDARD
和间隔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