dgaramond2 has asked for the wisdom of the Perl Monks concerning the following question:

Having spent some time yesterday creating a simple web application which has some date/time stuffs in it, I realized how horrible the Unix API (and thus, PHP's API) and MySQL's date/time handling are. Conversion between month that starts from 0 and 1, year that needs to be substracted/added with 1900, these certainly have bitten (and will continue to bite) people's behind on a regular basis.

Luckily Perl 5.10 will include Time::Piece which will make localtime/gmtime become saner and more straightforward. It even includes many bonuses like leap year- and DST checking, strftime(), simple date calculation, etc.

Unfortunately we can't modify date/time elements in a Time::Piece object, e.g. $t->mon(1) (setting the month to January).

Now on to the quiz question: what would be the simplest and most straightforward way in Perl to, given a Unix timestamp t, returns two Unix timestamps t1 and t2 that are the start and end of day (or week, or month, or year) where t1 <= t <= t2.

  • Comment on Date calculation: start and end of period

Replies are listed 'Best First'.
Re: Date calculation: start and end of period
by gamache (Friar) on Nov 03, 2007 at 12:57 UTC
    The Time::Local core module, along with localtime, can get you day, month and year periods pretty easily:
    #!/usr/bin/perl use strict; use warnings; use Time::Local; my $t = time; ## or, plug in your own timestamp my ($day_start, $day_end) = day_period ($t); my ($month_start, $month_end) = month_period ($t); my ($year_start, $year_end) = year_period ($t); print "The day starts at ", scalar localtime $day_start, "\n"; print "The day ends at ", scalar localtime $day_end, "\n"; print "The month starts at ", scalar localtime $month_start, "\n"; print "The month ends at ", scalar localtime $month_end, "\n"; print "The year starts at ", scalar localtime $year_start, "\n"; print "The year ends at ", scalar localtime $year_end, "\n"; sub day_period { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localti +me shift; my $t1 = timelocal (0, 0, 0, $mday, $mon, $year); my $t2 = timelocal (0, 0, 0, $mday+1, $mon, $year); wantarray ? ($t1, $t2) : \($t1, $t2) } sub month_period { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localti +me shift; my $t1 = timelocal (0, 0, 0, 1, $mon, $year); my $t2 = timelocal (0, 0, 0, 1, $mon+1, $year); wantarray ? ($t1, $t2) : \($t1, $t2) } sub year_period { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localti +me shift; my $t1 = timelocal (0, 0, 0, 1, 0, $year ); my $t2 = timelocal (0, 0, 0, 1, 0, $year+1); wantarray ? ($t1, $t2) : \($t1, $t2) }
    Output:
    The day starts at Sat Nov 3 00:00:00 2007 The day ends at Sun Nov 4 00:00:00 2007 The month starts at Thu Nov 1 00:00:00 2007 The month ends at Sat Dec 1 00:00:00 2007 The year starts at Mon Jan 1 00:00:00 2007 The year ends at Tue Jan 1 00:00:00 2008
    If you need week-long periods, you can either use a heavier, non-core module like Date::Manip, or write the logic yourself (hope you like special cases!).

    Update: And if you want the periods to end just at the end of the day/month/year rather than at the beginning of the next one... subtract 1 from $XXXX_end.

Re: Date calculation: start and end of period
by johngg (Canon) on Nov 04, 2007 at 10:53 UTC
    Here is a way that doesn't use Time::Local but just relies on there being 86,400 seconds in your average day. I decided that each end period should be at 23:59:59 on the last day of the period rather than at 00:00:00 of the first day of the next.

    use strict; use warnings; use POSIX q{strftime}; my @daysInM = ( [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ], [ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ], ); # ----- my $rcDay = sub # ----- { my $ts = shift; my $tsStartDay = int( $ts / 86400 ) * 86400; my $tsEndDay = $tsStartDay + 86399; return ( $tsStartDay, $tsEndDay ); }; # ------ my $rcWeek = sub # ------ { my $ts = shift; my ( $t1, $t2 ) = $rcDay->( $ts ); my $wday = ( localtime( $ts ) )[ 6 ]; $t1 -= 86400 * $wday; $t2 += 86400 * ( 6 - $wday ); return ( $t1, $t2 ); }; # ------- my $rcMonth = sub # ------- { my $ts = shift; my ( $t1, $t2 ) = $rcDay->( $ts ); my ( $mday, $mon, $year ) = ( localtime( $ts ) )[ 3 .. 5 ]; $t1 -= 86400 * ( $mday - 1 ); $t2 += 86400 * ( $daysInM[ isLeap( $year + 1900 ) ]->[ $mon ] - $mday ); return ( $t1, $t2 ); }; # ------ my $rcYear = sub # ------ { my $ts = shift; my ( $t1, $t2 ) = $rcDay->( $ts ); my ( $year, $yday ) = ( localtime( $ts ) )[ 5, 7 ]; $t1 -= 86400 * ( $yday ); $t2 += 86400 * ( ( 365, 366 )[ isLeap( $year + 1900 ) ] - $yday - 1 ); return ( $t1, $t2 ); }; my %periodDispatch = ( day => $rcDay, week => $rcWeek, month => $rcMonth, year => $rcYear, ); my $rxValidPeriod = do { local $" = q{|}; qr{(?xi) ^ (?: @{ [ keys %periodDispatch ] } ) $ }; }; my $timeNow = time(); foreach my $t ( $timeNow, $timeNow + 8640000, $timeNow - 20000000 ) { print q{=} x 48, qq{\n}, strftime( qq{\$t - $t - %a %Y-%m-%d.%H:%M:%S %z\n}, localtime( $t ) ), ; foreach my $period ( qw{ day week month year } ) { my ( $t1, $t2 ) = tsPeriod( $t, $period ); print q{-} x 48, qq{\n}, qq{$period:\n}, strftime( qq{\$t1 - $t1 - %a %Y-%m-%d.%H:%M:%S %z\n}, localtime( $t1 ) ), strftime( qq{\$t2 - $t2 - %a %Y-%m-%d.%H:%M:%S %z\n}, localtime( $t2 ) ), ; } } print q{=} x 48, qq{\n}; # -------- sub tsPeriod # -------- { my ( $ts, $period ) = @_; die qq{Timestamp not a +ve integer\n} unless $ts =~ m{^\+?\d+$}; die qq{Period "$period" not recognised\n} unless $period =~ $rxValidPeriod; $period = lc $period; return $periodDispatch{ $period }->( $ts ) ; } # ------ sub isLeap # ------ { my $year = shift; return 0 if $year % 4; return 1 if $year < 1753; return 1 if $year % 100; return 1 unless $year % 400; return 0; }

    Here is the output.

    I hope this is of interest.

    Cheers,

    JohnGG

Re: Date calculation: start and end of period
by Krambambuli (Curate) on Nov 04, 2007 at 19:49 UTC
    With Date::Manip, if that is accepted, something like the following code might be considered simple and straightforward (read: most of the inherent underlying difficulties remain hidden inside Date::Manip)):
    use warnings; use strict; use Date::Manip; my $t = time(); my $date = ParseDateString( "epoch $t" ); my $day_start = Date_SetTime( $date, 0, 0, 0); my $day_end = Date_SetTime( $date, 23, 59, 59); print "\nDay: ", start_end( $day_start, $day_end ); my $err; my ($year, $month, $day, $day_of_week) = UnixDate( $date, '%Y','%m', '%d', '%w'); my $week_start = $day_of_week == 1 ? $date : DateCalc( $date, 'last Monday', \ +$err) ; $week_start = Date_SetTime( $week_start, 0, 0, 0); my $week_end = $day_of_week == 7 ? $date : DateCalc( $date, 'next Sunday', \$e +rr) ; $week_end = Date_SetTime( $week_end, 23, 59, 59); print "\nWeek: ", start_end( $week_start, $week_end ); my $month_start = Date_SetDateField( $date, 'D', 1 ); $month_start = Date_SetTime( $month_start, 0, 0, 0); my $next_month = DateCalc( $date, 'next month' ); my $month_end_epoch = UnixDate( $next_month, '%s' ) - 1; my $month_end = ParseDateString( "epoch $month_end_epoch" ); $month_end = Date_SetTime( $month_end, 23, 59, 59); print "\nMonth: ", start_end( $month_start, $month_end ); my $year_start = Date_SetDateField( $date, 'M', 1); # January $year_start = Date_SetDateField( $year_start, 'D', 1 ); # 1 $year_start = Date_SetTime( $year_start, 0, 0, 0); my $year_end = Date_SetDateField( $date, 'M', 12); # December $year_end = Date_SetDateField( $year_end, 'D', 31 ); # 31 $year_end = Date_SetTime( $year_end, 23, 59, 59 ); print "\nYear: ", start_end( $year_start, $year_end ); exit; sub start_end { my ($date_start, $date_end) = @_; return join ( '', 'starts at ', UnixDate( $date_start, '%O'), ', ends at ', UnixDate( $date_end, '%O'), "\n\t(", UnixDate( $date_start, '%s'), ' - ', UnixDate( $date_end, '%s'), ")\n", ); }

    which produces output like
    Day: starts at 2007-11-04T00:00:00, ends at 2007-11-04T23:59:59 (1194127200 - 1194213599) Week: starts at 2007-10-29T00:00:00, ends at 2007-11-04T23:59:59 (1193608800 - 1194213599) Month: starts at 2007-11-01T00:00:00, ends at 2007-11-30T23:59:59 (1193868000 - 1193954399) Year: starts at 2007-01-01T00:00:00, ends at 2007-12-31T23:59:59 (1167602400 - 1199138399)
    A slight simplification is done anyway: timezone of the starting epoch is considered to be identical to the one in which the results are given.

    Hope that helps.

    Krambambuli
    ---
    enjoying Mark Jason Dominus' Higher-Order Perl