Sample solutions and discussion
Perl Quiz of The Week #3 (20021030)
Write a program, 'spoilers-ok'. It will read the
quiz-of-the-week email message from the standard input,
extract the date that the message was sent, and print a
message that says
It is okay to send spoilers for this quiz
or
It is too soon to send spoilers for this quiz.
You may send spoilers in another 4 hours 37 minutes.
It becomes okay to send spoilers after 60 hours have elapsed
from the time the quiz was sent.
You can be sure that the 'Date' field of the QOTW email
message will always be in the same format, which is dictated
by internet mail format standards. For those unfamiliar with
this format:
Date: Wed, 23 Oct 2002 16:10:15 -0400
The "16:10:15" is the time of day. "-0400" means that the
time of day is in a time zone that is 4 hours behind
Greenwich.
Effective use of modules turned out to be the key to the best
solutions to this quiz. There are three key items:
extract the date field from the mail header
parse the date
format the output
When I went to write up a sample solution last week, I knew there must
be a module for parsing mail headers, but I just couldn't find it. I
spent some time looking for it, and then lost patience. It turned out
to be Mail::Header.
This short and utterly straightforward solution was provided by Craig
Sanders:
#! /usr/bin/perl -w
use Mail::Header;
use Date::Parse;
use strict;
my $head = new Mail::Header [<>], Modify => 0;
my $date = $head->get("Date");
my $message_time = str2time($date);
my $ok_time = $message_time + 3600 * 60;
my $now = time();
if ($now >= $ok_time) {
print "It is okay to send spoilers for this quiz\n" ;
} else {
my $diff = $ok_time - $now ;
my $hours = int($diff / 3600);
my $minutes = int(($diff - $hours * 3600) / 60);
print "It is too soon to send spoilers for this quiz.\n" ;
print "You may send spoilers in another $hours hours $minutes minutes.\n" ;
}
Some people (including me) wrote more than twice as much code to
accomplish the same thing.
1. People used a variety of date-parsing modules. In addition to
Date::Parse, people also used Date::Manip, Time::Local, and
HTTP::Date. But if you use Time::Local, you must extract and
combine the parts of the date yourself; then there is a possibility
to make a mistake. One of the submitted solutions that used
Time::Local made an error in the time zone handling:
$release_time += ((-$3 * 36) + 216000); # timezone, 60 hr.delay
The (-$3 * 36) is the time zone adjustment here; $3 contains the
time zone part of the date field. This adjustment works for most
time zones, but not all. For example, had the quiz-of-the-week
been sent from India, where the time zone is +0530 (five hours,
thirty minutes) the calculated adjustment would have been 19080
seconds, instead of 19800. This is probably an argument in favor
of the modules.
2. Craig's solution has a minor defect: at times, it will generate
outputs like
You may send spoilers in another 1 hours 1 minutes.
This is bad English. One easy way to take care of it:
my $Hours = $hours == 1 ? 'hour' : 'hours';
my $Minutes = $minutes == 1 ? 'minute : 'minutes;
print "You may send spoilers in another $hours $Hours $minutes $Minutes.\n" ;
Seth Blumberg used Lingua::EN::Inflect to handle this.
3. Another possible defect in Craig's solution is that if $diff is
7379 seconds, the output is "... 2 hours 2 minutes"; but really
it's 2 hours, 2 minutes, and 59 seconds. There was a brief
discussion of how to round off times; Kevin Pfeiffer observed:
For dividing seconds into hours and minutes, I believe that a
normal rounding operation is wrong. If you have 1.7 hrs, you
don't want to round up, but rather take the 1 and leave the
remainder to convert to minutes.
To handle this in Craig's code, you could use:
my $hours = int($diff / 3600);
my $minutes = int(($diff - $hours * 3600) / 60 + .5);
(The + .5 is the only new thing here.)
Iain Truskett used Time::Duration to format the output, which takes
care of the plural and the rounding issues. It doesn't produce the
specified output format; it might say "1 day 17 hours" instead of
"41 hours 17 minutes". Whether this is a bug or a feature is up to
you.
4. The solution I wrote up beforehand seems to me to be clearly
inferior to Craig's; it's longer and more complicated because it
does everything manually:
#!/usr/bin/perl
use Time::Local 'timegm';
my $date_field;
while (<>) {
chomp;
last unless /\S/;
if (s/^Date:\s+//) {
$date_field = $_;
while (<>) { # read continuation lines?
last unless s/^\s//;
chomp;
$date_field .= $_;
}
last;
}
}
die "No Date: field found\n" unless defined $date_field;
# Typical value:
# Wed, 30 Oct 2002 21:34:54 -0000
my ($dy, $mo, $yr, $hr, $mn, $sc, $tzd, $tzh, $tzm) =
$date_field =~
/\w\w\w,\ # Day of week
([\d\s]\d)\ (\w\w\w)\ (\d\d\d\d)\ # Day, month, year
(\d\d):(\d\d):(\d\d)\ # Time
([+-])(\d\d)(\d\d)/x; # Time zone
unless (defined $dy) {
die "Couldn't parse Date: field\n";
}
my %mo = qw(jan 0 feb 1 mar 2 apr 3 may 4 jun 5
jul 6 aug 7 sep 8 oct 9 nov 10 dec 11);
die "Unknown month name '$mo'\n" unless exists $mo{lc $mo};
my $msgtime = timegm($sc, $mn, $hr, $dy, $mo{lc $mo}, $yr-1900);
my $tz_adjust = ($tzm * 60 + $tzh * 3600);
$tz_adjust *= -1 if $tzd eq '+';
$msgtime += $tz_adjust; # msgtime is now adjusted for time zone
my $time_left = ($msgtime + 60 * 3600) - time();
if ($time_left < 0) {
print "It is okay to send spoilers for this quiz\n";
} else {
print "It is too soon to send spoilers for this quiz.\n";
my $hr = int($time_left / 3600);
my $min = int(($time_left - 3600*$hr)/60 + 0.5);
my $hours = ($hr == 1 ? 'hour' : 'hours');
my $minutes = ($min == 1 ? 'minute' : 'minutes');
print "You may send spoilers in another $hr $hours $min $minutes.\n";
}
And sure enough, it did have a bug: In the date-parsing regex, I
originally wrote (\d\d) to match the day of the month, instead of
([\d\s]\d); as a result, any message sent in the first 9 days of
any month would fail to match, and the program would die.
Thanks again for your interest. I will send another quiz tomorrow;
it will not contain any date arithmetic.