Определение времени начала даты
Скажем, я хочу создать ежедневный планировщик, и я хочу разделить день на 15-минутные куски.
Легко, правда? Просто начинай в полночь и... Неправильно! В Америке /Sao _Paulo, один день каждый год начинается в 01:00 из-за изменения летнего времени.
Учитывая часовой пояс и дату, как можно найти время, в которое начинается день?
Моя первая мысль заключалась в том, чтобы использовать следующее, но предполагает, что каждый день имеет 23:59. Вероятно, это не лучше предположения, чем предположить, что каждый день имеет полночь.
perl -MDateTime -E'
say
DateTime->new( year => 2013, month => 10, day => 20 )
->subtract( days => 1 )
->set( hour => 23, minute => 59 )
->set_time_zone("America/Sao_Paulo")
->add( minutes => 1 )
->strftime("%H:%M");
'
01:00
Существует ли более надежная или более прямая альтернатива?
Ответы
Ответ 1
[Эта функциональность теперь доступна из DateTimeX::Start]
Здесь решение, использующее только общедоступные DT-методы:
sub day_start {
my ($y, $m, $d, $tz) = @_;
$tz = DateTime::TimeZone->new( name => $tz )
if !ref($tz);
my $dt = DateTime->new( year => $y, month => $m, day => $d );
my $target_day = ( $dt->utc_rd_values )[0];
my $min_epoch = int($dt->epoch()/60) - 24*60;
my $max_epoch = int($dt->epoch()/60) + 24*60;
while ($max_epoch > $min_epoch) {
my $epoch = ( $min_epoch + $max_epoch ) >> 1;
$dt = DateTime->from_epoch( epoch => $epoch*60, time_zone => $tz );
if (( $dt->local_rd_values )[0] < $target_day) {
$min_epoch = $epoch;
} else {
$max_epoch = $epoch;
}
}
return DateTime->from_epoch(epoch => $max_epoch*60, time_zone => $tz);
}
Поскольку большинство дат имеют полночь, в верхней части следует добавить проверку, чтобы обойти поиск, когда это не нужно.
Предположения:
- Нет dt, на который можно добавить время для получения dt с более ранней датой.
- В часовом поясе дата начинается больше, чем за 24 * 60 * 60 секунд до начала даты в UTC.
- В часовом поясе дата начинается более 24 * 60 * 60 секунд после начала даты в UTC.
- Переходы в часовых поясах происходят только в моменты времени с нулевыми секундами. (Оптимизация)
Тест:
{
# No midnight.
my $tz = DateTime::TimeZone->new( name => 'America/Sao_Paulo' );
my $dt = day_start(2013, 10, 20, $tz);
print($dt->epoch, " ", $dt->iso8601, "\n"); # 1382238000 2013-10-20T01:00:00
$dt->subtract( seconds => 1 );
print($dt->epoch, " ", $dt->iso8601, "\n"); # 1382237999 2013-10-19T23:59:59
}
{
# Two midnights.
my $tz = DateTime::TimeZone->new( name => 'America/Havana' );
my $dt = day_start(2013, 11, 3, $tz);
print($dt->epoch, " ", $dt->iso8601, "\n"); # 1383451200 2013-11-03T00:00:00
$dt->subtract( seconds => 1 );
print($dt->epoch, " ", $dt->iso8601, "\n"); # 1383451199 2013-11-02T23:59:59
}
Ответ 2
Вы думаете, что это то, что нужно делать обычно! Я подозреваю, что там есть много ошибок...
Вот решение, закодированное с намерением попытаться включить его в DateTime.
use strict;
use warnings;
use DateTime qw( );
use DateTime::TimeZone qw( );
# Assumption:
# There is no dt to which one can add time
# to obtain a dt with an earlier date.
sub day_start {
my $tz = shift;
my $dt = shift;
my $local_rd_days = ( $dt->local_rd_values() )[0];
my $seconds = $local_rd_days * 24*60*60;
my $min_idx;
if ( $seconds < $tz->max_span->[DateTime::TimeZone::LOCAL_END] ) {
$min_idx = 0;
} else {
$min_idx = @{ $tz->{spans} };
$tz->_generate_spans_until_match( $dt->utc_year()+1, $seconds, 'local' );
}
my $max_idx = $#{ $tz->{spans} };
my $utc_rd_days;
my $utc_rd_secs;
while (1) {
my $current_idx = int( ( $min_idx + $max_idx )/2 );
my $current = $tz->{spans}[$current_idx];
if ( $seconds < $current->[DateTime::TimeZone::LOCAL_START] ) {
$max_idx = $current_idx - 1;
}
elsif ( $seconds >= $current->[DateTime::TimeZone::LOCAL_END] ) {
$min_idx = $current_idx + 1;
}
else {
my $offset = $current->[DateTime::TimeZone::OFFSET];
# In case of overlaps, always prefer earlier span.
if ($current->[DateTime::TimeZone::IS_DST] && $current_idx) {
my $prev = $tz->{spans}[$current_idx-1];
$offset = $prev->[DateTime::TimeZone::OFFSET]
if $seconds >= $prev->[DateTime::TimeZone::LOCAL_START]
&& $seconds < $prev->[DateTime::TimeZone::LOCAL_END];
}
$utc_rd_days = $local_rd_days;
$utc_rd_secs = -$offset;
DateTime->_normalize_tai_seconds($utc_rd_days, $utc_rd_secs);
last;
}
if ($min_idx > $max_idx) {
$current_idx = $min_idx;
$current = $tz->{spans}[$current_idx];
if (int( $current->[DateTime::TimeZone::LOCAL_START] / (24*60*60) ) != $local_rd_days) {
my $err = 'Invalid local time for date';
$err .= " in time zone: " . $tz->name;
$err .= "\n";
die $err;
}
$utc_rd_secs = $current->[DateTime::TimeZone::UTC_START] % (24*60*60);
$utc_rd_days = int( $current->[DateTime::TimeZone::UTC_START] / (24*60*60) );
last;
}
}
my ($year, $month, $day) = DateTime->_rd2ymd($utc_rd_days);
my ($hour, $minute, $second) = DateTime->_seconds_as_components($utc_rd_secs);
return
$dt
->_new_from_self(
year => $year,
month => $month,
day => $day,
hour => $hour,
minute => $minute,
second => $second,
time_zone => 'UTC',
)
->set_time_zone($tz);
}
Тест:
sub new_date {
my $y = shift;
my $m = shift;
my $d = shift;
return DateTime->new(
year => $y, month => $m, day => $d,
@_,
hour => 0, minute => 0, second => 0, nanosecond => 0,
time_zone => 'floating'
);
}
{
# No midnight.
my $tz = DateTime::TimeZone->new( name => 'America/Sao_Paulo' );
my $dt = day_start($tz, new_date(2013, 10, 20));
print($dt->iso8601(), "\n"); # 2013-10-20T01:00:00
$dt->subtract( seconds => 1 );
print($dt->iso8601(), "\n"); # 2013-10-19T23:59:59
}
{
# Two midnights.
my $tz = DateTime::TimeZone->new( name => 'America/Havana' );
my $dt = day_start($tz, new_date(2013, 11, 3));
print($dt->iso8601(), "\n"); # 2013-11-03T00:00:00
$dt->subtract( seconds => 1 );
print($dt->iso8601(), "\n"); # 2013-11-02T23:59:59
}
Практический пример,
sub today_as_floating {
return
DateTime
->now( @_ )
->set_time_zone('floating')
->truncate( to => 'day' );
}
{
my $tz = DateTime::TimeZone->new( name => 'local' );
my $dt = today_as_floating( time_zone => $tz );
$dt = day_start($tz, $dt);
print($dt->iso8601(), "\n");
}
Ответ 3
Разумный подход состоял бы в том, чтобы начать в 12:00 (в полдень) в этот день и постепенно работать до тех пор, пока не изменится дата. То же самое произойдет и в конце дня.
Полдень подходит, потому что (AFAIK) все часовые пояса, у которых DST меняется переход в середине ночи, чтобы свести к минимуму воздействие на людей. Предположительно, подавляющее большинство людей бодрствуют в течение дня, поэтому правительствам было бы глупо устанавливать изменения ДСТ в рабочее время.
Вы хотите двигаться с шагом в 15 минут, чтобы охватить все базы. Есть несколько часовых поясов с: 30 или: 45 минутами, а некоторые - только на 30 минут для DST.
Теперь, если вы вернетесь в древность, это не лучшее решение, потому что во многих часовых поясах были настроены другие причины, кроме DST, такие как начальная синхронизация с UTC, что может быть нечетным количеством минут или секунд. Таким образом, это должно отлично работать с достаточно датированными датами, но не для всех прошлых дат.
Если вам нужно что-то менее линейное, тогда алгоритм должен будет определить интервал границ для правила часового пояса, в который была введена дата, а затем использовать их для проверки того, попадают ли они в этот день или нет, В исходный код для Datetime::TimeZone
я вижу, что он определяет внутреннюю концепцию "span". Вы можете использовать DateTime::TimeZone->_span_for_datetime
, чтобы найти интервал, в который попала данная дата, а затем проверить даты начала и окончания.
Я не программист на Perl, поэтому я оставлю это упражнение вам или кому-то еще. Кроме того, я проверил, и значения в диапазоне не выглядят как временные метки unix, поэтому я не совсем уверен, как их оттуда оттуда - и они выглядят недокументированными/внутренними, поэтому я не думаю, что обязательно хорошая идея в Perl в любом случае.
Ответ 4
Функция Time:: Local timelocal()
достаточно умна, чтобы делать правильные вещи здесь, если вы попросите время эпохи в полночь. В течение 2014 года изменения DST выглядят следующим образом:
$ zdump -v America/Sao_Paulo | fgrep 2014
America/Sao_Paulo Sun Feb 16 01:59:59 2014 UTC = Sat Feb 15 23:59:59 2014 BRST isdst=1 gmtoff=-7200
America/Sao_Paulo Sun Feb 16 02:00:00 2014 UTC = Sat Feb 15 23:00:00 2014 BRT isdst=0 gmtoff=-10800
America/Sao_Paulo Sun Oct 19 02:59:59 2014 UTC = Sat Oct 18 23:59:59 2014 BRT isdst=0 gmtoff=-10800
America/Sao_Paulo Sun Oct 19 03:00:00 2014 UTC = Sun Oct 19 01:00:00 2014 BRST isdst=1 gmtoff=-7200
Так что полночь "отсутствует" в 2014-10-19. Однако, если мы на самом деле попросим время для этого в любом случае, а затем преобразуем его обратно в локальное время:
$ TZ=America/Sao_Paulo perl -MTime::Local -E 'say scalar localtime(timelocal(0, 0, 0, 19, 9, 114))'
Sun Oct 19 01:00:00 2014
И за секунду до:
$ TZ=America/Sao_Paulo perl -MTime::Local -E 'say scalar localtime(timelocal(0, 0, 0, 19, 9, 114)-1)'
Sat Oct 18 23:59:59 2014
Ответ 5
Одно (громоздкое) возможное решение: выясните консервативное время (например, 23:00:00 или 23: 50: 00 - единственная важная часть заключается в том, что никакая дата или будущее не должны перевернуться до этого времени), и затем увеличивайте это время до изменения даты:
#Assume $year/$month/$day contain the date one day prior to the target date
my $dt = DateTime->new(
time_zone => $tz,
year => $year,
month => $month,
day => $day,
hour => 23,
minute => 59,
second => 0,
);
while($dt->year == $year && $dt->month == $month && $dt->day == $day) {
$dt->add(seconds => 1);
}
#At this point $dt should, if I understand the functioning of DateTime correctly, contain the earliest "valid" time in the target date.
Я на 100% уверен, что есть лучшее решение для этого; идеальным было бы, если бы DateTime по умолчанию выполняла самое раннее действительное время для данного часового пояса, учитывая дату без времени - в настоящее время по умолчанию она равна нулю для всех этих значений, и я не уверен, что она исправит значение if это недействительно для этого TZ. Если это внутренне исправить эти значения, то это решение было бы намного предпочтительнее; возможно, стоит обратиться к хранителю DateTime, чтобы узнать, что такое фактическое поведение, и если указанное поведение будет гарантировано в будущем, если в настоящее время это желаемое поведение.
Ответ 6
Вы можете использовать DateTime:: TimeZone напрямую, чтобы запросить допустимое местное время. DateTime:: TimeZone вызывает исключение, если локальное время не существует из-за смещения смещения.
use DateTime;
use DateTime::TimeZone;
my $zone = DateTime::TimeZone->new(name => 'America/Sao_Paulo');
my $dt = DateTime->new(year => 2013, month => 10, day => 20);
sub valid_local_time {
eval { $zone->offset_for_local_datetime($dt) };
return [email protected] !~ /^Invalid local time/;
}
while (!valid_local_time()) {
$dt->add(minutes => 15);
}
$dt->set_time_zone($zone);
sub local_time_lt {
my ($x, $y) = @_;
return $x->local_rd_as_seconds < $y->local_rd_as_seconds;
}
sub local_time_eq {
my ($x, $y) = @_;
return $x->local_rd_as_seconds == $y->local_rd_as_seconds;
}
my $copy = $dt->clone->subtract(seconds => 1);
if (local_time_lt($dt, $copy)) {
my $delta = $copy->local_rd_as_seconds - $dt->local_rd_as_seconds;
local_time_eq($dt, $copy->subtract(seconds => $delta))
or die qq/Could not determine start of day ($dt [${\$zone->name}])/;
$dt = $copy;
}
print $dt->strftime('%H:%M'), "\n";
Ответ 7
Неужели у всех отсутствует действительно очевидный способ сделать это? Это полночь в текущий день. То есть установите секунды, минуты и часы на ноль и возьмите поля mday, mon и year из локального времени.
use POSIX qw( mktime tzset );
$ENV{TZ} = 'America/Sao_Paulo';
tzset();
my $epoch = mktime( 0, 0, 0, 20, 10-1, 2013-1900 );
print localtime($epoch)."\n"; # Sun Oct 20 01:00:00 2013