ikegami - 1 year ago 54
Perl Question

# Determining the time at which a date starts

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")
->strftime("%H:%M");
'
01:00
``````

Is there a more robust or more direct alternative?

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
}
``````
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download