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?
You'd think this is something that needs to be done commonly! I suspect there's a lot of buggy code out there...
Here's a solution that's coded with the intention of trying to get it incorporated into 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);
}
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($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
}
A practical example,
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");
}
A reasonable approach would be to start at 12:00 PM (noon) on that day, and work backwards incrementally until the date changed. The same going forward to find the end of the day.
Noon is appropriate, because (AFAIK) all time zones that have DST changes transition in the middle of the night, to minimize the impact on human beings. Presumably, the vast majority of people are awake during the day, so governments would be foolish to set DST changes during business hours.
You would want to move in 15 minute increments to cover all bases. There are some time zones with :30 or :45 minute offsets, and some that only change by 30 minutes for DST.
Now if you are going back into antiquity, this isn't the best solution because many time zones had adjustments for other reasons than DST - such as initial synchronization with UTC, which could be by some odd minutes or seconds value. So this should work fine with reasonably present dates, but not for all past dates.
If you want something that is less linear, then the algorithm would have to determine the interval of the boundaries for the time zone rule that the date fell into, then use those to check if they fall on the day in question or not. In the source code for Datetime::TimeZone, I see that it defines an internal concept of a "span". You could use DateTime::TimeZone->_span_for_datetime to find the span that the date in question fell into, and then check the start and end dates from there.
I am not a Perl programmer, so I'll leave that exercise to you or someone else. Besides, I checked and the values in the span don't appear to be unix timestamps, so I'm not quite sure how to take it from there - and they appear to be undocumented/internal so I don't think that's necessarily a good idea in Perl anyway.
Time::Local's timelocal() function is clever enough to do the right thing here if you ask for the epoch time for midnight. For 2014, DST changes are as follows:
$ 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
So midnight is "missing" on 2014-10-19. However, if we actually ask for the epoch time for that anyway, and then convert it back into a local time:
$ 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
And one second before:
$ 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
[This functionality is now available from DateTimeX::Start]
Here's a solution using only DT's public methods:
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);
}
Since most dates do have a midnight, a check should be added at the top to bypass the search when it's not needed.
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 24*60*60 seconds before the date starts in UTC.
In no time zone does a date starts more than 24*60*60 seconds after the date starts in UTC.
Jumps in time zones only occur on times with zero seconds. (Optimization)
Test:
{
# 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
}
One (cumbersome) possible solution: figure out a conservative time (say, 23:00:00 or 23:50:00--the only important part is that no date past or future should roll over before this time), and then increment that time until the date changes:
#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.
I'm 100% sure there is a better solution to this; the ideal would be if DateTime defaulted to the earliest valid time for a given time zone, given a date with no time--currently it defaults to zero for all of those values, and I'm not certain that it will correct the value if it's not valid for that TZ. If it does internally correct those values, then that solution would be vastly preferable; it might be worth contacting the maintainer of DateTime to see what the actual behaviour is, and if said behaviour is guaranteed in the future if it is currently the desired behaviour.
You can use DateTime::TimeZone directly to query for a valid local time. DateTime::TimeZone raises an exception if the local time does not exist due to a nearby offset change.
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";
Is everyone missing the really obvious way to do it? It's midnight on the current day. I.e. set the seconds, minutes and hours to zero, and take the mday, mon and year fields from localtime.
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
Related
I'm trying to use Perl's DateTime to subtract one day from another with a sign. I can get the days in between easily:
sub delta_days {
my $date1 = shift;
my $date2 = shift;
my $d1 = str_to_date_object($date1);
my $d2 = str_to_date_object($date2);
return $d2->delta_days($d1)->delta_days();
}
say delta_days('2021-10-21', '1980-8-20');
say delta_days('1980-8-20', '2021-10-21');
but both of these calls give the difference as 15037, without a sign.
Following the documentation https://metacpan.org/pod/DateTime
I see
# not DST
my $dt1 = DateTime->new(
year => 2003,
month => 5,
day => 6,
time_zone => 'America/Chicago',
);
# is DST
my $dt2 = DateTime->new(
year => 2003,
month => 11,
day => 6,
time_zone => 'America/Chicago',
);
my $dur = $dt2->subtract_datetime($dt1)->days();
I have checked a similar question in How to make DateTime::Duration output only in days? but I don't see how to get a signed difference there.
How can I get the difference in days with a sign?
Two issues: You're doing the subtraction on dates in the wrong order if you want a negative offset, and with the dates you're testing subtract_datetime() with, the duration object is going to have 0 days.
Compare with this version:
#!/usr/bin/env perl
use strict;
use warnings;
use feature qw/say/;
use DateTime;
use Data::Dumper;
# not DST
my $dt1 = DateTime->new(
year => 2003,
month => 5,
day => 6,
time_zone => 'America/Chicago',
);
# is DST
my $dt2 = DateTime->new(
year => 2003,
month => 11,
day => 6,
time_zone => 'America/Chicago',
);
say $dt1;
say $dt2;
my $dur = $dt1->subtract_datetime($dt2);
print Dumper({$dur->deltas});
which when run produces
2003-05-06T00:00:00
2003-11-06T00:00:00
$VAR1 = {
'minutes' => 0,
'nanoseconds' => 0,
'months' => -6,
'seconds' => 0,
'days' => 0
};
Note the -6 months offset. DateTime::Duration objects will not convert between months and days according to the documentation so this looks like a dead end.
The delta_days method you looked at first returns the actual difference in days, but as noted it's an absolute value. You can add a check to convert it to negative if the first date is before the second:
my $days = $dt1->delta_days($dt2)->in_units('days');
$days *= -1 if $dt1 < $dt2;
say $days; # Prints -184
and a variation of your delta_days() function:
sub delta_days {
my $date1 = shift;
my $date2 = shift;
my $d1 = str_to_date_object($date1);
my $d2 = str_to_date_object($date2);
return $d1->delta_days($d2)->in_units('days') * ($d1 < $d2 ? -1 : 1);
}
say delta_days('2021-10-21', '1980-8-20');
say delta_days('1980-8-20', '2021-10-21');
PERL : I take mday value from locatime as below ,now I want value of day before yesterday . How can I subtract 1 from mday which I take from localtime
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();
my $part = "P".$mday;
print "Today value is $part \n";
my $part_yes = "P".$mday - $num;
print "$part_yes \n";
Using DateTime:
my $dt =
DateTime
->now( time_zone => 'local' )
->set_time_zone('floating') # Do this when working with dates.
->truncate( to => 'days' ); # Optional.
$dt->subtract( days => 2 );
my $yesterday = $dt->day;
DateTime is pretty heavy, and it seems people asking date-time questions invariably come back and say "core modules only!", so here's a solution using only core modules.
use Time::Local qw( timegm );
# Create an timestamp with the same date in UTC as the one local one.
my $epoch = timegm(0, 0, 0, ( localtime() )[3,4,5]);
# We can now do date arithmetic without having to worry about DST switches.
$epoch -= 2 * 24*60*60;
my $yesterday = ( gmtime($epoch) )[3] + 1;
my $now = time(); # time as seconds since 1970-01-01
my $day_ago = $now - 24*60*60;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($day_ago);
my $part = "P".$mday;
WARNING (ikegami comment): Not all days have 24 hours. This can produce a result that's 0, 1 or 2 days earlier
https://perldoc.perl.org/functions/time
https://perldoc.perl.org/functions/localtime
I have a task in perl in which I need to execute 'some_code' but only if date is older than 24 hours counting from now. I'm trying the below code but it doesn't seems to be working.
sub function {
use Date::Manip::Date
use Date::Parse
use Date::Format;
my $yesterday = time() - 60*60*24;
my $x = shift;
my $env = shift;
$env->{some_code} = 1 if $x < $yesterday;
return $x;
}
You can do it easily, only using core functions.
#!/usr/bin/perl
use strict;
my $new_time = 1350570164; # 2012-10-18 14:22:44
my $older_time = 1350450164; # 2012-10-17 05:02:44
printf "time in sec: %d older that 24 hours: %d\n", $new_time, is_time_older_24($new_time);
printf "time in sec: %d older than 24 hours: %d\n", $older_time, is_time_older_24($older_time);
sub is_time_older_24 {
my $given_time = shift;
my $yesterday_time = time() - 60 * 60 * 24;
return $given_time <= $yesterday_time
? 1
: 0;
}
Output:
time in sec: 1350570164 older that 24 hours: 0
time in sec: 1350450164 older than 24 hours: 1
#! /usr/bin/env perl
use Modern::Perl;
use Data::Dumper;
use DateTime;
my $now = DateTime->new(
year => 2012, month => 10, day => 18,
hour => 17, minute => 30,
time_zone => 'UTC'
);
# my $now = DateTime->now(time_zone => 'UTC');
my $last_run = DateTime->new(
year => 2012, month => 10, day => 17,
hour => 19, minute => 30,
time_zone => 'UTC'
);
my $duration= $now->subtract_datetime($last_run);
say "hours: " . $duration->hours;
Result:
hours: 22
see also:
https://metacpan.org/module/DateTime
https://metacpan.org/module/DateTime::Duration
https://metacpan.org/module/DateTime::Format::Duration
I thought this was going to be very simple but I am really out of options now. I want to substract 15 minutes from a given time.
Example
My time is 15:04 I want to substract 15 minutes to be 14:49. I have searched for solutions on the internet but there is no perl module that can help me out.
You can use DateTime:
my $dt = DateTime->new(
year => 1,
month => 1,
day => 1,
hour => 15,
minute => 4,
);
$dt->subtract(minutes => 15);
printf "%d:%d\n", $dt->hour, $dt->minute; # prints 14:49
Well it all depends on how your time is stored. I prefer to use a time_t as returned by the time built in.
my $now = time();
my $before1 = $now - (15*60); # 15 minutes ago
my $before2 = $now - (3*60*60); # 3 hours ago
my $before3 = $now - (2*24*60*60); # 2 days ago
For output I use the POSIX module
print POSIX::strftime( '%Y-%m-%d %T', localtime($before1) );
perl -MClass::Date -e 'my $d=Class::Date->new("2011-07-13 15:04:00"); my $d2 = $d-"15m"; print $d2, "\n";'
Output:
2011-07-13 14:49:00
Try using Date::Calc
use Date::Calc qw(Add_Delta_DHMS);
($year2, $month2, $day2, $h2, $m2, $s2) =
Add_Delta_DHMS( $year, $month, $day, $hour, $minute, $second, $days_offset, $hour_offset, $minute_offset, $second_offset );
($y,$m,$d,$H,$M,$S) = Add_Delta_DHMS(Today_and_Now(), 0, 0, -15, 0);
convert the time to unix time, for example the current time: $unixtime = time(); then subtract 15*60 from it then convert to a nice string with something like
sub display_time {
my ($sec,$min,$hour,$mday,$mon,$year,undef,undef,undef) = localtime(time);
$year += 1900;
$mon += 1;
return "$year.".sprintf("%02d.%02d %02d:%02d:%02d",$mon,$mday,$hour,$min,$sec);
}
You can use the below sub-routine if you are only concerned about time not date:
sub subTime{
my ($time) = #_;
my #splittime = split(':', $time);
my $hour = $splittime[0];
my $min = $splittime[1];
if($min < 15){
$min=($min+60)-15;
$hour-=1;
}
else{
$min = $min-15;
}
return "$hour:$min";
}
Disclamer: This was the solution OP used, he mentioned it in comments in above answer (in #eugene's answer).
I have the following loop to calculate the dates of the current week and print them out. It works, but I am swimming in the amount of date/time possibilities in Perl and want to get your opinion on whether there is a better way. Here's the code I've written:
#!/usr/bin/env perl
use warnings;
use strict;
use DateTime;
# Calculate numeric value of today and the
# target day (Monday = 1, Sunday = 7); the
# target, in this case, is Monday, since that's
# when I want the week to start
my $today_dt = DateTime->now;
my $today = $today_dt->day_of_week;
my $target = 1;
# Create DateTime copies to act as the "bookends"
# for the date range
my ($start, $end) = ($today_dt->clone(), $today_dt->clone());
if ($today == $target)
{
# If today is the target, "start" is already set;
# we simply need to set the end date
$end->add( days => 6 );
}
else
{
# Otherwise, we calculate the Monday preceeding today
# and the Sunday following today
my $delta = ($target - $today + 7) % 7;
$start->add( days => $delta - 7 );
$end->add( days => $delta - 1 );
}
# I clone the DateTime object again because, for some reason,
# I'm wary of using $start directly...
my $cur_date = $start->clone();
while ($cur_date <= $end)
{
my $date_ymd = $cur_date->ymd;
print "$date_ymd\n";
$cur_date->add( days => 1 );
}
As mentioned, this works, but is it the quickest or most efficient? I'm guessing that quickness and efficiency may not necessarily go together, but your feedback is very appreciated.
A slightly improved version of friedo's answer ...
my $start_of_week =
DateTime->today()
->truncate( to => 'week' );
for ( 0..6 ) {
print $start_of_week->clone()->add( days => $_ );
}
However, this assumes that Monday is the first day of the week. For Sunday, start with ...
my $start_of_week =
DateTime->today()
->truncate( to => 'week' )
->subtract( days => 1 );
Either way, it's better to use the truncate method than re-implement it, as friedo did ;)
You can use the DateTime object to get the current day of the week as a number ( 1-7 ). Then just use that to find the current week's Monday. For example:
my $today = DateTime->now;
my $start = $today->clone;
# move $start to Monday
$start->subtract( days => ( $today->wday - 1 ) ); # Monday gives 1, so on monday we
# subtract zero.
my $end = $start->clone->add( days => 7 );
The above is untested but the idea should work.
Would this work:
use strict;
use warnings;
use POSIX qw<strftime>;
my ( $day, $pmon, $pyear, $wday ) = ( localtime )[3..6];
$day -= $wday - 1; # Get monday
for my $d ( map { $day + $_ } 0..6 ) {
print strftime( '%A, %B %d, %Y', ( 0 ) x 3, $d, $pmon, $pyear ), "\n";
}
I'm printing them only as an illustration. You could store them as timestamps, like this:
use POSIX qw<mktime>;
my #week = map { mktime(( 0 ) x 3, $day + $_, $pmon, $pyear ) } 0..6;
This should work:
use POSIX; # for strftime
my $time = time ();
my $seconds = 24*60*60;
my #time = gmtime ();
$time = $time - $time[6] * $seconds;
for my $wday (0..6) {
$time += $seconds;
my #wday = gmtime ($time);
print strftime ("%A %d %B %Y\n", #wday);
}
Gives me:
$ ./week.pl
Monday 24 May 2010
Tuesday 25 May 2010
Wednesday 26 May 2010
Thursday 27 May 2010
Friday 28 May 2010
Saturday 29 May 2010
Sunday 30 May 2010
If you want to get weeks starting on Sunday, change $time[6] to ($time[6] + 1).
This assumes you want the GMT weeks. Change gmtime to localtime to get local time zone weeks.