Perl的多种日期和时间

一些基本概念

为了了解您可能想如何使用日期和时间,了解一些基本概念是很重要的。以下是我在这篇文章中将使用的一些术语

  • datetime
  • UTC(也称为GMT和Zulu) UTC代表“协调世界时”。这是一个国际标准,使用原子钟来保持,并且保持在地球自转轴上的0.9秒以内,以便与传统的计时标准很好地协同工作。UTC时间是在本初子午线(0度经度)测量的。

    世界各地的时区都是以UTC的偏移量来指定的。广泛使用的术语GMT(格林尼治标准时间)指的是与UTC等效的时区。换句话说,它没有偏移。

    美国军事使用一套基于字母表(A-Z)的术语来指定世界各地的时区。在这个系统中,UTC是Z,有时称为“Zulu”。

    UTC是日期和时间的内部表示的优良标准,因为它使得比较日期和时间或进行日期时间数学运算变得更容易。

  • 时区和夏令时

    如上所述,时区被定义为相对于UTC的偏移量。大多数时区都是整小时的偏移量。澳大利亚阿德莱德的偏移量为UTC快9小时半,尼泊尔偏移量为UTC快5小时45分钟。

    夏令时的使用使时区变得复杂,它改变了给定位置的偏移量,使其在一年中变化。美国东部的偏移量为UTC慢5小时,即UTC-0500。这意味着12:00(中午)UTC变为07:00(上午)。然而,当夏令时生效时,偏移量变为UTC-0400,即UTC慢4小时。因为时区由政府决定,夏令时的使用以及基础偏移量随着时间的推移而变化,未来也可能发生变化。

    这给处理非UTC日期时间的数学运算带来了很大的复杂性。如果我有一个美国东部地区的本地日期时间为2002-10-25 14:15:00,然后我向该日期时间添加六个月,那么我将跨过一个夏令时变更。

    所有这一切的后果是,任何将时区表示为固定偏移量的代码,一旦涉及到日期时间数学,很可能会开始产生错误。

    时区偏移量和规则的权威来源是Olson时区数据库。它根据“America/New_York”等名称定义时区,而不是“EST”。后者是常用缩写,但可能应该避免使用,因为这些简短名称不是唯一的或确定的。例如,-0500和+1000都有“EST”。

  • 本地时间

    本地时间等于UTC加上本地时区偏移量。虽然UTC非常适合内部使用,但大多数人希望以他们自己的位置为单位的日期时间。在某种意义上,本地时间是显示格式,而UTC是存储格式。

  • 纪元

    纪元是一个通用术语,指的是任何特定系统的“开始”。例如,格里历的纪元是公元1年1月1日。

    大多数操作系统使用的纪元系统将日期时间表示为自特定日期和时间之后的秒数。对于Unix系统,纪元始于1970年1月1日午夜GMT(UTC)。其他系统有不同的纪元。因此,您不能假设2003,131的纪元时间在各个系统间具有相同的意义,因为不同的系统有不同的“第0秒”。将日期时间存储为其纪元是不兼容的。

    更糟糕的是,在大多数现有系统中,epochs 以 32 位有符号整数的形式表示,这只能让你表示大约 136 年的日期时间。在目前大多数使用的 UNIX 系统中,这意味着你现在可以表示的最新日期是 2038 年左右,最早的是大约 1902 年。如果你试图表示你曾祖母的出生日期,这并不太合适。

    所有这一切的后果是我强烈建议 不要 使用 epochs,除非你真的没有其他选择。当然,你通常没有选择,因此了解这个系统的工作原理很重要。

  • 格里高利历

    历史上曾经使用过许多不同的历法系统。格里高利历是目前国际上普遍认可的日期表示标准,也是你所说的“1999 年 8 月 8 日”时所使用的历法。仍在使用的其他历法包括希伯来历、伊斯兰历和中国历。

    尽管格里高利历直到 1582 年才被创造出来,而且直到本世纪才被全球采用,我们仍然可以使用格里高利历来推算过去的时间。

需要对日期和时间做什么呢?

你可以用日期和时间做很多事情,不同的模块/发行版提供不同类型的功能。广义上讲,我们可以考虑以下功能领域:

  • 解析/格式化

    在计算机界使用的日期时间格式比你能挥棒的还要多。你通常需要解析一种格式的日期时间,以便将其转换为可以内部处理的某种形式,比如一堆整数或一个 epoch 时间。

    另一方面,你通常需要将某种标准表示(如 epoch)转换为其他格式。

  • 基本表示

    一个不错的日期时间对象非常有用。这些对象从围绕 Perl 的 localtime() 的轻量级包装到尝试提供所有可能的日期时间操作方法的对象。

  • 数学和计算

    你经常需要回答像“今天之后七天是星期几”或“现在到午夜还有多少时间”这样的问题。这与确定复活节在哪一年、马丁·路德·金诞生日是星期几等日期有关。

