ikegami - 1 year ago 35

Perl Question

Say I want to create a daily planner, and I want to divide the day into 15 minute chunks.

Easy, right? Just start at midnight, and... Wrong! In America/Sao_Paulo, one day each year starts at 01:00 because of Daylight Saving Time changes.

Given a time zone and a date, how does one find the epoch time at which the day starts?

My first thought was to use the following, but it assumes each day has a 23:59. That's probably no better of an assumption than assuming each day has a midnight.

`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

Is there a more robust or more direct alternative?

Answer Source

Here's a solution using only DT's public methods:

```
sub day_start {
my $dt = shift;
my $tz = shift;
# Expect floating or UTC, but let's make sure.
$dt->set_time_zone('floating');
$dt->truncate( to => 'day' );
# This will work except for days without midnight.
if (eval { $dt->set_time_zone($tz); 1 }) {
return $dt;
}
my $ymd = $dt->ymd();
my $min_epoch = $dt->epoch() - 48*60*60;
my $max_epoch = $dt->epoch() + 48*60*60;
while (1) {
my $current_epoch = int( ( $min_epoch + $max_epoch )/2 );
_set_epoch($dt, $current_epoch);
my $current_ymd = $dt->ymd();
if ($current_ymd lt $ymd) {
$min_epoch = $current_epoch + 1;
} else {
$dt->subtract( seconds => 1 );
my $earlier_ymd = $dt->ymd();
if ($earlier_ymd ge $ymd) {
$max_epoch = $current_epoch - 1;
} else {
_set_epoch($dt, $current_epoch);
return $dt;
}
}
}
}
# Based on DateTime::from_epoch
sub _set_epoch {
my ($dt, $epoch) = @_;
my %args;
# # Epoch may come from Time::HiRes, so it may not be an integer.
# ($epoch, my $dec) = $epoch =~ /^(-?\d+)?(\.\d+)?/;
# $epoch ||= 0;
#
# $args{nanosecond} = int($dec * 1_000_000_000) if $dec;
# Note: For very large negative values, this may give a blatantly wrong answer.
@args{qw( second minute hour day month year )} = gmtime($epoch);
$args{year} += 1900;
++$args{month};
my $tz = $dt->time_zone();
$dt->set_time_zone('UTC');
$dt->set(%args);
$dt->set_time_zone($tz);
return $dt;
}
```

Assumptions:

- There is no dt to which one can add time to obtain a dt with an earlier date.
- In no time zone does a date starts more than 48*60*60 seconds before the date starts in UTC.
- In no time zone does a date starts more than 48*60*60 seconds after the date starts in UTC.

Test:

```
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(new_date(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(new_date(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
}
```