#4 Date Reminder

The commercial calendar programs out there, such as Microsoft Outlook, do a good job of reminding you of your wife's birthday, on her birthday, when it's much too late to get her a present. What's really needed is a program that reminds you when an important date is approaching.

It would also be nice if the program could also tell you how many days have elapsed since an important event, such as, for example, how many days since you sent out a rebate form.

The Code

  1 #
  2 # Usage: remind.pl [<calendar-file>]
  3 #
  4 # File format:
  5 #       date<tab>delta<tab>Event
  6 #
  7 #       Date -- a date
  8 #       delta --
  9 #               -xxx -- Remind after the event for xxx days
 10 #               +xxx -- Remind before the event for xxx days
 11 use strict;
 12 use warnings;
 13 use Time::ParseDate;
 14 use Date::Calc(qw(Delta_Days));
 15
 16 #############################################################
 17 # time_toYMD($time) -- Convert unit time into a year, month
 18 #       and day.  Returns an array containing these three
 19 #       values
 20 #############################################################
 21 sub time_to_YMD($)
 22 {
 23     my $time = shift;   # Time to convert
 24
 25     my @local = localtime($time);
 26     return ($local[5]+1900, $local[4]+1, $local[3]);
 27 }
 28 #------------------------------------------------------------
 29 #
 30 my $in_file = $ENV{'HOME'}."/calendar";
 31
 32 if ($#ARGV == 0) {
 33     $in_file = $ARGV[0];
 34 }
 35 if ($#ARGV > 0) {
 36     print STDERR "Usage: $0 [calendar-file]
";
 37 }
 38
 39 open IN_FILE, "<$in_file" or
 40    die("Unable to open $in_file for reading");
 41
 42 # Today's date as days since 1970
 43 my @today_YMD = time_to_YMD(time());
 44
 45 while (<IN_FILE>) {
 46     # Lines that begin with "#" are comments
 47     if ($_ =~ /^s+#/) {
 48         next;
 49     }
 50     # Blank lines don't count
 51     if ($_ =~ /^s*$/) {
 52         next;
 53     }
 54     # The data on the line
 55     my @data = split /	+/, $_, 3;
 56     if ($#data != 2) {
 57         next;   # Silently ignore bad lines
 58     }
 59     my $date = parsedate($data[0]);
 60     if (not defined($date)) {
 61         print STDERR "Can't understand date $data[0]
";
 62         next;
 63     }
 64     my @file_YMD= time_to_YMD($date);
 65     # Difference between now and the date specified
 66     my $diff = Delta_Days(@today_YMD, @file_YMD);
 67     if ($data[1] > 0) {
 68         if (($diff >= 0) && ($diff < $data[1])) {
 69             print "$diff $data[2]";
 70         }
 71     } else {
 72         if (($diff < 0) && ($diff < -($data[1]))) {
 73             print "$diff $data[2]";
 74         }
 75     }
 76 }

Running the Script

The script uses an input file containing a date, and a number of days. If the number of days is positive, you will be reminded of the event before it happens. (Wife's birthday in 30 days, get present now!) If the number is negative, you will be informed of the number of days which have passed since the event occurred. (They said the rebate would come in 6 to 8 weeks. It's been 80 days, what's going on?) Here's an example:

Oct 14  -100    Rebate Seagate $10
Sept 12 -100    Rebate Costco $50
Nov 1   +30     Wife's birthday

The Results

$ remind.pl events.txt
-3 Rebate Seagate $10
-5 Rebate Costco $50
14 Wife's birthday

This indicates that it's been only three days since I sent out my Seagate rebate form and five since the Costco rebate form was sent. Nothing to worry about there.

It's also two weeks until my wife's birthday, so I'd better start shopping as soon as I finish this chapter.

How It Works

For hours, minutes, and seconds you use a hexasegimal (base 60) system that comes from the ancient Babylonians. But then you suddenly shift to base 24 for the hours in a day (or base 12 and base 2 if you wish to use am and pm).

But things really fall apart when it comes to the number of days in a month. You see, the Romans, specifically Julius Caesar, gave us our base for the modern calendar. This good work was negated by the fact that the Romans decided to name some of the months after politicians. Thus July is actually named in honor of Julius Caesar.

The problem is that Augustus Caesar decided that his month, August, had to be at least as grand as July and decided that his month also had to have 31 days. So he stole an extra day from February. (February was named after a feast, Februa, so it was safe to steal days from this month.) As a result of politics, we have the mess that is the modern day calendar.

And we haven't even touched on some of the other problems, such as the fact that the days from September 3 to September 13, 1752 are missing entirely. That's when the switch from the Julian to the Gregorian calendar was made. Because the Julian calendar was so far off at that time, they had to remove 11 days from it to catch up.

The good news is that as far as Perl is concerned, all this calendar insanity is mostly hidden from you by some Perl modules. The Time::ParseDate module is designed to convert time/data specifications into something usable by a program.

This script needs to know the number of days between two dates. The Date::Calc module can calculate date differences for us. There's just one problem. Time::ParseDate returns the date/time in Unix standard format (number of sections since January 1, 1970) and Date::Calc wants things in Year, Month, Day.

Fortunately, the built-in function localtime splits Unix time into its component fields. So if you combine the three fields and do a little bookkeeping, you can perform your calculations.

You start by reading in a line from a calendar file and parsing it:

 45 while (<IN_FILE>) {
 46     # Lines that begin with "#" are comments
 47     if ($_ =~ /^s+#/) {
 48         next;
 49     }
 50     # Blank lines don't count
 51     if ($_ =~ /^s*$/) {
 52         next;
 53     }
 54     # The data on the line
 55     my @data = split /	+/, $_, 3;
 56     if ($#data != 2) {
 57         next;   # Silently ignore bad lines
 58     }
 59     my $date = parsedate($data[0]);
 60     if (not defined($date)) {
 61         print STDERR "Can't understand date $data[0]
";
 62         next;
 63     }

The parsedate function returns the date in Unix format and the date calculation module needs it as Year, Month, Day. So you convert it:

 64     my @file_YMD= time_to_YMD($date);

Now you can compute the difference between the date in the file and the current date:

 65     # Difference between now and the date specified
 66     my $diff = Delta_Days(@today_YMD, @file_YMD);

If you want to be reminded about an upcoming event, and the event is in range, it's printed:

 67     if ($data[1] > 0) {
 68         if (($diff >= 0) && ($diff < $data[1])) {
 69             print "$diff $data[2]";
 70         }

Otherwise, you want to be reminded about a past event. So if the event is in range, it's printed:

 71     } else {
 72         if (($diff < 0) && ($diff < -($data[1]))) {
 73             print "$diff $data[2]";
 74         }
 75     }
 76 }

Hacking the Script

The core of this script utilizes logic that lets you count up or down days to specified dates. The script can easily be adapted for other counting tasks. For example, you may wish to count down the number of days until a deadline or display the number of days your favorite politician has left in office.

Computers are good at counting, and Perl's modules are good at hiding the complexities of time and dates. Thus it's easy to put the two together to perform any time-based calculations you require.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset