【问题标题】:Normalize ISO 8601 datetime to any time zone with support of non-standard time zone abbreviations将 ISO 8601 日期时间标准化为任何时区,支持非标准时区缩写
【发布时间】:2019-09-17 16:21:30
【问题描述】:

有时间在 ISO 8601 格式 (例如 2019-09-17T16:15:20Z,我如何将这个时间从一个时区转换/标准化为另一个时区(例如 ET = 美国东部时间, CT = 美国中部时间, PT =美国太平洋时间) ?

所需的解决方案应接受任何time zone abbreviation、标准和非标准缩写。


Perl 子程序

sub normalizeDateTime
{
  ... # ???
}

print normalizeDateTime('2019-09-17T16:15:20Z', 'ET');

【问题讨论】:

    标签: perl datetime timezone iso8601


    【解决方案1】:

    注意 在发布和编辑后,问题和标题已更改,以坚持要求“support of non-standard”缩写。

    但是,通常建议不要使用短名称,因为该答案的第二部分已经详细讨论过。更重要的是,在这个问题的上下文中,这显然是不行的,因为没有程序可以知道任意缩写(也没有任何“标准”)。

    一旦提供了到接受名称的映射,那么这将成为一个非问题,并且在答案中也已考虑到这一点。因此,我将保留此答案,并稍作修改。


    使用DateTime::Format::ISO8601 从您的字符串构建一个DateTime 对象,或者通常使用DateTime::Format::Strptime。然后根据需要使用DateTime 来处理它

    use warnings;
    use strict;
    use feature 'say';
    
    use DateTime::Format::ISO8601;
    use DateTime;
    
    my $dt_string = shift or die "Usage: $0 datetime-ISO8601\n";
    
    my $fmt = DateTime::Format::ISO8601->new(); 
    my $dt = $fmt->parse_datetime($dt_string); 
    say $dt->time_zone->name; 
    
    $dt->set_time_zone("America/Chicago"); 
    say $dt->time_zone->name;
    

    这使用DateTime::set_time_zone 来转换(更改)对象上的时区。


    问题要求使用时区缩写名称进行转换。但是有一个问题:缩写名称没有任何标准,可能只是本地约定,不验证/使用解析器的方法......甚至可能是模棱两可的。

    这在很多地方都有讨论。 DateTime::TimeZone 中的简短摘要,在 short_name_for_datetime 方法下,表示“短名称”(如要求的缩写)

    强烈建议您不要将这些名称用于显示以外的任何内容。这些名称并非官方名称,其中许多只是 Olson 数据库维护者的发明。此外,这些名称并不是唯一的。例如,在 -0500 和 +1000/+1100 处都有一个“EST”。

    (原重点)

    仍然尝试处理用户出现的缩写的一种方法是使用all_namesDateTime::TimeZonegrep 其输出感兴趣的缩写。例如,

    grep { /P(?:S|D)?T/ } DateTime::TimeZone->all_names
    

    返回(一个列表)一个唯一的字符串PST8PDT。该字符串在我尝试过的所有方法下似乎都有效,并且可以正确地在 DateTime 对象上设置时区。然而,正确的是,对于/E(?:S|D)?T/,这将返回列表CET EET EST EST5EDT MET WET;不好用。

    很明显,这既不系统也不可靠——就像缩写一样,一开始就不是。

    最好的办法是构建某种本地查找,它将您的简称转换为正确的名称,以便您知道它在您的工作中是正确的。然后,可以将添加到 OP(后来更改)的存根填充到

    use DateTime;
    use DateTime::Format::ISO8601;
    
    sub convert_time_zone_for_ISO8601
    {
      my ($iso, $tz) = @_;
      # Provide a lookup/mapping that knows locally used abbreviations
      #my $tz_name = convert_local_short_name($tz); 
      my $tz_name = 'America/New_York';             # for a working example
    
      # Returns a DateTime object (or generate a string in a desired format) 
      return DateTime::Format::ISO8601->new
          -> parse_datetime($iso)
          -> set_time_zone($tz_name);
    }
    
    my $dt = convert_time_zone_for_ISO8601('2019-09-17T16:15:20Z', 'ET');
    
    # Sole stringification doesn't include timezone but there are other methods
    say $dt->time_zone_short_name;
    say $dt->time_zone_long_name;
    say $dt->strftime("%F %T %{time_zone_short_name}");
    say $dt->strftime("%a, %d %b %Y %H:%M:%S %z");       # RFC822-conformant
    

    (请参阅有关各种打印方法的文档说明。)

    构建返回对象的链式方法使用可接受的时区名称提供解析和时区更改。

    为了使其与缩写一起使用,代码显然需要将本地使用的感兴趣的缩写转换(映射)到用户提供的正确时区名称。

    在上面的 sn-p 中,它有一个占位符子例程,例如,它可以在模块中使用散列,其中包含正确的映射,或者更好地来自可以由其他软件管理的 JSON 文件好吧;或通过查询数据库表或本地服务或某种方式。

    【讨论】:

    • 请注意,如果您知道偏移量但不知道政治时区,则时区也可以是偏移量(例如-0500)。请注意,对以这种方式创建的 dt 进行任何算术运算可能会使时区无效。例如,-0500 可能在冬季在多伦多有效,但多伦多在夏季使用 -0400。使用America/Toronto 将全年有效。
    • 另一个复杂情况是短名称根本不是唯一的。例如,三个不同的时区可以使用缩写 BST,它们的偏移量大不相同。唯一的解决方案是保留您关心的时区的映射。
    • @Grinnz 好吧,是的,我在这个答案中讨论了这个问题。我建议用户提供一个映射(“local lookup”到“translate”)
    • 致读者:经过额外审查,不知道 -1 的用途。
    【解决方案2】:

    Time::Moment 擅长解析部分,但需要一点帮助才能转换为任意时区,我提供了 role

    use strict;
    use warnings;
    use Time::Moment;
    use Role::Tiny ();
    use DateTime::TimeZone::Olson 'olson_tz';
    
    my $class = Role::Tiny->create_class_with_roles('Time::Moment', 'Time::Moment::Role::TimeZone');
    my $mt = $class->from_string('2019-09-17T16:15:20Z');
    my $tz = olson_tz 'America/New_York';
    my $in_eastern = $mt->with_time_zone_offset_same_instant($tz);
    

    DateTime::TimeZone::Olson 只是DateTime::TimeZone 的替代品,对于命名区域来说往往更快; DateTime::TimeZone 对象也可以。其他答案已涵盖根据您的缩写确定要使用的实际时区。

    【讨论】:

      【解决方案3】:

      我们可以使用基于DateTime 的库

      use DateTime::TimeZone;
      use DateTime::Format::ISO8601;
      use DateTime::TimeZone::Alias;
      

      并将所需的非标准缩写设置为别名,

      DateTime::TimeZone::Alias->set('ET' => 'America/New_York');
      DateTime::TimeZone::Alias->set('CT' => 'America/Chicago');
      DateTime::TimeZone::Alias->set('PT' => 'America/Los_Angeles');
      

      sub normalizeDateTime
      {
        my $dt = DateTime::Format::ISO8601
                   ->new()
                   ->parse_datetime($_[0])
                   ->set_time_zone($_ = DateTime::TimeZone->new(name => $_[1]));
      
        $dt . DateTime::TimeZone::offset_as_string($_->offset_for_datetime($dt))
                =~ s/^[+-]00:?00$/Z/r
                =~ s/^([+-]\d{2})(\d{2})$/$1:$2/r;
      }
      

      那么我们可以直接使用这样的时区名称作为有效时区:

      print normalizeDateTime('2019-09-17T16:15:20Z', 'ET');
      

      2019-09-17T12:15:20-04:00

      【讨论】:

        猜你喜欢
        • 2013-10-05
        • 1970-01-01
        • 1970-01-01
        • 2013-08-26
        • 2016-10-20
        • 1970-01-01
        • 2021-01-19
        • 1970-01-01
        • 2018-03-16
        相关资源
        最近更新 更多