提醒:终极个人日历

作者:David F. Skoll

Remind 是一款适用于 Linux 和大多数 UNIX 系统的日历和提醒程序。我于 1989 年开始编写 Remind,因为我对 UNIX 日历程序的局限性感到厌烦。在过去的十年里,它积累了许多功能,并已成为我见过的最复杂的日历程序之一。我想带您踏上 Remind 的历史之旅,展示一些使我敢于自诩它为“终极个人日历”的功能。

最初:日历

首先,让我们从历史悠久的 UNIX calendar 程序开始。此程序只是扫描文本文件,查找包含看起来像日期的行。它打印包含今天或明天日期的任何行(其中星期一被认为是相对于星期五的“明天”)。您还可以安排将提醒邮件发送给您。

嗯,这对于非常简单的事情来说很好。但是,没有重复提醒的规定,因此您无法(例如)提醒自己每个月的第一天要做什么。

下一个更高级的步骤是 cron。这使您可以用相对复杂的格式指定日期,因此您可以提醒自己每天、每周或每月的事件。但是,cron 仍然无法处理相当简单的事情,例如每月第一个星期三发生的事件。

Remind 的诞生

为了弥补这些缺点,我开始着手开发 Remind。在其最简单的形式中,Remind 类似于 calendar,因为它读取文本文件并将提醒发送到标准输出。但是,Remind 使用了一种复杂的日期指定语言,可以发出非常复杂的提醒。尽管该语言很复杂,但在简单情况下仍然非常直观。让我们看一些来自 Remind 脚本文件的示例

REM 6 January MSG David's birthday.

这个例子非常简单明了。每年的 1 月 6 日,它都会提醒我我的生日。但是,假设我需要提前警告我妻子的生日,以便我有时间为她买礼物。试试这个

REM 20 December +7 MSG Norine's birthday is %b.
稍微复杂一点,但仍然可以理解:+7 表示 Remind 在提前七天开始警告我。%b 是一个特殊的替换序列,它的工作方式如下
  • 在 12 月 13 日,%b 被替换为“7 天后”

  • 在 12 月 19 日,%b 被替换为“明天”

  • 在 12 月 20 日,%b 被替换为“今天”

因此,我们已经有一些不错的功能:可选择的提前警告量以及消息正文,该正文根据提醒的“触发日期”相对于今天的日期而变化。
更复杂的提醒

再来一些更棘手的提醒怎么样?我当地的 Linux 用户组在每个月的第一个星期三聚会。我们如何在 Remind 中表达这一点?

REM Wed 1 MSG OCLUG Meeting.

它是如何工作的?Wed 被识别为工作日标记,1 被识别为月份中的日期标记。如果这两个标记都存在,则触发日期是月份中的日期当天或之后的第一个工作日。如果您指定多个工作日,则使用第一个匹配的工作日。这里有更多示例

REM Wed MSG Issued every Wednesday.
REM Mon Tue Wed Thu Fri MSG Issued every working\
   day.
REM Mon Tue Wed Thu Fri 1 MSG Issued first\
  working day of the month.
REM Sat Sun 1 June MSG Issued first weekend day\
  in June.
看似更困难的事情,例如一个月中第一个星期一之后的第一个星期二,实际上很容易:稍加思考就会发现,一个月中第一个星期一之后的第一个星期二实际上就是该月 2 号或之后的第一个星期二
REM Tue 2 MSG Presto! First Tuesday-after-Monday.
一个月中的最后一个星期一像这样处理
REM Mon 1 --7 MSG Last Monday of a month.
这个有点棘手:Mon 1 部分是一个月中的第一个星期一,但是 --7 表示“倒退 7 天”。因此,该提醒在每个月第一个星期一的前七天触发——这恰好是上个月的最后一个星期一。--n 序列可以解决一些与“一个月中的最后一个某物”相关的棘手问题。
节假日

到目前为止,任何商业日历软件包都可以与 Remind 相媲美,但现在我们开始领先于其他软件。

