Guide
日历重复事件 API:RRULE 详解
每周例会在数据库里是一个事件,在屏幕上却是五十二个。每个日历系统都用重复规则——RFC 5545 中的 RRULE 字符串——来处理这个差距,而每个集成最终都会遇到同样的问题:规则语法是什么意思、由谁把系列展开成实例、单个发生实例被移动时会怎样。本指南回答这三个问题,并把 Google、Microsoft Graph 和 CalDAV 的具体差异并排呈现。
Written by Hazik Director of Product Management
本指南使用的命令参考: nylas calendar events list 和 nylas calendar events show。
什么是 RRULE?
RRULE 是 RFC 5545 (iCalendar 规范)中定义的重复规则语法,用一个字符串描述一个重复日程。一条 规则取代数百行存储记录:事件只存在一次,各个发生实例由软件推导得出。三大主流 日历系统要么直接存储 RRULE,要么在系统边界处与它相互转换。
这套语法只有一个必需部分 FREQ,外加一组修饰符。你实际会用到的 6 个 部分见下表——而 RFC 中有一条约束比它们都更重要:规范规定 UNTIL 和 COUNT 两部分“MUST NOT occur in the same 'recur'”(不得出现在同一条 重复规则中)。一条规则的结束方式只有 3 种:永不结束、发生 N 次后结束,或在某个 日期结束。
| 部分 | 含义 | 示例 |
|---|---|---|
FREQ | 基础频率(必需) | FREQ=WEEKLY |
INTERVAL | 每 N 个周期一次(默认为 1) | INTERVAL=2 ——隔周 |
BYDAY | 星期选择器;支持序数 | BYDAY=MO,WE,FR 或 BYDAY=-1FR(最后一个星期五) |
BYMONTHDAY | 月内日期选择器 | BYMONTHDAY=15 |
COUNT | 发生 N 次后结束 | COUNT=12 |
UNTIL | 在某个 UTC 时间戳结束 | UNTIL=20261231T235959Z |
# Every Monday and Wednesday for 12 occurrences
RRULE:FREQ=WEEKLY;BYDAY=MO,WE;COUNT=12
# Biweekly Friday, forever
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=FR
# Last Friday of each month through 2026
RRULE:FREQ=MONTHLY;BYDAY=-1FR;UNTIL=20261231T235959Z由谁把系列展开成实例?
展开——把一条规则变成带日期的发生实例——在每个 API 上发生的位置都不同,而这个 位置决定了你要写多少重复事件代码。Google 在收到请求时于服务端展开,Graph 存储 结构化对象并暴露 instances 视图,CalDAV 则原样下发规则、把计算留给客户端。这 3 种模型简述如下:
- Google Calendar API ——向
events.list传入singleEvents=true,响应就会包含时间窗口 内的各个实例,每个实例带有一个recurringEventId。Google 的 重复事件指南 介绍了主事件与实例的模型。 - Microsoft Graph ——事件存储一个 patternedRecurrence 对象,由
pattern(频率)和range(边界)组成,而不是原始的 RRULE 字符串;实例展开来自系列主事件的instances视图,由开始和结束 参数限定范围。 - CalDAV ——服务器原样返回带 RRULE 的 VEVENT,由客户端负责展开,包括时区计算。在没有辅助的情况下把这件事做对是最难 的路径;RFC 5545 的重复规则章节长达数十页不是没有原因的。
实用法则:如果你的集成只需要回答“本周会发生什么”,优先选择会替你展开的 API (或工具)。编写一个正确的客户端展开器是一项工程,而不是一个函数。关于这 3 个 API 在重复事件之外的差异(认证、空闲状态、Webhook),参见 日历 API 对比;本页只聚焦重复事件问题。
单个发生实例被移动或取消时会怎样?
被修改的单次发生会变成例外(exception):一条独立记录,只在某一个日期覆盖规则, 系列其余部分仍跟随主事件。每个系统的编码方式各不相同——Google 创建一个带 originalStartTime 字段的独立事件,Graph 将其建模为 exception 类型的发生实例,iCalendar 则添加第二个 VEVENT,其 RECURRENCE-ID 与被 移动的实例对应。
例外正是简单同步代码出问题的地方。大多数 bug 来自两种失败模式:把例外当成全新 事件(会议出现两次:一次来自规则展开,一次来自覆盖记录),以及在例外之上应用 系列级编辑(用户一次性的会议室调整被悄悄还原)。同步逻辑在执行任一路径之前都 必须先检查例外标记——这个两分支检查比一张重复会议的支持工单便宜得多。
如何从 CLI 读取重复事件?
nylas calendar events list 命令返回你所请求窗口内已展开的实例,因此 每周系列会以带日期的发生实例呈现,你这边无需做任何 RRULE 解析。重复系列的每个 实例都带有一个 master_event_id 链接回系列主事件,实例自身的 ID 中 嵌入了它的发生时间戳——两者都能在 JSON 输出中看到。
# Expanded instances for the next 30 days
nylas calendar events list --days 30 --json | jq '.[] | {
id: .id,
title: .title,
master: .master_event_id,
start: .when.start_time
}'
# Group instances by series to spot every recurring meeting
nylas calendar events list --days 30 --json | jq '
[.[] | select(.master_event_id != null)]
| group_by(.master_event_id)
| map({series: .[0].title, occurrences: length})'同样这 2 条命令对 Google、Microsoft 和 iCloud 账户都适用,这就在读取路径上消除了 上一节中各 API 的展开差异。一个坦诚的限制:用 CLI 的 calendar events create 命令创建事件时不支持重复规则标志——系列创建仍要在服务商的 UI 或 API 中完成, CLI 负责读取、过滤和脚本化这一侧。
为什么重复事件会在夏令时前后发生偏移?
每周 9:00 的会议指的是某个命名时区里的 9:00,而不是固定的 UTC 偏移量——因此每年 有两次,它的 UTC 时间会移动一小时。按原始 UTC 展开的规则会在每次 DST 切换后漂移; 正确的展开要通过时区数据库解析每次发生(用 America/Toronto,而不是 UTC-5)。这是客户端 CalDAV 展开之所以困难的主要原因,也体现在 RFC 5545 的要求中:UNTIL 时间戳必须是 UTC,而事件开始时间携带时区引用。
排查在 3 月或 11 月出现的同步 bug 时,先检查展开器用的是哪个时钟。上面的实例 JSON 在时间段事件上包含 start_timezone 字段,所以用一行 jq 过滤出 时区与预期不符的实例,就能很快定位发生漂移的系列。
下一步
- 日历 API 对比:Google、Microsoft、CalDAV ——认证、空闲状态和重复规则差异一站式对比
- 跨服务商同步日历 ——重复事件例外最容易出问题的地方
- 从 CLI 管理 Google Calendar ——Google 特有的事件工作流
- 从 CLI 管理 Yahoo 日历 ——在 Yahoo 基于 CalDAV 的日历上读取重复事件
- 完整命令参考 ——每个 calendar events 标志均有文档