【问题标题】:Generate ics with dynamic VTIMEZONE using moment js使用 Moment js 生成具有动态 VTIMEZONE 的 ics
【发布时间】:2019-06-29 15:54:46
【问题描述】:

尝试创建一个具有 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)Object,它基于时刻 https://momentjs.com/timezone/docs/#/zone-object/ 的文档,我假设拥有必要的数据 untils(应该是 TZOFFSETFROM,TZOFFSETTO)和 offsets(DTSTART)。

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

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

【问题讨论】:

    标签: momentjs icalendar moment-timezone


    【解决方案1】:

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

    总结

    • 使用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,但它会非常臃肿。

    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
    

    关键是示例中的VTIMEZONEusing only the "DTSTART" property...在这种情况下,VTIMEZONE 仅对STANDARDDAYLIGHT 中明确列出的时间间隔所涵盖的事件日期有效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 的存在解释了这些 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 在 1996 年有一个 DTSTARTDAYLIGHT 在 1981 年有一个DTSTART,尽管事件发生在 2021 年。RRULE 的存在使他们能够避免包括更多标准/日光间隔。

    最佳解决方案

    ...可能是生成RRULE。这使您可以最大限度地减小 ics 文件的大小,同时支持远在未来的重复事件。

    缺点:我找不到任何简单的方法来生成 RRULEmoment-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
    

    【讨论】:

      【解决方案2】:

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

      您可以在时间戳上使用moment.tzformat() 来获取DTSTART。您可以将ZZ 令牌传递给format() 以获得TZOFFSETFROMTZOFFSETTO 的偏移量。

      您可以使用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>

      【讨论】:

      • 经过测试发现这个函数返回了错误的欧洲/马德里时区(还没有弄清楚原因)
      • 您的代码假设 DAYLIGHTSTANDARD 在迭代 zone.untils 时根据索引 (i) 是偶数还是奇数索引。有什么保证吗? Moment 的文档示例显示了在索引 0 处具有 DAYLIGHT 的一个案例,以及在索引 0 处具有 STANDARD 的另一个示例:herehere。我错过了什么吗?
      • 看起来您正在使用来自zone.abbrs[i]TZNAME,使用与DTSTART 相同的索引i(来自zone.untils[i])。这看起来是不正确的。 文档说的那一刻:abbrsuntilsoffsets 的长度都是一样的。任何索引处的 offsetabbr 仅在时间戳小于该索引处的 until 时才处于活动状态。朗读此内容的一种简单方法是“在untils[n-1]untils[n] 之间,abbr 应为abbrs[n],偏移量应为offsets[n]”。
      【解决方案3】:

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

      http://tzurl.org/

      【讨论】:

      • 但是请注意,在 IANA 数据库的更新和此处可用的时区之间经常存在延迟:例如,它目前落后 4 个版本。还要注意与 vzic 项目确实同步,因此某些时区可能无法正确转换:-(
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多