PAYATHON 2026

如何在 Perl 中将指定年份的日历周转换为 Unix timestamp?

数据阿明

可以用 Perl 的 DateTime 模块,根据 ISO 周所属年份和周数算出该周周一零点,再通过 epoch 获取 Unix timestamp。原命令使用 %V,因此这里按 ISO 8601 周规则计算。

转换时要使用 ISO 周所属年份,并为周一零点指定时区。

先修正年份与周数的组合

原命令中的两个格式符使用了不同的年份口径:

  • %Y:日期所属的公历年份。
  • %V:ISO 周数,周一为一周的开始。

ISO 第 1 周是包含 1 月 4 日的那一周。跨年附近的日期,其公历年份可能与 ISO 周所属年份不同。例如,2024-12-30 属于 2025-W01。

在支持 %G 的 strftime 实现中,应使用 %G-%V:

perl -E "use POSIX qw(strftime); say strftime('%G-%V', localtime)"

%G 表示 ISO 周所属年份。POSIX::strftime 对格式符的支持取决于运行平台;如果使用 DateTime,可以直接读取 week_year 和 week_number。

如果已有数据只保存了 %Y-%V,跨年附近的记录可能存在歧义,应尽量从原始日期重新计算。

根据 ISO 年份和周数生成 timestamp

如果尚未安装 DateTime,可以通过 CPAN 安装:

cpan DateTime

下面的函数返回指定 ISO 周的周一零点对应的 Unix timestamp。示例使用 UTC;实际使用时,应传入业务采用的时区。

use strict;
use warnings;
use DateTime;

sub iso_week_start_epoch {
    my ($iso_year, $week, $time_zone) = @_;
    $time_zone //= 'UTC';

    die "Invalid ISO week number\n"
        unless defined $week
        && $week =~ /\A[0-9]{1,2}\z/
        && $week >= 1
        && $week <= 53;

    # 1 月 4 日始终位于 ISO 第 1 周。
    my $start = DateTime->new(
        year      => $iso_year,
        month     => 1,
        day       => 4,
        time_zone => $time_zone,
    );

    # 回到第 1 周的周一,再移动到目标周。
    $start->subtract(days => $start->day_of_week - 1);
    $start->add(weeks => $week - 1);

    # 并非每个 ISO 年份都有第 53 周。
    die "ISO week does not exist in the requested year\n"
        unless $start->week_year == $iso_year
        && $start->week_number == $week;

    return $start->epoch;
}

# 2025-W01 从 2024-12-30 开始。
print iso_week_start_epoch(2025, 1, 'UTC'), "\n";

函数先找到 ISO 第 1 周的周一,再增加 周数 − 1 周。最后检查计算结果,避免把不存在的第 53 周静默转换成下一年的第 1 周。

原始输入是日期时,可以直接计算所在周的起点

对于按 YYYY-MM-DD 分组的场景,可以直接从日期回退到本周周一,无须先格式化成周编号,再反向解析:

use strict;
use warnings;
use DateTime;

my $input = '2025-01-01';
my $time_zone = 'UTC';

my ($year, $month, $day) =
    $input =~ /\A([0-9]{4})-([0-9]{2})-([0-9]{2})\z/;

die "Expected YYYY-MM-DD\n" unless defined $day;

my $date = DateTime->new(
    year      => $year,
    month     => $month,
    day       => $day,
    time_zone => $time_zone,
);

my $week_key = sprintf(
    '%04d-%02d',
    $date->week_year,
    $date->week_number,
);

my $week_start = $date->clone;
$week_start->subtract(days => $week_start->day_of_week - 1);

print "$week_key\n";
print $week_start->epoch, "\n";

正则表达式检查日期格式,DateTime->new 检查具体日期是否合法。这里创建的日期默认时间为零点,因此回退后得到的是周一零点。

时区和夏令时需要统一处理

YYYY-MM-DD 只有日期信息,不能单独确定唯一的 Unix timestamp,必须先约定它表示哪个时区的日期。例如,业务按中国标准时间分周,可以将示例中的时区改为 Asia/Shanghai。

原命令使用 localtime,采用进程所在环境的本地时区。如果后续转换使用 UTC,接近午夜或周边界的记录可能被分到不同的周。日期解析、周分组和周起点计算应使用同一时区。

对于采用夏令时的地区,应按日历日期移动到周一,避免直接减去固定的 86400 秒来表示一天。在存在午夜跳时的时区,还需要明确遇到不存在或重复的本地零点时如何处理。

备注:内容仅供参考。