大多数日历程序最令人恼火的事情之一是它们如何处理节假日。假设您每周四都有会议,但如果是节假日则没有。典型的日历程序仍然会继续提醒您。在加拿大,7 月 1 日是节假日,而 1999 年 7 月 1 日是星期四。看看这段脚本片段

OMIT 1 July MSG Canada Day.
REM Thursday SKIP MSG Meeting.

OMIT 行告诉 Remind 7 月 1 日是节假日,并且它还在该日期打印一条友好的消息。REM 行中的 SKIP 标记告诉 Remind 如果提醒落在节假日,则跳过该提醒。因此,提醒将在 1999 年 6 月 24 日和 1999 年 7 月 8 日触发,但不会在 1999 年 7 月 1 日触发。

还有其他变体

REM Thursday BEFORE MSG Meeting moved to preceding\
   Wednesday if Thursday is a holiday.
REM Thursday AFTER MSG Meeting moved to next\
   Friday if Thursday is a holiday.

Remind 具有相当复杂的机制,可以根据节假日和周末调整提醒;请阅读手册以获取更多信息。

GUI,GUI,GUI!

Remind: The Ultimate Personal Calendar

图 1. 主要 TK Remind 窗口

此时,您可能会感到头晕。您不想再学习另一种命令行语言或晦涩的配置文件格式。您渴望您的微软同事使用的 GUI。没问题;Remind 附带一个名为 TkRemind 的图形前端,用 Tcl/Tk 编写。TkRemind 提供了一个图形日历,并允许您使用简单的图形输入框输入提醒。图 1 显示了主要的 TkRemind 窗口,图 2 显示了提醒输入框。

Remind: The Ultimate Personal Calendar

图 2. 提醒输入框

使用 TkRemind,您永远不必学习 Remind 的脚本语言,只要您可以使用 GUI 表达您需要的所有提醒。但是,我们鼓励您学习编写 Remind 脚本;从 GUI 中,只需单击“预览提醒”即可查看将实现您的提醒的 Remind 代码。

定时提醒

GUI 还暗示了“定时提醒”的存在。这是一种指定了一天中时间的提醒。您可以安排 Remind 在重要会议之前弹出提醒,或者更重要的是,提醒您回家。

这是一个定时提醒的示例

REM Mon Tue Wed Thu Fri AT 17:00 +15 *3 MSG Go home!

AT 关键字引入了一个“AT 子句”。17:00 表示触发时间是下午 5:00。+15 表示 Remind 在提前十五分钟开始提醒您,*3 表示它每三分钟烦扰您一次。

TkRemind 前端以特殊的“守护程序模式”运行 Remind,以便像前面示例中的定时提醒在 X 窗口中弹出。

超级高级脚本

虽然到目前为止我们所看到的已经很酷了,但仍然存在顽固的异常提醒,需要更强大的脚本来处理。以美国的 7 月 4 日为例。如果这一天是星期六,则前一个星期五是节假日。如果这一天是星期日,则下一个星期一是节假日。否则,7 月 4 日本身就是节假日。我甚至不打算解释这段脚本;请自己获取手册,并成为一名硬核 Remind 程序员。

列表 1 说明了 Remind 脚本的几个功能:Remind 具有内置函数(确切地说是 66 个)并允许用户定义的函数(例如,FSET)。它还具有条件测试(例如,IF/ENDIF)。一点巧妙的脚本可以表达对于大多数日历程序来说太棘手的提醒。

天文

最终,所有日历都源于天文观测。Remind 包括计算您居住地的日出和日落时间以及月相的例程。月相在 GUI 日历中进行了说明。这些天文计算可作为内置的 Remind 函数使用。

PostScript 和 HTML 输出

Remind: The Ultimate Personal Calendar

图 3. PostScript 输出

除了在标准输出或弹出窗口上发出提醒之外,Remind 还可以创建高质量的 PostScript 和 HTML 日历。实际的 Remind“引擎”对 PostScript 或 HTML 一无所知。相反,如果使用命令行选项调用,它会以方便后端处理的格式打印出提醒。Rem2PS 后端生成 PostScript 输出(图 3),而 rem2html 生成 HTML 输出。Remind 本身可以生成一个过得去的纯文本月历。

