将999年之前的时间转换为纪元

Converting times before the year 999 to epoch

在 Perl 中,存在一个名为 timelocal 的函数来将时间转换为纪元。

示例: my $epoch = timelocal($sec, $min, $hour, $mday, $mon, $year)

然而,在处理过去很久(999 年之前)的时间时,此函数似乎存在固有缺陷 - 请参阅 Year Value Interpretation 部分。更糟糕的是,它处理两位数年份的方式使事情变得更加复杂......

给定999年之前的时间,如何准确转换为对应的纪元值?

Given a time before the year 999 how can I accurately convert it to its corresponding epoch value?

你不能用 Time::Local。 timegm(被timelocal使用)contains以下:

if ( $year >= 1000 ) {
    $year -= 1900;
}
elsif ( $year < 100 and $year >= 0 ) {
    $year += ( $year > $Breakpoint ) ? $Century : $NextCentury;
}

如果年份在 0 到 100 之间,它会自动转换为当前世纪的年份,如文档中所述; 100 到 999 之间的年份被视为自 1900 年以来的偏移量。如果不破解源代码,就无法解决这个问题。


如果您的 perl 被编译为使用 64 位整数*,您可以改用 DateTime 模块:

use strict;
use warnings 'all';
use 5.010;

use DateTime;

my $dt = DateTime->new(
    year       => 1,
    month      => 1,
    day        => 1,
    hour       => 0,
    minute     => 0,
    second     => 0,
    time_zone  => 'UTC'
);

say $dt->epoch;

输出:

-62135596800

请注意,公历直到 1582 年才被采用,因此 DateTime 使用所谓的 "proleptic Gregorian calendar",只需从 1582 年向后扩展即可。


* 对于 32 位整数,过去或未来太远的日期将导致整数溢出。如果 use64bitint=define 出现在 perl -V 的输出中(大写 'V'),你的 perl 支持 64 位整数。

查看投票和评论,DateTime module would seem to be the authoritative, go-to module for this sort of stuff. Unfortunately its $dt->epoch() 文档附带了这些警告;

Since the epoch does not account for leap seconds, the epoch time for 
1972-12-31T23:59:60 (UTC) is exactly the same as that for 1973-01-01T00:00:00.

This module uses Time::Local to calculate the epoch, which may or may not 
handle epochs before 1904 or after 2038 (depending on the size of your system's 
integers, and whether or not Perl was compiled with 64-bit int support).

看来这些是您必须在其中工作的限制。

话虽如此,这条评论可能是对

用户的明智警告
  1. 正在使用具有 32 位整数的机器;或者
  2. 即使 "old" 日期也有低容错性

第一个 如果你有一台 32 位机器,这将是一个问题。带符号的基于 32 位的纪元的范围(以年为单位)约为 2^31 / (3600*24*365) 或(仅)68 年 to/from 1970(假设为 unix 纪元)。但是,对于 64 位 int,它变为 290,000 年 to/from 1970 - 我想这没问题。 :-)

只有你能说第二个问题是否会成为问题。 以下是对错误程度进行粗略检查的结果;

$ perl -MDateTime -E 'say DateTime->new( year => 0 )->epoch / (365.25 * 24 * 3600)'
-1969.96030116359  # Year 0ad is 1969.96 years before 1970
$ perl -MDateTime -E 'say DateTime->new( year => -1000 )->epoch / (365.25*24*3600)'
-2969.93839835729  # year 1000bc is 2969.94 years before 1970
$ perl -MDateTime -E 'say ((DateTime->new( year => -1000 )->epoch - DateTime->new( year => 0 )->epoch ) / (365.25*24*3600))'
-999.978097193703  # 1,000bc has an error of 0.022 years
$ perl -MDateTime -E 'say 1000*365.25 + ((DateTime->new( year => -1000 )->epoch - DateTime->new( year => 0 )->epoch ) / (24 * 3600))'
8  # ... or 8 days
$

注意: 我不知道这个 "error" 中有多少是由于我检查它的方式造成的——一年不是 365.25 天。事实上,让我更正一下 - 我从 here 中对一年中的天数进行了更好的定义,我们得到;

$ perl -MDateTime -E 'say 1000*365.242189  + ((DateTime->new( year => -1000 )->epoch - DateTime->new( year => 0 )->epoch ) / (24 * 3600))'
0.189000000013039

所以,当处理大约公元前 1,000 年的日期时,大约有 0.2 天的错误。

简而言之,如果你有 64 位机器,你应该没问题。

公历每 146,097 天重复一次,即 400 年。我们可以将小于 1000 的年份映射到循环中的等效年份。下面实现将小于1000的年份映射到第三个循环的年份,比如0001映射到1201。

#!/usr/bin/perl
use strict;
use warnings;
use Time::Local qw[timegm timelocal];

use constant CYCLE_YEARS   => 400;
use constant CYCLE_DAYS    => 146097;
use constant CYCLE_SECONDS => CYCLE_DAYS * 86400;

sub mytimelocal {
    my ($sec, $min, $hour, $mday, $month, $year) = @_;

    my $adjust = 0;
    if ($year < 1000) {
        my $cycles = 3 - int($year/CYCLE_YEARS) + ($year < 0);
        $year   += $cycles * CYCLE_YEARS;
        $adjust  = $cycles * CYCLE_SECONDS;
    }
    return timelocal($sec, $min, $hour, $mday, $month, $year) - $adjust;
}


use Test::More tests => CYCLE_DAYS;

use constant STD_OFFSET => 
  timelocal(0, 0, 0, 1, 0, 1200) - timegm(0, 0, 0, 1, 0, 1200);

my $seconds = -5 * CYCLE_SECONDS; # -0030-01-01
while ($seconds < -4 * CYCLE_SECONDS) { # 0370-01-01
    my @tm  = gmtime($seconds);
       $tm[5] += 1900;
    my $got = mytimelocal(@tm);
    my $exp = $seconds + STD_OFFSET;
    is($got, $exp, scalar gmtime($seconds));
    $seconds += 86400;
}