我们还可以用日期时间做很多其他事情,但这些大多是上述功能领域的扩展。

Perl 的内置日期/时间功能

Perl 有一些处理日期和时间的内置功能。这些功能包括:

  • time()

    一个返回当前 epoch 时间的函数。

  • localtime()gmtime()

    这些函数将 epoch 时间转换为表示本地时间的组件集。它们都返回包含小时、分钟、月份等内容的数组,尽管其中一些返回值很难使用。例如,年份是实际年份减去 1900。

    localtime() 函数根据你的系统时区设置返回你的当前位置的日期时间,而 gmtime 函数返回当前的 UTC 日期时间。

    Time::localtimeTime::gmtime 模块为 gmtime()localtime() 提供了一个薄的对象层,因此你可以做一些像 print gmtime()->year 这样的事情。当然,那 仍然 会打印出减去 1900 年的年份。

  • Time::Local

    这个核心模块提供了一些函数,可以将 localtime()gmtime() 返回的组件数组转换回 epoch 时间。

  • POSIX.pm

    Perl 中的 POSIX 模块提供了对几个常见 C 库日期时间函数的接口,例如 strftime()。我认为这是绝望者的最后一根稻草,因为 POSIX.pm 模块是一个内存消耗者,并且 C 库接口相当不符合 Perl 风格。

明星模块

这些模块具有最悠久的历史,并且是使用最广泛的。

  • 时间日期分布

    这个分布由Graham Barr维护和编写,包括三个模块。《Date::Format》模块提供了一些用于格式化日期时间输出的函数,包括类似于标准C库中的strftime()的函数。它可以与纪元时间或Perl的localtime()函数返回的组件数组一起工作。

    Date::Parse解析一组有限的常见日期时间格式,返回纪元时间或组件数组。

    该分布还包括一些语言模块,可用于本地化解析和格式化。

    最后,Time::Zone提供了一个基于短时区名称(如“EST”或“GMT”)的时间区偏移接口。正如之前提到的,这些名称不是官方或标准化的,所以它们的实用性有限。

    所有这些模块都受限于它们内部使用纪元时间,但它们相当快速且轻量级。对于复杂的日期时间问题,这些模块可能没有自动化足够的工作。

  • Time::Piece

    由Matt Sergeant编写和维护的此模块基于Larry Wall设计的接口。它提供了一个方便的日期时间对象API,尽管API有点令人困惑。例如,$time->mon返回月份编号(1-12),而$time->month返回月份的缩写名称。

    它还通过使用C级的strptime()strftime()函数实现了基本的解析和格式化。包含的Time::Seconds模块允许进行基本的日期计算,如$tomorrow = $time + ONE_DAY

    实现相当轻量级,但受限于它使用纪元时间作为内部表示。它当然很有用,但与TimeDate模块一样,它对于更复杂的使用来说还不够。

    据本文写作时,Matt Sergeant已发布了一个基于我的DateTime.pm模块的实验性Time::Piece版本。这保留了Time::Piece API不变,但允许它处理无法由系统纪元表示的日期。

  • Date::Manip

    Sullivan Beck的Date::Manip非常庞大,大约有3,000行代码。正如那么多的代码所预期的,这里总有一些适合每个人的东西。它处理解析、格式化、日期计算,以及诸如重复日期时间和企业日计算等更复杂的事情。值得注意的是,它的时间区支持与Time::Zone提供的大致相同。

    此模块最独特的功能是其非常灵活的解析器,可以处理像“3周前的星期一”或“下周日”这样的东西。它还提供了一些解析,用于指定重复,如“2003年的第三个星期一”。

    与之前涵盖的所有内容不同,此模块不仅限于纪元时间。它有一个完全功能的接口,在我看来,API可以更干净。我不喜欢某些函数做很多事情,输出的依赖性要么是参数类型,要么是显式标志,或者两者兼而有之。

    但此模块最大的问题,这也是作者所认可的,是其大小。它使用大量内存(在我的系统上约为3MB),并且加载速度相当慢。前者使其对mod_perl变得有问题,后者导致CGI脚本出现问题。您可以在其他更轻量级的模块中找到其大多数功能,但如果大小和速度不是问题,则此模块几乎肯定可以做您想要的一切。

  • Date::Calc

    当您需要功能与速度相结合时,Steffen Beyer的Date::Calc发行版是您前往的地方。这个模块提供了与Date::Manip类似的大部分功能,但核心的Date::Calc模块比Date::Manip具有更小的内存占用(在我的机器上约为1MB),速度更快。这是因为它的核心实现是C语言。

    此模块提供用于计算各种日期相关信息的函数,以及一些基本的解析和格式化操作。由于每个函数返回一个或多个元素,而不是数据结构(如散列),因此在使用接口时需要一些指导,您必须不断处理值数组的传递和接收。

    发行版还包括一个名为Date::Calc::Object的类,它可以表示日期时间“delta”,即两个日期时间的差。这种双重性质很奇怪,因为许多适用于其中一个的方法对另一个不起作用。该类支持日期数学和比较的重载,因此您可以执行类似于$date + [1, 2, 3]的操作,即给指定日期增加一年、两个月和三天。

    最后,还有一个Date::Calendar对象,可以用来设置定义假期、工作日和部分工作日的日历“配置文件”。如果您需要计算未来X个工作日,同时考虑特定组织的假期,这非常有用。

    这个发行版中的所有模块都不依赖于纪元时间,尽管它们只支持正数年份。时区支持非常有限,仅作为偏移量实现,不支持夏令时规则。实现了多种语言的本地化以进行解析和格式化。

