3

尝试创建一个具有 VTIMEZONE 组件的 .ics 文件,该组件基于提供的时区动态设置标准时间和夏令时。

只是一个样本:

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

在我尝试解决这个问题时,我创建了一个moment.tz.zone(timezone)基于时刻https://momentjs.com/timezone/docs/#/zone-object/的文档的对象,我假设它包含必要的数据untils(应该是 TZOFFSETFROM、TZOFFSETTO)和offsets( DT 开始)。

然而,我找不到关于如何提取这些数据的明确文档。

想知道是否可以在 moment-timezone.js 中提取标准时间和日光的 DTSTART、TZOFFSETFROM 和 TZOFFSETTO

4

3 回答 3

4

您可以在此处下载预制的 VTIMEZONE 组件:

http://tzurl.org/

于 2019-02-06T14:33:33.210 回答
3

正如您在问题中已经提到的,您可以使用该moment.tz.zone(name)方法。这将为您提供一个Zone包含untils属性中时间戳列表的对象,然后您可以应用您的逻辑来获取您想要的时间戳VTIMEZONE(我untils在代码示例中使用了数组的第一个时间戳)。

您可以在时间戳上使用moment.tzandformat()来获取DTSTART. 您可以将ZZ令牌传递给以format()获取TZOFFSETFROM和的偏移量TZOFFSETTO

您可以使用abbrs值来获取TZNAME.

这是一个现场样本:

const MAX_OCCUR = 2;
const getVtimezoneFromMomentZone = (tzName) => {
  const zone = moment.tz.zone(tzName);
  const header = `BEGIN:VTIMEZONE\nTZID:${tzName}`;
  const footer = 'END:VTIMEZONE';
  
  let zTZitems = '';
  for(let i=0; i<MAX_OCCUR && i+1<zone.untils.length; i++){
    const type = i%2 == 0 ? 'STANDARD' : 'DAYLIGHT';
    const momDtStart = moment.tz(zone.untils[i], tzName);
    const momNext = moment.tz(zone.untils[i+1], tzName);
    const item = 
`BEGIN:${type}
DTSTART:${momDtStart.format('YYYYMMDDTHHmmss')}
TZOFFSETFROM:${momDtStart.format('ZZ')}
TZOFFSETTO:${momNext.format('ZZ')}
TZNAME:${zone.abbrs[i]}
END:${type}\n`;
    zTZitems += item;
  }
  const result = `${header}\n${zTZitems}${footer}\n`;
  return result;
};

console.log(getVtimezoneFromMomentZone('America/New_York'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.23.0/moment-with-locales.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.23/moment-timezone-with-data-2012-2022.min.js"></script>

于 2019-02-06T15:05:52.487 回答
0

以稳健的方式执行此操作有点挑战性。

概括

  • 用于RRULE避免使您的 ics 臃肿并支持长时间运行或开放式重复事件。
    • moment-timezone不会以任何方式公开底层的 zoneinfo 数据,这将使RRULE为给定区域构建变得容易(据我所知)。
  • 对于具有固定日期的一次性事件,您可以moment.tz.zone('America/New_York').untils根据事件日期选择正确的间隔以包含在 ics 中。

细节

例如:moment.tz.zone('America/New_York').untils包括从 1918 年到 2037 年的 235 个间隔(DAYLIGHTSTANDARD多年来)。
您不想将它们全部包含在您的 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和间隔所涵盖的事件日期有效。DAYLIGHTVTIMEZONE

另一个来自 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,解释这些STANDARDDAYLIGHT间隔何时再次发生的存在意味着我们不必明确添加多年来的所有特定间隔。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都有。存在允许他们避免包括更多的标准/日光间隔。DTSTARTRRULE

最佳解决方案

...可能是生成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
于 2021-08-14T18:23:03.717 回答