------_extPart_001_01C5A413.D0D27634
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset="iso-8859-1"

I thought I would try my hand at a ruby program.  I am a C++/perl programmer, but have really been impressed with ruby.  I picked the latest ruby quizthe scheduling problem).  It was challenging, but I have a solution.  I thought I would share my solution in the hopes that people would comment on things like style and alternative ways to do things in a more ruby-ish fashion.
 
Before I begin I would like to say that there were two things that bit me over and over again.  The first was that !0 == false.  This is just unintuitive to me as a C++/Perl programmer and was very frustrating.  The second was the need to clone to avoid references to the same object.  Once again, this was unintuitive to me.  However, I don't see those hurdles as being too big.
 
Ok, here is my solution.
 
#First, I define a day class.  This class has only singleton objects for sunday through saturday.  It also defines an #order amongst days.
 
class Day
   def initialize( name, idx )
     @name = name
     @idx = idx
   end
   def to_s
     @name
   end
   def to_i
     @idx
   end
   def <=>( other )
     return self.to_i <=> other.to_i
   end
   @@days = [ new("Sunday", 0), new( "Monday", 1),
                      new("Tuesday", 2), new("Wednesday", 3),
                      new("Thursday", 4), new("Friday", 5),
                      new("Saturday", 6) ]
   def self.sunday
     @@days[0]
   end
   def self.monday
     @@days[1]
   end
   def self.tuesday
     @@days[2]
   end
   def self.wednesday
     @@days[3]
   end
   def self.thursday
     @@days[4]
   end
   def self.friday
     @@days[5]
   end
   def self.saturday
     @@days[6]
   end
   private_class_method :new
   include Comparable
end
 
# Next I define some useful constants
 
WEEKDAYS = [Day.monday, Day.tuesday, Day.wednesday, Day.thursday, Day.friday]
WEEKEND = [Day.sunday, Day.saturday]
MWF = [Day.monday, Day.wednesday, Day.friday]
TR = [Day.tuesday, Day.thursday]

# Now, I define a class to do work hours.  I use military time so I don't have to deal with AM/PM problems
class WorkHours
   include Comparable
   def initialize( days, start_time, stop_time )
     @days = days.sort { |a,b| a.to_i <=> b.to_i }
     @start_time = start_time
     @stop_time = stop_time
   end
   def include?( hour )
     hour.start_time >= @start_time and
     hour.stop_time <= @stop_time and
     (hour.days & @days).length == hour.days.length
   end
   def to_s
     "[" + @days.map { |val| val.to_s }.join(',') + "]" +
     format_time(start_time) + "-" + format_time(stop_time)
   end
   def <=>( other )
      for arr in @days.zip(other.days)
         return -1 if !arr[0]
         return 1 if !arr[1]
         val = arr[0] <=> arr[1] ||
                 @start_time <=> other.start_time ||
                 @stop_time <=> other.stop_time
         return val unless val.zero?
      end
      return 0
   end
   attr_reader :days, :start_time, :stop_time
   private
   def format_time( time )
     hour = time / 100
     minutes = time % 100
     sprintf( "%0.2d", hour ) + ":" + sprintf( "%0.2d", minutes )
   end
end

#Now a class to represent the worker
INVALID_SCORE = -1000000
class Worker
   @@default_scores = { :preferred_scalar => 3,
                                    :non_preferred_scalar => 0,
                                    :exact_time_scalar => 1,
                                    :am_pm_scalar => 0,
                                    :time_change_scalar => -1 }
   def initialize( preferred_hours, impossible_hours, scores = {} )
      @preferred_hours = preferred_hours
      @impossible_hours = impossible_hours
      @assigned = []
      @scores = @@default_scores.merge( scores )
   end
   def preferred?( hours )
      @preferred_hours.include?( hours )
   end
   def impossible?( hours )
      @impossible_hours.include?( hours )
   end
   def available?( hours )
      !impossible?( hours ) and !assigned?( hours )
   end
   def assign( hours )
      hours = [hours] unless hours.class == Array
      for hour in hours
         raise "Already working!" unless available?( hour )
      end
      @assigned.concat( hours )
   end
   def assigned?( hours )
      !(@assigned.find { |obj| obj.include?( hours ) }).nil?
   end
   def clear_assigned_hours
      @assigned = []
   end
   def clone
      obj = Worker.new( @preferred_hours, @impossible_hours, @scores )
      obj.assigned = @assigned.clone
      obj
   end
   # a worker's happiness is dependent on whether he is working his preferred hours, or similar hours across all days, etc...   The scores are configurable from the constructor
   def happiness
      preferred_hours = (@assigned.select { |obj| @preferred_hours.include?(obj) })
      total_count = @assigned.inject(0) { |memo, obj| memo + obj.days.length }
      preferred_count = preferred_hours.inject(0) { |memo, obj| memo + obj.days.length }
      preferred_score = preferred_count * @scores[:preferred_scalar]
      non_preferred_score = (total_count - preferred_count) * @scores[:non_preferred_scalar]
      start_time_hash = Hash.new(0)
      @assigned.each do |obj|
          start_time_hash[obj.start_time] += obj.days.length
      end
      exact = 0
      am_count = 0
      pm_count = 0
      start_time_hash.each_pair do | key, val |
         if( val > 1 )
            exact += val
         else
            key < 1200 ? am_count += val : pm_count += val
         end
      end
      preferred_score + non_preferred_score +
         (exact * @scores[:exact_time_scalar]) +
         ((start_time_hash.length - 1) * @scores[:time_change_scalar]) +
         ((am_count + pm_count) * @scores[:am_pm_scalar])   
    end
    attr_reader :preferred_hours, :impossible_hours
    attr_accessor :assigned
    protected :assigned=