以及成千上万...

如果没有至少十二个具有重叠功能的其他模块,那还算不上Perl,对吧?在这种情况下,有二十多个!为了保持理智,我已经排除了许多模块,特别是那些看起来没有维护,或者没有足够的可理解文档供我了解它们究竟做了什么。按字母顺序排列,剩下的有

  • Astro::Sunrise - Ron Hill

    此模块提供特定日期和地点的日出和日落时间。

  • Astro::Time - Chris Phillips

    包含许多在处理天文日期时有用的函数。

  • Class::Date - Balázs Szabó

    这基本上是Time::Piece API加上更多功能。它似乎提供了基于Olson数据库的完整时区支持,通过POSIX.pm实现。它是纪元限制的。

  • Date::Business - Richard DeSimine

    这是一个简单的用于执行业务日期数学的接口。换句话说,周末不计入,您可以定义自己的假期。此模块是纪元限制的。

  • Date::Convert - Mordechai Abzug

    在格里历、希伯来历和儒略历之间转换日期。

  • Date::Convert::French_Rev - Jean Forget

    在法国革命历之间转换。

  • Date::Day - John Von Essen

    告诉您给定日期是星期几。

  • Date::DayofWeek - Rich Bowen

    Date::Day完全相同,但仅限于1500-2699年。另一方面,它使用了一个更可爱的算法。

  • Date::Decade - Michael Diekmann

    提供三个十年计算函数,API与Date::Calc类似。

  • Date::Discordian - Rich Bowen

    在至暗历之间转换。

  • Date::Easter - Rich Bowen

    计算给定年份复活节在哪一天。

  • Date::Handler - Benoit Beausejour

    一个类似于 Time::Piece 的日期对象,但具有更一致的接口。它实现了本地化,多种日期数学操作,并包括一个表示日期时间跨度的对象。它基于本地操作系统实现提供时区支持,在某些系统上意味着支持Olson数据库。它是以纪元为限制的。

  • Date::ICal - Rich Bowen

    一个提供简单API、日期数学和iCal格式化解析(见RFC 2445)的日期对象。它只支持作为偏移量的时区,但它不受纪元限制。

  • Date::ISO - Rich Bowen

    Date::ICal的一个子类,增加了ISO 8601格式的解析和格式化。

  • Date::Japanese::Era - Tatsuhiko Miyagawa C

    将日本纪元日期进行转换。

  • Date::Japanese::Holiday - Tomohiro Ikebe

    告诉您给定日期是否是日本假日。

  • Date::Leapsecond - Flávio Soibelmann Glock

    在UT1和UTC时间之间进行转换。

  • Date::Leapyear - Rich Bowen

    一个函数,告诉您给定年份是否是闰年。

  • Date::Maya - Abigail

    在儒略日和玛雅日历之间进行转换。

  • Date::Passover - Rich Bowen

    计算给定年份的逾越节和罗什哈沙纳的日期。

  • Date::PeriodParser - Simon Cozens

    解析类似“今天早上”或“大约两周前”的英语时间描述,并将它们转换为表示时间段开始和结束的纪元时间对的数组。

  • Date::Range - Tony Bowden

    表示日期范围的对象。让您了解两个范围是否重叠,给定日期是否包含在给定范围中,以及其他一些有用的事情。

  • Date::Roman - Leo Cacciari

    一个表示根据罗马日历的日期的对象。

  • Date::Set - Flávio Soibelmann Glock

    表示一组日期时间的对象。集合可以是日期时间跨度,周期性日期集,或一组特定日期时间。它为所有这些提供了集合数学操作,并允许您遍历集合的成员。另见 Date::Set::Timezone

  • Date::Simple - John Tobey A

    仅表示日期而没有时间成分的日期对象。它提供日期数学。它不受纪元限制。

  • Date::Tie - Flávio Soibelmann Glock

    一个基本的日期对象,具有日期数学功能。其功能通过绑定散列表接口实现。它支持固定偏移量时区,并且是纪元限制的。

  • HTTP::Date - Gisle Aas

    此模块是LWP分发的部分。它解析许多常见的日期时间格式,包括HTTP协议中使用的所有格式。如果Date::Parse无法理解您需要处理的格式,此模块提供了良好的替代方案。

  • Time::Duration - Sean Burke

    给定秒数,此模块返回人类语言描述的持续时间。例如,3660秒将是“1小时1分钟”。

  • Time::Human - Simon Cozens

    给定纪元时间,此模块可以返回描述该时间的字符串,如“九点半”。它具有本地化的挂钩。

  • Time::Unix - Nathan Wiger

    加载此模块强制Perl的time()函数使用Unix纪元系统,而不管代码运行的操作系统。

  • Time模块 - David Muir Sharnoff

    此分发包括Time::CTimeTime::ParseDateTime::Timezone,它们是Graham Barr的Date::FormatDate::ParseTime::Zone的更强大版本。

