Sample solutions and discussion
Perl Quiz of The Week #22 (20040825)
The purpose of this problem is very simple (and hopefully
something many of us will be able to use).
Inside a directory (say $ENV{HOME}/.upcoming) we have several
files. Here's part of one:
02/26 léon brocard
03/06 michelangelo
05/29 simon cozens
12/28 randal schwartz
02/27 eduardo nuno
03/05 crapulenza tetrazzini
03/16 richard m. stallman
This particular file is appropriately named 'birthdays'. You
can have as many different files as you wish in that
directory.
Here's part of another file, 'events':
01 payday
15 payday
08/13/2004 slides for YAPC::EU::2004
03/01 feast of st. david
03/01/1565 Rio de Janeiro founded
03/09/2004 dentist appointment 10:00
As you can see, both the month and the year are optional. When
not given a month, we'll have three spaces; by the end of the
date we may have as many spaces and/or tabs up to the
description. The 'events' file says that payday occurs on the
1st and 15th of every month, and that the Feast of St. David
occurs each year on the first day of March.
This week's problem consists of writing the script 'upcoming',
which tells us about our upcoming events. Suppose today is 26
February. Then the output will contain:
birthdays
===> 02/26 léon brocard
--> 02/27 eduardo nuno
events
--> 03/01 payday
--> 03/01 Feast of St. David
Explanation:
* For each file, you get a paragraph, if there are upcoming
events mentioned in that file.
* The program will print all the events that will occur in the
next 'n' days, where 'n' is specified with a '-n'
command-line flag. If '-n' is omitted, 'n' will default to
7 days.
* For each event, you get a string that tells you about the
event's proximity:
0 => ' ===>',
1 => ' -->',
2 => ' -->',
3 => ' -->',
4 => '-->',
5 => '->',
6 => '>',
7 => ' ',
If the '-n' switch is given, and the event is in the specified
range, but more then 7 days ahead, then the proximity string
is something like (8) or (13) depending on how many days ahead
the event is. Here we're running the program on 26 February,
as before, but with the option '-n 12':
birthdays
===> 02/26 léon brocard
--> 02/27 eduardo nuno
(8) 03/05 crapulenza tetrazzini
(9) 03/06 Michelangelo
events
--> 03/01 Feast of St. David
--> 03/01 payday
(12) 03/09 dentist appointment 10:00
Note that the founding of Rio de Janeiro did not occur in
either output, since it has already passed.
As you'll notice, it's a little hard to schedule things such
as the fourth Thursday of each month, or dates like Mother's
Day (I don't know about the rest of the world, but that
changes, here in Portugal). It might be a good idea to find a
reasonable way to solve this.
Happy hacking :
----------------------------------------------------------------
This week's problem had four submissions, by Roger West, Zed Lopez
Dave Cash and Mark Dominus. Thanks, guys :-)
[ Code for the four solutions can be found at
http://perl.plover.com/qotw/misc/r022/
- MJD ]
Their four solutions have different ways of solving the problem, which
we'll discuss below.
Though the problem wasn't all that hard, something terrible happened...
it was solved in mid August, and tested by the end of August... and
you'll see in a moment what happened because of that :-)
First, let's talk about input. The problem stated:
> both the month and the year are optional
Here are the four types of dates this would allow for:
09/01/2004 should pass 1
09/01 should pass 2
01/2004 should pass 3
01 should pass 4
As you'll notice, the first date has month, day and year, the second
does not contain the year (optional), the third does not contain the day
(optional) and the last one has only the day (thus not including both
optional parameters).
For a test suite regarding dates, here's what I used:
09/01/2004 should pass 1
09/01 should pass 2
01/2004 should pass 3
01 should pass 4
09/01/2003 should NOT pass a
09/01/2005 should NOT pass b
08/01 should NOT pass c
09/11 should NOT pass d
01/2003 should NOT pass e
01/2005 should NOT pass f
29 should NOT pass g
18 should NOT pass h
You'll notice that all this input is valid. Given that I ran the tests
on August 30th, all of the first four tests should be displayed. As for
the others, none of them should. Tests a,c,e,g contain dates that have
already passed, while tests b,d,f,h all have dates that are not in the 7
days range from today (Aug 30th). Here are the results for these tests:
1 2 3 4 a b c d e f g h
roger _ _ x _ _ _ _ _ _ _ _ _
zed _ _ E x _ _ _ _ E E _ _
dave _ _ _ _ _ _ _ _ _ _ x _
mark _ _ _ _ _ _ _ _ _ _ _ _
_ - the entry "was displayed" for test 1-4 or "wasn't displayed" for a-h
x - the entry "wasn't displayed" for test 1-4 or "was displayed" for a-h
E - fatal error
As you can see, most of the solutions had one problem or another (hey,
dates are tricky...)
[ I also wonder what these programs would have done on December 30 for
dates that occur the following January. I was at some pains to get
this right, but I think it's a subtle point. For example, when your
program sees "09/11" it's tempting to have it assume that the year
defaults to *this* year, but in the case of "01/11" it should
default to *next* year, unless the current date is in early
January. - MJD ]
Let's see what went wrong:
Roger's Solution:
Test 3: Roger's code has five different regexps to get all the possible
cases... here's what they do:
first - catches dates having only the day
second - catches complete dates
third - catches dates without the year
fourth - ??
fifth - ??
I gave up at trying to understand what the last two ones did, but I
would bet it has something to do with the "2nd tuesday of every month"
format... (am I wrong?)
There doesn't seem to be any regexp to catch dates without the month,
but with the year...
Zed's Solution:
Test 3: Module Date::Calc produced an error with this test. To
comprehend the problem, take a look at this regexp:
m|^\s*(\d+)(?:/(\d+))?(?:/(\d*))?\s+(.*)$|
Here's the problem: since no month was provided, $1 (given to $month)
captures the day instead of the month. That would be OK, as later on, if
$day has no value, $day gets the value of $month and $month another
value... the problem is that this regexp puts the day in the month and
the year in the day. Since it has no year, $year gets the value of the
current year, and hence we have day, month and year, but what we really
have for day is the month, and for the month, the year; we have this
date: 2004/01/2004. Date::Calc, as it should, complains about this.
Test 4: It fails because the month, if not available, is replaced with
the current month. Given that I did the test on one of the last days of
August, the date was considered as being 08/01/2004, which had indeed
already passed. 09/01/2004 wasn't considered, but should have been.
Test e: Same thing as test 3.
Test f: Same thing as test 3.
Dave's Solution:
Test g: For some strange reason, Dave's solution regards yesterday's
events as today's ones :-| It still does consider today's events as it
should, though. Since Dave said nothing about this (at least that I
remember of), I don't consider this a feature :-) I had nor time nor
skills to discover the reason for this, but I sure would like to know...
:-)
Now that we've dealt with the input, let's take a quick look at the
output. I tested with this:
08/30/2004 today
08/31/2004 in 1 day
09/01/2004 in 2 days
09/02/2004 in 3 days
09/03/2004 in 4 days
09/04/2004 in 5 days
09/05/2004 in 6 days
09/06/2004 in 7 days
09/07/2004 in 8 days
09/08/2004 in 9 days
09/09/2004 in 10 days
Here's what I got:
Roger:
birthdays
===> today
--> in 1 day
--> in 2 days
--> in 3 days
--> in 4 days
-> in 5 days
> in 6 days
in 7 days
Zed:
birthdays
===> 08/30 today
--> 08/31 in 1 day
--> 09/01 in 2 days
--> 09/02 in 3 days
--> 09/03 in 4 days
-> 09/04 in 5 days
> 09/05 in 6 days
09/06 in 7 days
Dave:
birthdays:
===> 08/30 today
===> 08/31 in 1 day
--> 09/01 in 2 days
--> 09/02 in 3 days
--> 09/03 in 4 days
--> 09/04 in 5 days
-> 09/05 in 6 days
> 09/06 in 7 days
09/07 in 8 days
Mark:
birthdays:
===> 8/30/2004 today
--> 8/31/2004 in 1 day
--> 9/ 1/2004 in 2 days
--> 9/ 2/2004 in 3 days
--> 9/ 3/2004 in 4 days
-> 9/ 4/2004 in 5 days
> 9/ 5/2004 in 6 days
9/ 6/2004 in 7 days
Well... close enough :-) Roger apparently isn't printing dates, while
Zed isn't indenting. Mark decided that a month is a month, and it needs
no stinking zeroes :-)
Did you notice something else funny? Yep, so did I... Dave's solution is
not only allowing for the date of yesterday, but also for one day more
then the specified range... I think those "proximity strings" are not
right, either...
Now that we've taken care of that, here are some other things:
* Recursion: it wasn't asked for, but Zed's solution implements this
quite well, via File::Find.
* -n switch: Every solution dealt with this. No problem.
* "2nd something of every something" format:
Roger tried this. I made a test with this:
1th thursday of every month: something happens
It worked, but gave me a warning... I wrote this and discovered that I
was being stupid :-) Then, I used this:
1st thursday of every month: something happens
It worked. No warnings :-)
* Speed: I had no time to test for speed... Roger started his entry by
stating that Date::Manip is slow, while Dave stated his code would run
pretty fast... that's all I have to say for now...
* Others: Dave implemented some other features: letting the user select
the files he wants to be processed, providing an option for specifying
the date format (which I didn't test much) and another for the directory
to process.
Bottom line:
While Roger demonstrated his ability with the "Swiss Army Chainsaw of
date modules", Date::Manip, making available the "2nd Tuesday of every
month" format, Zed made use of File::Find for immediate recursion and
Dave went with POSIX and implemented a lot of nice features. Mark, on
his side, decided to go with Time::Local and actually made it work for
all tested cases... O:-) I believe an even better script could be made
out of these three ones, as they all have strong points.
Final considerations:
By using File::Find, Zed's solution made it hard for me to test it... It
was trying to parse my test file, but also the swap file created by vim,
given that I was editing the file :-) It took me a while to figure that
out (the time for printing some debugging information including the name
of the file being opened, at least).
Testing your code was very fun, and something I hope to do again in a
near future :-) Thanks for your time, guys :-)
jac
[ My thanks also to anyone who worked on the quiz and didn't send in a
solution.
I have a bit of a problem now. I sent out an 'expert' quiz, the
word ladder one, thinking that I would write up the report about it
myself, since I am on leave this week. I had not expected it to
turn out to be one of the most popular quizzes ever, and now I don't
think I have time to write it up. I would be grateful if someone
else would volunteer to do it. Is anyone interested in looking over
and testing the many submitted solutions for this quiz? If so, drop
me a note at mjd@plover.com. I will be happy to offer asistance and
guidance. Thanks, - MJD ]