Нормализация даты и времени ISO 8601 для любого часового пояса с поддержкой нестандартных сокращений часовых поясов

Имея время в формате ISO 8601 (например, 2019-09-17T16:15:20Z), как я могу преобразовать/нормализовать это время из одного часового пояса в другой часовой пояс (например, ET = восточное время США, CT = центральное время США, PT = тихоокеанское время США) ?

Желаемое решение должно принимать любые сокращения часовых поясов, стандартные и нестандартные сокращения.


Подпрограмма Perl

sub normalizeDateTime
{
  ... # ???
}

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

person Ωmega    schedule 17.09.2019    source источник


Ответы (3)


Примечание. Вопрос и заголовок были изменены после того, как он был опубликован и отредактирован, чтобы настаивать на запросе «поддержки нестандартных» сокращений.

Однако обычно не рекомендуется использовать короткие имена, поскольку вторая часть этого ответа уже подробно обсуждалась. Более того, в контексте этого вопроса это явно недопустимо, поскольку ни одна программа не может знать произвольные сокращения (и для этого также нет никаких «стандартов»).

Как только будет предоставлено сопоставление с принятыми именами, это перестанет быть проблемой, и это также было учтено в ответе. Поэтому я оставляю этот ответ как есть, с небольшими правками.


Используйте 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 говорит о "коротких именах " (аббревиатуры, такие как запрошенные)

Настоятельно рекомендуется использовать эти имена только для отображения. Эти имена не являются официальными, и многие из них просто выдуманы сопровождающими базы данных Олсона. Более того, эти имена не уникальны. Например, «EST» есть как при -0500, так и при +1000/+1100.

(первоначальный акцент)

Один из способов попытаться справиться с аббревиатурами, которые появляются у пользователя, - это all_names из DateTime::TimeZone и grep его вывод для интересующей аббревиатуры. Например,

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

возвращает (список) единственную строку PST8PDT. Эта строка кажется допустимой для всех методов, которые я пробовал, и правильно работает для установки часового пояса для объекта DateTime. Однако, как бы то ни было, для /E(?:S|D)?T/ это возвращает список CET EET EST EST5EDT MET WET; не прост в использовании.

Ясно, что это не является систематическим или надежным, как и аббревиатуры, для начала.

Лучше всего было бы создать какой-то локальный поиск, который переводил бы ваше короткое имя в имя собственное, чтобы вы знали, что оно правильное в вашей работе. Затем заглушка, которая была добавлена ​​в ОП (и позже изменена), может быть заполнена до

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

(См. примечания к документации по различным методам печати.)

Сцепленные методы, создающие возвращаемый объект, обеспечивают синтаксический анализ и изменение часового пояса, используя принятое имя часового пояса.

Для того, чтобы код работал с аббревиатурами, явно требуется преобразование (отображение) локально используемых аббревиатур, представляющих интерес, в правильные имена часовых поясов, предоставленные пользователем.

В приведенном выше фрагменте для этого есть подпрограмма-заполнитель, которая может, например, использовать хэш в модуле с прописанным прямо в нем сопоставлением или, что еще лучше, из файла JSON, которым, таким образом, может управлять и другое программное обеспечение; или запросив таблицу базы данных или, возможно, локальную службу или что-то в этом роде.

person zdim    schedule 17.09.2019
comment
Обратите внимание, что часовой пояс также может быть смещением (например, -0500), если вы знаете смещение, но не политический часовой пояс. Обратите внимание, что выполнение любых арифметических операций с dt, созданным таким образом, может привести к аннулированию часового пояса. Например, зимой в Торонто можно использовать -0500, а летом в Торонто — -0400. Использование America/Toronto будет работать круглый год. - person ikegami; 17.09.2019
comment
Другая сложность заключается в том, что короткие имена просто не уникальны. Например, существует три разных часовых пояса, которые могут использовать аббревиатуру BST, с совершенно разными смещениями. Единственное решение — сохранить карту часовых поясов, которые вам интересны. - person Grinnz; 17.09.2019
comment
@Grinnz Ну да, я обсуждаю это в этом ответе. И я предлагаю, чтобы пользователь предоставил сопоставление (локальный поиск для перевода) - person zdim; 17.09.2019
comment
Читателю: после дополнительной проверки понятия не имею, для чего нужен -1. - person zdim; 30.09.2019

Time::Moment прекрасно справляется с парсингом, но нуждается в небольшой помощи для преобразования в произвольные часовые пояса, которым я предоставляю роль.

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 объекты также будут работать. Определение того, какой фактический часовой пояс использовать на основе ваших сокращений, было рассмотрено в других ответах.

person Grinnz    schedule 17.09.2019

Мы можем использовать библиотеки на основе 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

person Ωmega    schedule 17.09.2019