Подтвердить что ты не робот

Определение времени начала даты

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

Существует ли более надежная или более прямая альтернатива?

4b9b3361

Ответ 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