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 ]