但还有更多!

不满足于现状,我最近开始了一个项目,以解决我看到的Perl日期模块状态的根本问题。这个基本问题是,尽管几乎所有您可能需要的功能都存在,但它们散布在大量不兼容的模块中。

例如,Date::Calc 提供了各种日期时间计算和日期数学的良好功能,但它返回的值并不适合传递给 Date::Format。而虽然 Date::Manip 具有强大的解析功能,但其解析例程的返回值在不进行进一步调整的情况下不能传递给任何其他模块。如此等等。

例如,如果我想用 Date::Parse 解析一个日期,然后用 Date::Calc 计算一周后的日期,最后用 Date::Format 格式化它,我必须做以下操作

my $time1 = str2time($date_string); # Date::Parse

# Today() from Date::Calc returns
# date information for an epoch time
my ($y1, $m1, $d1) = Today($time);

my ($y2, $m2, $d2) = Add_Delta_Days($y1, $m1, $d1, 7);

my $time2 = Date_to_Time($y2, $m2, $d2);

print strftime('%B %d, %Y', $time2);

当然,我并不一定非要用 strftime() 函数来格式化日期。我可以用 Date::Calc 来完成它

print sprintf('%s %02d %04d',
Month_to_Text($m2), $d2, $y2);

但我需要便利性。如果我要处理许多日期时间,需要解析各种输入,生成不同的格式,并进行大量计算,那么一个便利且统一的 API 可以大大提高代码的可维护性。用于使不同模块协作的额外粘合代码可能会迅速模糊程序的实际意图。

过去尝试将所有现有模块作者引导到共同 API 的努力失败了,因此,而不是再次尝试这样做,我决定编写更多的日期时间代码。众所周知,扑灭火焰的最佳方法是往火上倒大量的汽油。为了使我的项目听起来更酷,我称之为“Perl DateTime Suite”,这听起来比“更多日期和时间模块”要好得多。

这个项目的目标是创建一系列日期时间模块,涵盖与日期和时间相关的所有功能。这个套件中的模块将相互协作,这意味着解析日期时间的模块将返回一个标准对象,而格式化日期时间的模块将接受这个标准对象。

到目前为止,这个项目已经吸引了众多人的关注,在 [email protected] 列表中进行的讨论进展顺利。最近,我发布了核心对象 DateTime.pm 的 alpha 版本,以及提供基于 Olson 数据库的完整时区支持的 DateTime::TimeZone。在 CPAN 上还有一个 DateTime::Format::ICal 的 alpha 版本,这是一个用于解析和格式化 iCal 日期时间的模块。未来,将会有更多以“DateTime::”开头的模块套件出现。

更多资源和阅读材料

一些优秀的在线资源包括 Claus Tondering 的日历 FAQ 在 http://www.tondering.dk/claus/calendar.html, 以及美国海军天文台时间服务网站 http://tycho.usno.navy.mil/。 关于时区和 Olson 数据库的更多信息,请参阅 http://www.twinsun.com/tz/tz-link.htm.

如果您对与 Perl 和日期时间相关的内容感兴趣,请查看 [email protected] 列表。您可以通过向 [email protected] 发送消息来订阅。

谢谢

感谢 Jonathan Scott Duff、Nick Tonkin 和 Iain Truskett 在发布前审查这篇文章。

标签

反馈

这篇文章有什么问题吗?请通过在 GitHub 上打开问题或拉取请求来帮助我们。