Phoenix.pm: Processing Dates

Victor Odhner vodhner at cox.net
Tue Feb 24 22:44:25 CST 2004


Was Re: Phoenix.pm: Thursday Meeting Automated Pre-Announcement

Hi, Scott.

This is by no means to demean on your admirable shot
at automatic notification.  You're among friends here,
and it gives us a chance to talk about Perl munging.

I enjoy date manipulation.  I've done *lots* of that
over the past 25 years, and it just isn't all that
tricky in most cases.  So I've taken this occasion
to ramble on a bit with my acquired (ahem) wisdom ...

(Hey, if anyone replies to this bloated message,
please don't quote it all!)

I have used POSIX::mktime, but otherwise the standard
functions in Perl are about all I have ever needed.

As usual when I get to designing, I got a little
carried away here.  The concept below will keep track
of multiple event notifications for multiple mailing
lists, even more than one a day.  Actually making it
work, as the professor would say, is an exercise for
the student ...

Scott Walters wrote:
> I shouldn't be trying to work on the dates directly
> Myself, but I've actually never worked with dates
> before! Amazing, I know.  Atleast nothing more complex
> than grouping by month and recording and logging.

Always convert dates to integers before trying to
do any arithmetic on them.  I say this because I've
been where you were today, and the code gets *very*
complicated before you get it right.  The localtime
function is gold.

Oops.  You are calling gmtime instead of localtime.
In Phoenix, you will be wrong if this runs before 7 a.m.,
or maybe before 8 a.m. during daylight savings.
The purpose of localtime is to take care of all that
for you.  Even if everything else runs on Universal
Time, you can set the time zone for -- or even inside --
this process.

Next problem:  you are doing all your computing with
respect to today's date, but you are not checking the
day of the meeting.  It's ok to make a computation
and trust your results, but it's better to check them.
See below ...

Eden Li suggested this:
   my $thursdayith = (localtime (time() + 6*24*60*60))[3];

The expression is good -- I tend to use 86400 because
I know the number, but 24*60*60 is self-documenting
and takes no time for the Perl compiler to convert to
86400.  Still, it's a mistake to bury the "6" in with the
other numbers.  They won't change, but the 6 will --
count on it.

Programs oversimplify the real world, so it is best to
keep our assumptions out in the open.  Something like this:

   my @daysahead = ( 6, 2 );
     # For all events in %schedule, we will send out
     # messages six days and two days in advance.
     # A more sophisticated design could vary the intervals
     # for individual events ...

   my %schedule = (
     "1:4" = # First week: Thursday
	[ "PerlMongers|7:00 P.M.|Bowne|PM" ],
     "3:2" = # Third week: Tuesday
	[ "PerlMongers|7:00 P.M.|TBD|PM" ],
   );

   my %eventdesc = (
     'PerlMongers' => "Perl Mongers meeting",
   );

   my %location = (

'Bowne' => qq(Bowne, which is located at 1500 N. Central Avenue,
which is on the Southwest corner of Central and McDowell.  The parking
lot is gated, so just press the button on the intercom, and tell the
receptionist that you are there for the Perl meeting.  Park in the lot
that is straight ahead from the entrance on the South side of McDowell.
Park in any uncovered, non-reserved space.  Proceed to the main lobby,
which is on the Northeast side of the parking lot.  ),

'TBD' => "a location to be announced.  ",

   );

   my %addr = (
	'PM' => "phoenix-pm-list at happyfunball.pm.org",
   );

For your purposes, you don't need to ask about the time
of day, so you can slice the localtime results like this:

   for my $interval ( @daysahead )  {
     my $secondsahead = $interval * 24*60*60;

     my ( $mday, $mon, $year, $wday ) =
        ( localtime( $secondsahead ) )[3..6]

     ... now you know that the day in which
     the time $secondsahead falls is in the future by
     one of the intervals declared in @daysahead.

Now when you do each day's run, you can use some sort of
modulo function against $mday to figure out $whichweek that
day is in, 1 thru 5.  Pair that with the day of week to
make up a hash key:

   $schd = $schedule{ "$whichweek:$wday" };

   if ( defined $schd && ref $schd )  {
     # $schd is a list reference.
     for my $event ( @{$schd) )  {
       my ( $what, $when, $where, $addy ) = split( /\|/, $event );
       my $eventtext = $event{$what};
       my $locationtext = $location{$where};
       my $recipients = $addr{$addy};

        ... send message here ...
     }

If that expression comes up non-empty, then there is a meeting
on $mday of $mon + 1.  You can have as many arbitrary
items on your schedule, as long as they are scheduled
by day-number of week-number.  And instead of just setting
these hash entries to 1 (True), you could store them
with any amount of information about the event:
   my %schedule = (
     "1:4" = "PerlMongers",  # First week : Thursday
     "3:4" = "PerlMongers",  # Third week : Thursday
   );

I like that you are suffixing the dates, but I think the
current schedule could result in our having a meeting on
the "12nd".  I'd lean towards something like this:

   my @suffix = qw(
    st nd rd th th th th th th th
    th th th th th th th th th th
    st nd rd th th th th th th th
    st );

Can you tell that I like table driven computing?  That's
from 30+ years of experience:  listen to the old guy.

It would be a good idea to mention the month the meeting
is in.  Matter of taste.

In case anyone is not aware of it, the GNU "date" command
(standard on Linux systems, often available elsewhere)
is extremely powerful.  There may be cases where a
system call to that command would be more efficient
(i.e., saving you hacking time) than working it out
in Perl.  Virtually endless formatting capabilities,
and expressions like "first tuesday after ...".

Just for good measure, I've attached code to convert
a text date into a "time" seconds value.

Take care,

Vic

If you want to convert a text date into a time_t (epoch seconds) value, 
you can use the standard Time::Local or POSIX::mktime.
The following uses the latter, after cleaning up its input.

sub ymd2time_t  {  # Input = year, month, day; Output - time_t.

     # Validates month.  Does windowing for two-digit year.
     my ( $Y, $M, $MD ) = ( @_, 0, 0, 0 );
     my @input = ( $Y, $M, $MD );
     my $Y_input = $Y;
     my $tim = 0;

     for my $v ( $Y, $M, $MD )
     {
         return $v  if $v =~/^.invalid date/;
         return "[invalid date: ymd2time arguments = $Y $M $MD]\n"
             if $v eq 0;
     }

     return "[invalid date: Month = $M]\n"
         if $M < 1 || $M > 12;

     $M -= 1;

     return "[invalid date: Day-of-Month = $MD]\n"
         if $MD < 1 || $MD > 31;

     if ( $Y < 80 )  { # Window 2-digit year, pivot at 1/1/1980.
         $Y += 2000;
     }
     elsif ( $Y <= 99 )  {  # Window 2-digit year
         $Y += 1900;
     }

     $Y -= 1900;  # Now make it relative to 1900.

     $tim = POSIX::mktime( 0, 0, 0, $MD, $M, $Y, 0, 0, 0 );

     if ( !$tim )  { # Check against known limits, ONLY if mktime failed.
         return "[ymd2time_t failed for date = $input]\n";
     }

     return $tim;

}





More information about the Phoenix-pm mailing list