Remind 使用经典的 UNIX 管道与后端通信。实际上,TkRemind 是一个纯 Tcl 脚本,它使用管道与后台 Remind 进程通信。通过这种方式,所有繁琐的日期计算代码都包含在 Remind 中,而漂亮的 GUI 和格式化代码则包含在相应的后端中。编写与 TkRemind 相当的 GNOME 和 KDE 版本应该相当简单。

除了向后端发送正常提醒之外,Remind 还可以传输“带外”数据,使后端执行一些神奇的操作。目前,定义了用于绘制月相和阴影日历块的特殊机制。PostScript、Tk 和 HTML 后端都尊重这些机制。其他后端可以轻松扩展该机制以用于特殊目的。

法语、西班牙语、德语...

并非每个人都会说英语。虽然您从大多数软件中不会知道这一点,但情况正在发生变化,软件作者正在将其软件国际化。Remind 也不例外;它已被翻译成十二种不同的欧洲语言。不幸的是,这是通过一种自定义机制完成的,该机制无法识别或尊重 POSIX 区域设置功能。您必须在编译时为 Remind 指定一种语言。

不同的语言在构成复数、表达时间和表达时间间隔方面有不同的规则。这使得简单的消息翻译变得不可能;在某些情况下,一种语言的代码是专门针对该语言的。

Remind 的设计使翻译人员可以相当轻松地将其移植到另一种语言;请参阅源代码以获取详细信息。如果有人想让 Remind 识别并尊重 LC_* 区域设置环境变量,那将是一个很棒的项目。

大杂烩

因为我喜欢知道犹太节日何时到来,所以我包含了用于处理希伯来日历的 Remind 函数。如果有人想贡献中文和穆斯林日历的代码,我愿意接受意见。

最后两段脚本。假设您想在每个 13 号星期五收到提醒。这不起作用

REM Fri 13 MSG Black Cat

因为它会在每个月 13 号或之后的第一个星期五发出。请尝试这样做

REM 13 SATISFY [wkdaynum(trigdate()) == 5] MSG\
Black Cat.
SATISFY 关键字使 Remind 遍历所有可能的“月份的 13 号”,直到找到一个工作日为星期五的日期。这种强大的机制使非常复杂的提醒变得非常简单。

最后,这里有一些 Remind 代码来计算蓝月亮何时出现。蓝月亮是日历月中第二次满月。(蓝月亮非常罕见。)

FSET isFirstFull(date) \
           monnum(moondate(2, date)) == \
                monnum(moondate(2, moondate(2, date)+1))
REM 1 SATISFY isFirstFull(trigdate())
        set blue moondate(2, moondate(2,\
        trigdate())+1)\
MSG Next blue moon is [trigger(blue)]

通过 Remind 运行此脚本表明,下一个蓝月亮将在 2001 年 10 月 31 日出现。之后的一个是 2004 年 7 月 31 日。

我敢说您无法让 Microsoft Schedule 警告您即将到来的蓝月亮。

还有更多...

我仅仅触及了 Remind 的表面。对于更令人兴奋的深奥的东西,如系统变量、优先级、SCHED 和 WARN、TAG 和 DURATION、替换过滤器、安全可移动的 OMIT、安全功能、OMIT 上下文、表达式、函数、调试功能等等,请下载 Remind。它是免费的——受 GPL 保护——可以在 ftp://ftp.doe.carleton.ca/pub/remind-3.0/ 找到。尽管 Remind 功能非常丰富,但它只有大约 20K 行代码,应该可以轻松地在 Linux 和任何其他类 UNIX 系统上编译和安装。

愿您永远不会忘记任何一个生日。

David F. Skoll (dfs@doe.carleton.ca) 是 Roaring Penguin Software Inc. 的创始人,这是一家 Linux 咨询公司 (www.roaringpenguin.com)。他花费大量时间摆弄 Remind,以至于经常忘记自己的约会。

加载 Disqus 评论