不太明显的是将一天添加到一天中的特定时间的问题。这是促使 LOCAL-TIME 概念发展及其实施的最初问题。简而言之,问题是确定一年中的哪两天不是 24 小时长。一个好的解决方案是假设一天是 24 小时,然后查看新时间是否与原始时间具有不同的时区。如果是这样,请将时区之间的差异添加到内部时间。然而,这并不是听起来应该是的微不足道的任务。第一个复杂问题是,没有一个通常的时间函数可以报告某个 timezoneidentifier 将导致适用于一天中的时间的 timezone 值发生变化的绝对时间。解决这种复杂性意味着我们不必在每次计算时都以艰难的方式测试跨时区边界,而只需与当前时区的边缘进行比较即可。目前大多数软件都采用困难的方式来实现这一点,包括 Unix cron 调度程序。但是,如果我们接受一次只能使用一个时区的限制,那么这个问题就不那么严重了,因此 Unix 和 C 人员往往会忽略这个问题。第二个复杂因素是,在任何计算中都无法使用内部时间表示——尝试调整解码时间的元素通常会失败,这不仅是因为程序员健忘,还因为边界条件难以枚举。当时间在内部以自一个纪元以来的秒数表示时,只有前者是容易的——后者与所有时区问题不可撤销地联系在一起。后者尤其可以在完全不参考时区的情况下进行计算,并且实际上应该在 UTC 中进行。据作者所知,现代编程语言或环境中没有可用的工具或包为日期计算和一天中的时间计算提供重要支持——这些通常被推迟到应用程序级别,并且似乎没有就应用程序员而言,已解决。罗马人使用 Ante Meridiem 和 Post Meridiem 来指代两半的传统在英语中幸存下来,尽管偏离了中午更改月份日期的习惯。因此,子午线在现代用法中的作用与在古代用法中的作用大不相同。这种遗留符号还带有一个相当不寻常的数字系统。从 24 小时世界的成员来看,映射到 0,1,2...,23 的顺序 12,1,2,...11,12,1,2,...,11 不仅是令人困惑的是,几乎不可能让人相信从上午 11 点到凌晨 12 点已经过去了 13 个小时。例如,几家斯堪的纳维亚餐厅每天只向 12 小时制世界的游客开放 1 小时,但每天向 24 小时制世界的当地人开放 13 小时。罗马在三月份开始一年的传统也已丢失。大多数农业社会对春天的到来比对冬至更感兴趣,尽管当太阳回来时自然会庆祝各种神灵大多数日历是由那些没有特别努力地在他们自己的一生或需要之外一般或准确的人设计的,但是朱利叶斯·凯撒决定将罗马历往后移两个月,因此它被称为儒略历。这意味着月份编号 7、8、9 和 10 突然出现在编号 9、10、11 和 12 中,但保留了它们的名称:九月、十月、十一月和十二月。这对那些记得他们的拉丁语的人来说很有趣,但更重要的是决定保留二月的闰日。在旧日历中,闰日被添加在年末,这很有意义,当月份已经很短时,现在却被挤到了第一季度的中间,使各种计算复杂化,影响了多少人工作。在过去,闰日被用作各种生育庆祝活动的额外日子。你只需要成为一名凯撒就会发现这没有吸引力。公历比儒略历中的四年闰年有所改进,仅将每四百周年设为闰年,但对于日历决策而言,这一决定出乎意料地明智。它仍然不准确,所以在几千年后,他们可能要像我们现在引入闰秒的方式插入一个额外的闰日,但方案的简单性却相当惊人:一个 400 年的周期不仅从 2000-03- 01(与 1600-03-01 一样),它包含偶数周:20,871。这意味着我们可以对公历内的所有时间进行一次 400 年的计算,包括星期几、闰日等。教皇格里高利十三世很可能已经向另一位毫无戒心的听众提供了与此类似的论文也未能欣赏到他的解决方案的优雅。400 多年后才能真正得到欣赏。
除了公历出人意料的优雅之外,世界现在非常幸运地在其日历上达成了共识。其他日历仍在使用,但我们现在有一个具有完全可转换性的全局参考日历。这对计算机来说是个好消息。这几乎与货币市场直到 1992 年才实现完全的货币间兑换一样好消息。在那之前,您可能会得到不同数量的资金,具体取决于您交易的是卢布等不知名货币的货币。这同样适用于日历:通常,根据您在日历系统之间的转换,您可能会在不同的日期结束,类似于将一年添加到任何一年的 2 月 29 日然后减去一年的问题。现在应该已经为引入在 LOCAL-TIME 概念的设计及其实施中做出的几个违反直觉的决定奠定了基础。 Unix 时间的优点是它可以表示为 32 位机器整数。如果时间不能表示为 32 位机器整数,它也有同样的缺点,因此只能表示 1901-12-13T20:45:52/2038-01-19T03:14:07 区间内的时间。如果我们选择一个无符号机器整数,则间隔为 1970-01-01T00:00:00/2106-02-07T06:28:16。 Common Lisp UNIVERSAL-TIME 概念的缺点是它在 1934-01-10T13:37:04 上的大多数 32 位机器上变成了 bignum,并且在 2036-02-07T06 比 Unix 时间早两年用完 32 位: 28:16。无论 2036 年是否还有任何 32 位计算机可以分担我的痛苦,我都觉得这些限制很不舒服。 Bignum 操作通常比 fixnum 操作昂贵得多,而且它们必须如此,无论 Common Lisp 实现对它们进行了多大程度的优化。因此,在时间密集型应用程序中使用 fixnums 成为一种明显的需求。决定落在了天和秒之间的分裂上,这应该不需要特别的解释,除了指出现在完全支持使用天计算而不考虑一天中的时间并且非常有效。因为我们非常接近下一个 400 年闰年周期的开始,多亏了教皇格雷戈里,第 0 天被定义为 2000-03-01,这比其他系统要少得多,但并不明显。每个 400 年周期包含 146,097 天,因此任意决定将这一天限制为最大负值 -146,097 或 1600-03-01。这可能会在准确表示不属于当时使用的日历的日期的危险下进行更改。没有尝试准确描述不属于公历的日期,因为这是一个只能参考国家之间的边界的问题,有时国家之间的边界在历史上的许多不同时期都是由君主、教会领袖或其他权力人物决定的更改为公历。满足这种需求也只有在俄罗斯日历转换为公历之前的日期,列宁在 1918 年做出的决定,或任何其他转换,例如欧洲大部分地区的 1582 年,美国的 1752 年,甚至更尴尬的是在挪威迟到了。上面没有提到需要毫秒分辨率。现代计算机上的大多数事件都在同一秒内,因此现在有必要通过增加时钟表示的粒度来将它们分开。这部分在大多数时间处理函数中显然是可选的。时代的选择需要更多的解释。转换到这个系统只需要从月份中减去两个并将一月和二月作为上一年的一部分。
fixnums 的适中大小让我们比传统的时间表示方式有另一个巨大的优势。由于现在闰年总是在年末,因此它与日期的年、月、日和星期几的解码无关。通过选择这个看起来很奇怪的时代,计算闰年和闰日的整个问题就消失了。这也意味着一个中等大小的解码日期元素表可以预先计算 400 年,与其他系统使用的基于除法的计算相比,提供了巨大的加速。类似地,一天中可能有 86400 秒的解码值(如果我们允许闰秒,则为 86401)比基于除法的计算产生了巨大的加速。 (根据您的处理器和内存速度,可能需要 10 到 50 的因数。对于完整的解码)数字设备公司的 David Olsen 在收集世界时区及其夏令时边界方面做了大量工作。与新泽西的 Unix System V 方法(插入适当的嘘声以获得最佳效果)相反,该方法仅将夏令时制度编入当前年份并适用于所有年份,David Olsen 的方法是维护所有时区更改的表。因此,一个特定的时区有一个相当长的表,其中列出了要添加的特定秒数以获取本地时间的适用期。每个间隔由特定值的开始和结束时间、特定值、夏令时标志和时区的习惯缩写表示。在大多数 Unix 系统上,这在 /usr/share/zoneinfo/ 中的编译文件中可用,大多数情况下以基于该地区的大陆和首都的名称命名,或者在其他情况下使用更通用的名称。虽然不完美,但这可能是一个不错的方案——很容易确定使用哪个方案。通常,表格还提供了映射到时区文件的地理坐标。对于时区信息,LOCAL-TIME 概念实现了一个包,TZ 或 TIMEZONEin full,其中包含以文件命名的符号,其值是延迟加载的时区对象。由于 zoneinfo 文件的源文件通常不如可移植编码的二进制信息可用,因此这些信息从编译文件加载到内存中,从而与系统上的其他时区函数保持最大的兼容性。在 LOCAL-TIME 实例中,时区被表示为一个符号,以帮助在编译的 Lisp 文件中保存文字时间对象。包 TZ 可以轻松地自动加载到支持此类功能的系统中,以降低加载顺序的复杂性。为了再次大幅提高效率,每个时区对象都保存了对时区周期的最后几个引用,以限制搜索时间。对长期运行系统的实证研究表明,在给定时区中超过 98% 的查找是针对同一时间段的,80% 以上的剩余查找都在相邻的时间段内,因此缓存这些值是很有意义的。为了有效地存储 400 年周期中的 146,072 个条目以及解码的年、月、日和星期几以及一天中的 86401 个条目以及解码的小时、分钟和秒,进行了各种优化受雇。使用列表的简单方法在 32 位机器上消耗大约 6519K。由于它们的开销,向量的表现更差。由于解码的元素是小的、表现良好的无符号整数,将它们编码在 fixnum 内的位域中可以节省大量内存: +----------+----+---- -+---+ +-----+------+------+| yyyy |毫米 |日 |道指| |小时 |分钟 |秒 |+------------+----+-----+---+ +-----+------+------+ 10 4 5 3 5 6 6
这种简单的优化意味着完全相同的数据的紧凑存储量增加了 7 倍,并显着改善了启动时的访问时间(根据处理器和内存速度以及缓存策略的考虑,在生产中测得的系数为 1.5 到 3)。尽管如此,909K 的存储空间来保存预先计算的日期和时间表似乎是为提高性能而付出的高昂代价。不出所料,更多的经验证据证实,大多数解码的日期都在同一世纪。未来几年最坏的情况,我们将频繁访问两个世纪,但存储四个完整世纪仍然是一种浪费。每个表减少到 100 年也意味着年数可以用 7 位表示,这意味着类型 (UNSIGNED-BYTE 16) 的专用向量可以表示它们全部。在此优化中将丢失星期几,但如果单个分区获取星期几太昂贵,则全长 (146097) 类型 (UNSIGNED-BYTE 4) 的专用向量可以容纳它们。事实证明,与其他解码元素相比,星期几的使用要少得多,因此专用向量被删除,并且在调用解码器时包含了一个选项以跳过星期几。类似地,通过在特定类型的向量 (UNSIGNED-BYTE 16) 中仅表示 12 小时,小时将只需要 4 位,并且查找可以在代码中进行 12 小时的移位。这将表内存需求减少到仅 156K,并且仍然比访问完整列表表示更快。这种压缩比朴素的方法产生了几乎 42 倍的改进 +-------+----+-----+ +----+------+------ +| 0-100 |1-12| 1-31| |0-11| 0-59 | 0-59 |+-------+----+-----+ +----+------+------+ 7 4 5 4 6 6现在解码一天意味着找到一周中某天的 400 年周期,其中的世纪用于表查找,并将表中的世纪和年份的值加在一起,可能是 100 来表示 1 月和 2 月下个世纪。所有这一切都可以通过大约 2,939,600 年的非常便宜的 fixnum 操作来完成,之后这一天将产生一个 bignum 减法,以将其带入下一个 2,939,600> 年的 fixnum 空间。 (这种优化实际上还没有实现。)Common Lisp 以打印和读回几乎所有数据类型的能力而闻名。 LOCAL-TIME 概念的动机包括在文件中保存人类可读的时间戳的能力,以及在编译的 Lisp 文件中有效存储文字时间对象的能力。前者是通过使用阅读器宏实现的。忽略 @ 字符的所有其他可能用途,它被选为读取器宏,用于完整表示 LOCAL-TIME 对象。考虑到与 UNIVERSAL-TIME 概念一起工作的软件的流行,特别是考虑到迄今为止缺乏替代方案,选择 #@ 作为时间对象的 UNIVERSAL-TIME 表示的读取器宏。后一种符号显然会丢失原始时区信息和任何毫秒。指示 Lisp 阅读器解析阅读器宏字符后的时间字符串。其他函数可以直接调用 PARSE-TIMESTRING。这样的时间字符串严格遵循 ISO 8601,但允许进行一些增强和一个附加选项:能够在逗号和句点之间选择小数秒分隔符。正在进行的工作包括从指定时间(例如现在)中添加和减去持续时间,解释 = 的使用,这也需要表示当前具有一个锚点的时期。然而,duration 语法充满了假设,这些假设很难简明扼要地表达和使用,而不会导致意外和不需要的结果。
ISO 8601 的标准语法具有相当丰富的选项。由于它们引入的歧义,这些大多不受支持。时间字符串语法的目标是,位置和时间段应该很容易以信息保留语法读取和写入,因此不需要迎合某些人喜欢的信息丢失格式,因为它们试图与他们的口语形式。考虑到时间格式的主要问题是元素顺序的随机性,LOCAL-TIME 对象的 timestringformatter 不允许在这方面有任何选项,但允许按照标准省略元素。失去 12 小时制会暂时惹恼一些人,但没有什么比彻底改变坏习惯更像的了。当然,持久的程序员无论如何都会编写他自己的格式化程序,因此对于在程序和面向 lisp 的输入文件中表示时间,默认值应该是最合理的。目前,时间字符串格式化程序的接口非常适合从具有 ~// 构造的 FORMAT 控制字符串调用,并采用以下参数:universal -- 如果为 true,则忽略时区并使用 UTC>。这是冒号修饰符。 timezone -- 如果为 true,则在末尾打印时区规范。这是 atsign 修饰符。 date-elements -- 要写入的日期的元素数,从右边开始计数。这是一个从 0 到 4 的数字(如果省略则默认为 NIL)。 time-elements -- 写入时间的元素数,从左边开始计数。这是一个从 0 到 4 的数字(如果省略则默认为 NIL)。 date-separator -- 要在日期元素之间打印的字符。如果省略或 NIL,则默认为连字符。
time-separator -- 要在时间元素之间打印的字符。如果省略或 NIL,则默认为冒号。此参数也适用于打印时区以及包含分钟组件时的时区。 internal-separator -- 要在日期和时间元素之间打印的字符。也可以指定为数字 0,完全省略它,如果完全省略日期或时间元素,这是默认值,否则为字母 T。 LOCAL-TIME [Type] [Constructor] Arguments: (&key Universal internal unix (msec 0) (zone 0)。从提供的数字时间表示产生一个 LOCAL-TIME 实例。LOCAL-TIME-ADJUST [Function] Arguments: (source timezone &optional destination) 返回两个值,new day 和 secslots 的值,或者,如果 destination 是 LOCAL-TIME 实例,则用新值填充槽并返回目的地。ENCODE-LOCAL-TIME [Function] 参数:( ms ss mm hh day month year &optional timezone) 返回一个新的与指定时间元素对应的LOCAL-TIME 实例 DECODE-LOCAL-TIME [功能] 参数:(local-time) 以多个值返回解码后的时间:ms, ss, mm, hh, day, month, year, day-of-week, daylight-saving-time-p,timezone, 以及习惯用的时区缩写。 FORMAT-TIMESTRING [功能] 参数: (stream local-time Universal-p timezone- p date-elementstime-elements date-separator time-separator internal-separator) 在流上生成对应的时间字符串使用给定的选项调整到 LOCAL-TIME。
TIMEZONE [功能] 参数:(local-time &optional timezone) 返回多个值时区为UTC以东的秒数,布尔夏令时-p,时区的习惯缩写,该时区的开始时间,以及这个时区的结束时间 LOCAL-TIMEZONE [Fu ......