说我想创建一个每日计划者,我想把这一天分成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
是否有更强大或更直接的替代方案?
答案 0 :(得分: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);
}
测试:
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");
}
答案 1 :(得分:3)
合理的方法是在当天中午12点(中午)开始,并逐步向后工作直到更改日期。同样可以在今天结束时找到。
中午是合适的,因为(AFAIK)所有具有DST的时区都会在半夜发生变化,以尽量减少对人类的影响。据推测,绝大多数人在白天都是清醒的,所以政府在工作时间设置DST变化是愚蠢的。
您希望以15分钟的增量移动以覆盖所有基础。有些时区有:30或45分钟的偏移,有些时区只有30分钟的DST。
现在,如果你要回到古代,这不是最佳解决方案,因为许多时区因DST之外的其他原因进行了调整 - 例如与UTC的初始同步,这可能是几分钟或几秒的奇数值。因此,这应该适用于合理的当前日期,但不适用于所有过去的日期。
如果你想要一些不那么线性的东西,那么算法必须确定日期所属的时区规则的边界间隔,然后用它们来检查它们是否落在有问题的那一天。在source code for Datetime::TimeZone
中,我看到它定义了“跨度”的内部概念。您可以使用DateTime::TimeZone->_span_for_datetime
查找相关日期所涉及的范围,然后从那里查看开始日期和结束日期。
我不是Perl程序员,所以我会把这个练习留给你或其他人。此外,我检查并且跨度中的值似乎不是unix时间戳,所以我不太确定如何从那里采取它 - 它们似乎没有文档/内部因此我认为这不一定是无论如何,在Perl中都是个好主意。
答案 2 :(得分:3)
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
答案 3 :(得分:2)
每个人都错过了真正明显的方法吗?这是当天的午夜。即将秒,分钟和小时设置为零,并从本地时间获取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
答案 4 :(得分: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);
}
由于大部分日期都有午夜,因此应在顶部添加支票,以便在不需要时绕过搜索。
假设:
测试:
{
# 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
}
答案 5 :(得分:1)
一个(繁琐)可能的解决方案:找出一个保守的时间(例如,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.
我百分百肯定有更好的解决方案;给定一个没有时间的日期,如果DateTime默认为给定时区的最早有效时间,那么理想情况就是 - 目前所有这些值都默认为零,我不确定它是否会更正值它对那个TZ无效。如果它在内部纠正这些值,那么该解决方案将是非常可取的;可能值得联系DateTime的维护者来查看实际行为是什么,以及如果当前行为是期望的行为,将来是否保证所述行为。
答案 6 :(得分:1)
您可以直接使用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 $@ !~ /^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";