perl - Determinar la hora a la que comienza una fecha.
datetime timezone php (7)
Digamos que quiero crear un planificador diario, y quiero dividir el día en partes de 15 minutos.
Fácil, ¿verdad? Simplemente comienza a la medianoche, y ... ¡Mal! En América / Sao_Paulo, un día cada año comienza a las 01:00 debido a los cambios en el horario de verano.
Dada una zona horaria y una fecha, ¿cómo se encuentra la hora de la época en que comienza el día?
Mi primer pensamiento fue usar lo siguiente, pero se supone que cada día tiene un 23:59. Probablemente no sea mejor suponer que asumir que cada día tiene una medianoche.
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
¿Existe una alternativa más robusta o más directa?
¡Usted pensaría que esto es algo que debe hacerse comúnmente! Sospecho que hay un montón de código de buggy por ahí ...
Aquí hay una solución que está codificada con la intención de intentar que se incorpore a 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);
}
Prueba:
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
}
Un ejemplo práctico,
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");
}
¿Todos están perdiendo la forma realmente obvia de hacerlo? Es medianoche en el día actual. Ponga los segundos, minutos y horas en cero, y tome los campos mday, mon y year de la hora local.
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
Puede usar DateTime :: TimeZone directamente para consultar una hora local válida. DateTime :: TimeZone genera una excepción si la hora local no existe debido a un cambio de desplazamiento cercano.
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";
Un enfoque razonable sería comenzar a las 12:00 PM (mediodía) de ese día, y trabajar hacia atrás de manera incremental hasta que la fecha cambie. Lo mismo se avanza para encontrar el final del día.
El mediodía es apropiado, porque (AFAIK) todas las zonas horarias que tienen cambios de horario de verano cambian en medio de la noche, para minimizar el impacto en los seres humanos. Presumiblemente, la gran mayoría de las personas están despiertas durante el día, por lo que los gobiernos serían tontos al establecer cambios de horario de verano en horario comercial.
Te gustaría moverte en incrementos de 15 minutos para cubrir todas las bases. Hay algunas zonas horarias con: 30 o: 45 compensaciones de minutos, y algunas que solo cambian en 30 minutos para DST.
Ahora, si está volviendo a la antigüedad, esta no es la mejor solución porque muchas zonas horarias tenían ajustes por otras razones que no eran DST, como la sincronización inicial con UTC, que podría tener un valor de minutos o segundos impares. Así que esto debería funcionar bien con fechas razonablemente presentes, pero no para todas las fechas pasadas.
Si desea algo que sea menos lineal, entonces el algoritmo tendría que determinar el intervalo de los límites para la regla de la zona horaria en la que se realizó la fecha y luego usarlos para verificar si caen en el día en cuestión o no. En el código fuente de Datetime::TimeZone
, veo que define un concepto interno de "intervalo". Puede usar DateTime::TimeZone->_span_for_datetime
para encontrar el intervalo en el que cayó la fecha en cuestión, y luego verificar las fechas de inicio y finalización desde allí.
No soy un programador de Perl, así que le dejaré ese ejercicio a usted oa alguien más. Además, verifiqué y los valores en el intervalo no parecen ser marcas de tiempo de Unix, así que no estoy muy seguro de cómo tomarlo desde allí, y parecen ser indocumentados / internos, por lo que no creo que sea necesariamente una Buena idea en Perl de todos modos.
Una posible (incómoda) solución: calcular un tiempo conservador (por ejemplo, 23:00:00 o 23: 50: 00 - la única parte importante es que ninguna fecha pasada o futura debe pasar antes de esta hora), y luego aumentar Ese tiempo hasta la fecha cambia:
#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.
Estoy 100% seguro de que hay una mejor solución para esto; lo ideal sería que DateTime estableciera de forma predeterminada la primera hora válida para una zona horaria determinada, dada una fecha sin hora; actualmente, el valor predeterminado es cero para todos esos valores, y no estoy seguro de que corrija el valor si no es válido para esa TZ. Si corrige internamente esos valores, entonces esa solución sería muy preferible; podría valer la pena ponerse en contacto con el mantenedor de DateTime para ver cuál es el comportamiento real, y si dicho comportamiento está garantizado en el futuro si actualmente es el comportamiento deseado.
[ Esta funcionalidad ahora está disponible desde DateTimeX::Start ]
Aquí hay una solución utilizando solo los métodos públicos de 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);
}
Como la mayoría de las fechas tienen medianoche, se debe agregar un cheque en la parte superior para omitir la búsqueda cuando no sea necesario.
Suposiciones
- No hay un dt al que se pueda agregar tiempo para obtener un dt con una fecha anterior.
- En ninguna zona horaria, una fecha comienza más de 24 * 60 * 60 segundos antes de que comience la fecha en UTC.
- En ninguna zona horaria, una fecha comienza más de 24 * 60 * 60 segundos después de la fecha en UTC.
- Los saltos en zonas horarias solo ocurren en tiempos con cero segundos. (Mejoramiento)
Prueba:
{
# 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
}
Time::Local función timelocal()
Time::Local es lo suficientemente inteligente como para hacer lo correcto aquí si pides la hora de la medianoche. Para 2014, los cambios de horario de verano son los siguientes:
$ 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
Así que la medianoche "falta" el 2014-10-19. Sin embargo, si realmente pedimos el tiempo de la época para eso de todos modos, y luego lo convertimos en un tiempo local:
$ 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
Y un segundo antes:
$ 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