end

#Finally a manager class to schedule the workers
class Manager
   def initialize( workers )
      @workers = workers
   end
   # run through all legal scheduling permutations and remember the maximumcore...yes, this is inefficient!
   def schedule_helper( hours, debug = false )
      return [calc_happiness, all_assigned] if hours.length == 0
      max_score = INVALID_SCORE
      max_assigned = nil
      for hour in hours
         for worker in @workers.find_all { |obj| obj.available?( hour ) }
            assign_hour_transaction(worker, hour) do
               (score,assigned) = schedule_helper(remove_hour(hours, hour), debug)
               (max_score, max_assigned) = [score, assigned] if score > max_score
            end
         end
      end
      [max_score, max_assigned]
   end
   def schedule( hours, debug = false )
      (score, assigned) = schedule_helper( hours, debug )
      if( score == INVALID_SCORE )
         raise "Unable to schedule work hours."
      end
      @workers.zip( assigned ) do |worker, hours|
         worker.clear_assigned_hours
         worker.assign( hours )
      end
      true
   end
   def print_schedule
      idx = 0
      for w in @workers
         print "#{idx}:\n"
         print w.assigned.join("\n")
      end
   end
   attr_reader :workers
   private
   def calc_happiness
      return @workers.inject(0) { |memo, val| memo + val.happiness }
   end
   def all_assigned
      @workers.collect { |obj| obj.assigned.clone() }
   end
   def debug_print( ary )
      @workers.each_index do | idx |
         print "worker[#{idx}] is assigned\n"
         for hour in ary
            print "\t #{hour[0].to_s}\n"
         end
      end
   end
   def remove_hour( hours, hour )
      temp =hours.clone
      temp.delete_at( temp.index(hour) )
      temp
   end
   def assign_hour_transaction( worker, hour )
      saved_assign = worker.assigned.clone
      worker.assign(hour)
      yield
      worker.clear_assigned_hours
      worker.assign(saved_assign)
   end
end
 
# Here are some test cases
class TestManager < Test::Unit::TestCase
 def setup
  @ph1 = WorkHours.new( WEEKDAYS, 800, 1800 )
  @ih1 = WorkHours.new( WEEKEND, 0, 2399 )
  @nph1 = WorkHours.new( MWF, 200, 500 )
  @nph2 = WorkHours.new( TR, 200, 500 )
  @worker = Worker.new( @ph1, @ih1 )
  @worker2 = Worker.new( @ih1, @ph1 )
  @worker3 = Worker.new( [], [] )
  @manager = Manager.new( [@worker] )
  @manager2 = Manager.new( [@worker, @worker2] )
  @manager3 = Manager.new( [@worker3, @worker, @worker2] )
 end
 def testPreferredSchedule
  @manager.schedule( [@ph1] )
  assert( @worker.assigned?( @ph1 ) )
 end
 def testNoSchedule
  assert_raise(RuntimeError) { @manager.schedule( [@ih1] ) }
 end
 def testPreferredSchedule2
  @manager2.schedule( [@ph1,@ih1] )
  assert( @worker.assigned?( @ph1 ) )
  assert( @worker2.assigned?( @ih1 ) )
 end
 def testNonPreferredSchedule
  @manager2.schedule( [@nph1, @nph2] )
  assert( @worker.assigned?( @nph1 ) )
  assert( @worker.assigned?( @nph2 ) )
  assert( !@worker2.assigned?( @nph1 ) )
  assert( !@worker2.assigned?( @nph2 ) )
 end
 def testDoubleOccupancy
  @manager2.schedule( [@nph1, @nph1, @nph2, @nph2] )
  assert( @worker.assigned?( @nph1 ) )
  assert( @worker.assigned?( @nph2 ) )
  assert( @worker2.assigned?( @nph1 ) )
  assert( @worker2.assigned?( @nph2 ) )
 end
 def testPreferredAndNonPreferred
  @manager3.schedule( [@ph1,@nph2] )
  assert( @worker.assigned?( @ph1 ) )
  assert( @worker3.assigned?( @nph2 ) )
  assert( !@worker2.assigned <mailto:!@worker2.assigned> ?( @ph1 ) )
  assert( !@worker3.assigned?( @ph1 ) )
 end
 
end
 
Thanks for any comments you may make!
Tanton
**************************************************************************
The information contained in this communication is confidential, is
intended only for the use of the recipient named above, and may be legally
privileged.

If the reader of this message is not the intended recipient, you are
hereby notified that any dissemination, distribution or copying of this
communication is strictly prohibited.

If you have received this communication in error, please resend this
communication to the sender and delete the original message or any copy
of it from your computer system.

Thank You.
**************************************************************************

------_extPart_001_01C5A413.D0D27634--