确定日期开始的时间

ike*_*ami 20 perl time timezone datetime date

假设我想创建一个每日计划器,我想将这一天分成15分钟的块.

容易,对吗?刚从午夜开始,......错了!在America/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
Run Code Online (Sandbox Code Playgroud)

是否有更强大或更直接的替代方案?

ike*_*ami 8

您认为这是需要共同完成的事情!我怀疑那里有很多错误的代码......

这是一个编码的解决方案,旨在将其合并到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);
}
Run Code Online (Sandbox Code Playgroud)

测试:

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
}
Run Code Online (Sandbox Code Playgroud)

一个实际的例子,

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");
}
Run Code Online (Sandbox Code Playgroud)


ike*_*ami 2

[此功能现在可从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);
}
Run Code Online (Sandbox Code Playgroud)

由于大多数日期都有午夜,因此应在顶部添加一个检查,以便在不需要时绕过搜索。

假设:

  • 不存在可以添加时间以获得日期更早的 dt 的 dt。
  • 在任何时区中,日期开始时间都不会早于 UTC 日期开始时间 24*60*60 秒。
  • 在任何时区中,日期开始时间都不会比 UTC 日期开始时间晚 24*60*60 秒。
  • 时区跳跃仅发生在零秒的时间。(优化)

测试:

{
    # 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
}
Run Code Online (Sandbox Code Playgroud)