支持非标准时区缩写,将 ISO 8601 日期时间规范化为任何时区

Normalize ISO 8601 datetime to any time zone with support of non-standard time zone abbreviations

有时间采用 ISO 8601 格式 (例如 2019-09-17T16:15:20Z,我这次如何convert/normalize从一个时区到另一个时区(比如ET=美国东部时间, CT =美国中部时间, PT =美国太平洋时间) ?

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


Perl 子例程

sub normalizeDateTime
{
  ... # ???
}

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

注意 发布和编辑后,问题和标题已更改,坚持要求“支持non-standard" 缩写。

但是,通常建议不要使用短名称,因为本答案的第二部分已经进行了详细讨论。更重要的是,在这个问题的上下文中,它显然是一个 no-go,因为没有程序可以知道任意缩写(而且也没有任何 "standards")。

一旦提供了到接受的名称的映射,那么这就变成了 non-issue,这也已在答案中说明。因此,我将按原样保留此答案,并进行少量修改。


根据需要使用DateTime::Format::ISO8601 to build a DateTime object from your string, or in general DateTime::Format::Strptime. Then use DateTime

use warnings;
use strict;
use feature 'say';

use DateTime::Format::ISO8601;
use DateTime;

my $dt_string = shift or die "Usage: [=10=] 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 转换(更改)object 上的时区。


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

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

It is strongly recommended that you do not rely on these names for anything other than display. These names are not official, and many of them are simply the invention of the Olson database maintainers. Moreover, these names are not unique. For example, there is an "EST" at both -0500 and +1000/+1100.

(原文强调)

仍然尝试处理突然出现在用户身上的缩写的一种方法是使用 DateTime::TimeZone 中的 all_names,并向 grep 输出感兴趣的缩写。例如,

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

returns(一个列表)只有一个字符串 PST8PDT。这个字符串在我尝试过的所有方法下似乎都有效,并且可以正确地在 DateTime object 上设置时区。但是,尽管如此,对于 /E(?:S|D)?T/ this returns the list 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

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

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

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

在上面的代码片段中有一个占位符子例程,例如可以在模块中使用散列,映射 spelled-out 就在其中,或者更好地来自 JSON 文件,因此可以也由其他软件管理;或者通过查询数据库 table 或本地服务或某种形式。

我们可以使用基于 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})$/:/r;
}

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

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

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

Time::Moment is great at the parsing part, but needs a little help to convert to arbitrary time zones, which I provide with a 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 对象也可以。根据您的缩写确定要使用的实际时区已包含在其他答案中。