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

Posted

技术标签:

【中文标题】将 999 年之前的时间转换为纪元【英文标题】:Converting times before the year 999 to epoch 【发布时间】:2016-07-09 23:15:53 【问题描述】:

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

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

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

给定 999 年之前的时间,我如何才能准确地将其转换为对应的纪元值?

【问题讨论】:

这不是缺陷,是 DWIM。 【参考方案1】:

给定 999 年之前的时间,我如何才能准确地将其转换为对应的纪元值?

你不能使用 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 通过简单地从 1582 年向后扩展来使用所谓的“预测公历”。


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

【讨论】:

【参考方案2】:

查看投票和评论,DateTime 模块似乎是此类内容的权威、首选模块。不幸的是,它的$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).

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

话虽如此,这条评论可能是对用户的明智警告

    正在使用具有 32 位整数的机器;或 即使对于“旧”日期也具有较低的容错能力

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

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

$ 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
$

注意:我不知道这个“错误”在多大程度上是由于我检查它的方式 - 一年不是 365.25 天。事实上,让我更正一下 - 我从 here 中更好地定义了一年中的天数,我们得到了;

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

因此,使用大约公元前 1000 年的日期时会出现 0.2 天的错误。

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

【讨论】:

现在很有趣。计算纪元的代码是( $self-&gt;utc_rd_days - 719163 ) * SECONDS_PER_DAY + $self-&gt;utc_rd_secs,其中utc_rd_days 是自第一年1 月1 日以来的天数。对于32 位整数,719163 * 86400 会溢出。 另外,您的计算已关闭。公历中一年的长度是 365.2425 天。在您上次的计算中,您使用的是热带年的长度。 感谢您指出这一点。这有点相对 - 你看我开始使用 365.25 - 当然,这个想法只是为了感受一下,以确定是否可以忽略诸如忽略闰秒之类的事情。事实证明,IMO 非常准确。我很高兴有人发现它很有趣 - 我花了太多时间在上面,同时出现了另外两个答案。 1972 年之前没有闰秒,因此出于本问题的目的,可以忽略它们。但我很高兴您指出了 32 位整数的局限性,我需要将其添加到我的答案中。【参考方案3】:

公历每 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;

【讨论】:

以上是关于将 999 年之前的时间转换为纪元的主要内容,如果未能解决你的问题,请参考以下文章

将纪元时间转换为时间戳颤动

Java将日期转换为纪元值会产生错误的输出

如何在 python 中将 1970 年前的日期转换为纪元? [复制]

将纪元转换为 time_t

使用 1990 年 1 月 1 日作为纪元日期将天数转换为 C 中的日期

SSIS:将纪元列转换为日期