When I used the term VCR in my latest book, my editor complained that it
“dates
me.” (Ouch. I’m only 30!) I’m told we all use Tivo to record our
shows now.
I’m not on that bandwagon yet, so the rest of you will need to tell me
if we
need a Tivo Program Manager quiz or if this same code will work.
First, let’s examine a super straight forward solution to the task to
see what’s
involved. When we’ve done that, I’ll make note of a few variations that
caught
my eye.
We will begin with some code by Peter Severin. Here’s a helper method
Peter
added to the Time class:
class Time
def seconds
(hour * 60 + min) * 60 + sec
end
end
This just calculates the number of seconds since midnight. That
measurement is
used by weekly programs in the quiz, to specify when to record on a
given day.
Let’s take our first steps into the Program objects now:
class Program
attr_reader :channel
def initialize(program_details)
@program_start = program_details[:start]
@program_end = program_details[:end]
@channel = program_details[:channel]
end
end
I think Peter has a very clean and correct OO design in these Program
classes.
Here we see the base class handling only the initialization that applies
to all
subclasses. Also note that only channel() is exposed to the outside
world,
since that’s all the ProgramManager really needs.
Here’s the subclass for one-shot recording:
class SpecificProgram < Program
def record?(time)
time.between?(@program_start, @program_end)
end
end
All Peter adds here is the ability for the Program to determine if it is
scheduled for the passed time. This keeps the Program logic in the
Program
classes where it belongs.
The other subclass is for repeat programming:
class RepeatingProgram < Program
WEEKDAYS = %w(mon tue wed thu fri sat sun)
def initialize(program_details)
super
@days = program_details[:days].map {|day| WEEKDAYS.index(day) + 1}
end
def record?(time)
@days.include?(time.wday) &&
time.seconds.between?(@program_start, @program_end)
end
end
Here initialization is modified to handle the :days parameter that only
applies
to this type of Program. From there, another record?() method is
created to
examine both the day and time.
I don’t see as much of this traditional OO design in solutions to the
quizzes,
but here I found it quite elegant. With each Program able to answer the
right
questions about itself, the ProgramManager is almost trivial to
construct:
class ProgramManager
def initialize()
@programs = []
end
def add(program_details)
case program_details[:start]
when Numeric
@programs << RepeatingProgram.new(program_details)
when Time
@programs[0, 0] = SpecificProgram.new(program_details)
end
self
end
def record?(time)
program = @programs.find {|program| program.record?(time)}
program ? program.channel : nil
end
end
Peter begins by constructing an Array to hold the @programs in
initialize().
Conflict management was pretty easy for this quiz in that the last
defined
Program wins out. You can deal with that by keeping them all in one
list and
making sure you order them correctly at insertion time.
The insertions are handled by the add() method. It determines the type
of
Program object to create and adds it to our list of @programs. The
second
addition is the tricky one, if you’re not familiar with how Array.[]=()
works.
Let’s see what that does in IRb:
>> programs = Array.new
=> []
>> programs << "repeating 1" << "repeating 2"
=> ["repeating 1", "repeating 2"]
>> programs[0, 0] = "specific 1"
=> "specific 1"
>> programs
=> ["specific 1", "repeating 1", "repeating 2"]
Put another way, the assignment to index zero, length zero adds the
element to
the front of the Array. It’s equivalent to the more common:
@programs.unshift(SpecificProgram.new(program_details))
The final method of Peter’s ProgramManager, record?(), just forwards the
record?() calls to the Array of Program objects via the find() iterator.
The
first one to claim the time is selected and that Program’s channel() is
returned.
There was an interesting element in Dema’s Program class that’s probably
worth a
quick look:
class Program
# ...
def initialize(program)
@start = program[:start]
@end = program[:end]
@channel = program[:channel]
@days = program[:days]
raise "Missing start or end" \
if @start.nil? || @end.nil?
raise "Wrong start or end types" \
unless (@start.is_a?(Time) && @end.is_a?(Time)) ||
(@start.is_a?(Integer) && @end.is_a?(Integer))
raise "Invalid program" \
if weekly? && (@start.is_a?(Time) || @end.is_a?(Time))
raise "End must come after Start" \
if !weekly? && @start > @end
raise "Missing channel" \
if [email protected]_a?(Integer)
raise "Invalid weekday" \
if @days.is_a?(Array) && @days.any? { |day| WEEKDAYS[day] == nil
}
end
# ...
end
The only difference here is that Dema does a fair amount of error
checking when
Programs are constructed. Ordinarily, I’m not a big fan of this kind of
lock-down coding, but this seems like a good case where it might just be
worth
the effort. Programs are going to come from users and of course users
are going
to make mistakes. Isolating these at Program construction time would
allow the
machine to respond to those errors intelligently at the time when it
matters,
instead of running into trouble down the road and recording at the wrong
times.
The focus of this quiz was not to implement a complete VCR, of course,
and most
solvers just assumed they would receive correct input. I just thought
it worth
mentioning that the safeguards employed by Dema do need to be in place
somewhere
in the system.
I won’t show the code here, but as a final point of interest I want to
recommend
everyone take a peek at Gordon T.'s solution. It uses a library
called
Runt to handle the majority of the scheduling. I wasn’t aware of this
resource
and Gordon’s code got me to look into it.
Runt is Temporal Expression library designed with things like recurring
events
and scheduling in mind. The documentation is pretty good and the
project worth
a look:
http://runt.rubyforge.org/
My thanks to all the solvers who always manage to teach me things, even
when we
do these simple problems. I love that aspect of the quiz.
Tomorrow we will take a shot at bringing Literate Programming to our
fair
Ruby…