On 11/15/06, James Edward Gray II <james / grayproductions.net> wrote:
> On Nov 15, 2006, at 12:35 PM, Jamie Macey wrote:
> > That being said, on a lark I did a bit of refactoring and tried to
> > move as much logic out of the ProgramManager into an actual Program
> > class - the results are below.
> >
> > I definitely prefer the second version.  It's got 10 more lines of
> > actual code, but there's less random logic strewn about (and the logic
> > is simpler).  There's a definite separation of responsibilities -
> > Program just does querying, ProgramManager handles priorities.
>
> You just learned what I also learned today, writing the summary.  ;)
>
> James Edward Gray II
>

So it seems I've already missed the summary, but here's my solution.
I took a different approach - I stored the programs to record in a
linked list sorted by start time.   As I was thinking about the
solution. I realized this is the first time I even considered a linked
list since I started using Ruby. For most places I would have used
them in other languages, I can use Arrays in Ruby.  (For a moment I
thought - oh, can't do lists, no pointers - but then I realized that
references worked just fine)

The solution needs serious refactoring.  I have 2 classes where I
should have 4 or 5  to separate the list logic from the VCR logic.
But I don't have any more time this week.

-Adam
----------------
# A Program node.  Contains the program recording information,
# the linked list handle, and a few functions for list maintainance
class Program
  attr_reader :start,:end_t,:channel, :repeat, :nxt
  def initialize start,finish,channel,repeat=false
    @start,@end_t,@channel = start,finish,channel
    @repeat = repeat
  end
  def advance
    @start+= (7*24).hours
    @end_t+= (7*24).hours
    self
  end
  def not_after? time
    @end_t < time ||
     (@end_t == time &&
      (@nxt && @nxt.start <=time))
   end

  def split_and_insert node
    tail = Program.new(node.start,@end_t,@channel,@repeat)
    @end_t = node.start
    insert_after_self node
    node.insert_after_self tail
  end
  def insert_after_self node
    node.set_next @nxt
    @nxt = node
  end
  def set_next node
    @nxt = node
  end
end

class Time
  #returns a Time object for the begining of the day
  def daystart
    arr = self.to_a
    arr[0,3]=[0,0,0]
    Time.local(*arr)
 end
end

def hours_between daynum, daystr
  days = %w{sun mon tue wed thu fri sat}.map{|d|Regexp.new(d)}
  days.each_with_index{|dayname,idx|
    if dayname =~ daystr
      idx+=7 until idx-daynum >= 0
      return (idx-daynum)*24.hours
    end
  }
end

class ProgramManager
  def initialize
    @head = nil
  end

  #adds programs to a linked list sorted by start time
  def add args
    start = args[:start]
    finish = args[:end]
    channel = args[:channel]
    if !daylist = args[:days]
      insert Program.new(start,finish,channel)
      repeat = nil
    else
      t = Time.now   #This is only going to future recurring dates on the list
      #t = Time.local(2006,10,30) #so in order to pass the unit tests,
                                               #force back to Oct 30th
      today = t.wday
      daystart = t.daystart
      daylist.each{|day|
        offset = hours_between(today,day)
        p_start = daystart+offset+start
        p_finish = daystart+offset+finish
        insert Program.new(p_start,p_finish,channel, true)
      }
    end
  end

  #inserts node in list, sorted by time.
  #newer entries are inserted before older entries for the same period.
  #we don't do anything special if the end of the new entry overlaps
  #the beginning of the older one - when the new entry is done, the
old one will be
  #next on the list, so it will become active, wherever it is in it's range.
  #the only tricky case is if a new program starts in the middle of an older one
  #then we have to split the old node, and insert the new one in the split.
  def insert new_node
    prevNode,curNode=nil,@head
    while curNode and curNode.end_t <= new_node.start
      prevNode,curNode=curNode,curNode.nxt
    end
    if curNode and curNode.start < new_node.start
      curNode.split_and_insert new_node
    elsif prevNode
      prevNode.insert_after_self new_node
    else
      new_node.set_next @head
      @head = new_node
    end
  end

  #pops all the nodes that end before the current time.
  #if they are repeating, advance their time one week, and reinsert them.
  def find_current_node time
    return false unless @head
    while @head && @head.not_after?(time)
      old_node = @head
      @head = @head.nxt
      insert old_node.advance if old_node.repeat
    end
     @head if @head && @head.start <= time
  end

  def record? time
    node = find_current_node time
    node && node.channel
  end
end