schedsolver2 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. data/.gitignore +23 -0
  2. data/Gemfile +4 -0
  3. data/LICENCE.txt +22 -0
  4. data/README.md +29 -0
  5. data/Rakefile +1 -0
  6. data/features/schedsolver_T1_initializes.feature +11 -0
  7. data/features/schedsolver_T2_seed.feature +16 -0
  8. data/features/schedsolver_T3_constraint_checker.feature +29 -0
  9. data/features/schedsolver_T3v2_constraints.feature +8 -0
  10. data/features/schedsolver_T4_print_attributes.feature +19 -0
  11. data/features/schedsolver_T5_crud_attributes.feature +26 -0
  12. data/features/schedsolver_T6_generate_schedules.feature +14 -0
  13. data/features/schedsolver_T7_log_output.feature +9 -0
  14. data/features/schedsolver_T8_big_example.feature +10 -0
  15. data/features/schedsolver_heuristic.feature +10 -0
  16. data/features/step_definitions/T1_steps.rb +19 -0
  17. data/features/step_definitions/T3_steps.rb +97 -0
  18. data/features/step_definitions/T3v2_steps.rb +14 -0
  19. data/features/step_definitions/T4_steps.rb +19 -0
  20. data/features/step_definitions/T6_steps.rb +62 -0
  21. data/features/step_definitions/heuristic_steps.rb +49 -0
  22. data/features/step_definitions/shared_steps.rb +10 -0
  23. data/features/support/env.rb +4 -0
  24. data/features/support/shared_contexts.rb +47 -0
  25. data/lib/schedsolver2.rb +84 -0
  26. data/lib/schedsolver2/ClassCounter.rb +42 -0
  27. data/lib/schedsolver2/constraint.rb +133 -0
  28. data/lib/schedsolver2/schedule.rb +107 -0
  29. data/lib/schedsolver2/school.rb +176 -0
  30. data/lib/schedsolver2/teacher.rb +49 -0
  31. data/schedsolver2.gemspec +23 -0
  32. data/script/console +10 -0
  33. data/script/destroy +14 -0
  34. data/script/generate +14 -0
  35. data/spec/ClassCounter_spec.rb +38 -0
  36. data/spec/constraint_spec.rb +83 -0
  37. data/spec/schedsolver2_spec.rb +4 -0
  38. data/spec/schedule_spec.rb +53 -0
  39. data/spec/school_spec.rb +56 -0
  40. data/spec/spec.opts +1 -0
  41. data/spec/spec_helper.rb +3 -0
  42. data/spec/support/masters/2ETs_8_to_14_by_1.schedule +12 -0
  43. data/spec/support/shared_contexts.rb +41 -0
  44. data/spec/teacher_spec.rb +43 -0
  45. metadata +151 -0
@@ -0,0 +1,49 @@
1
+ Given /^the school's (\d+)th ET works MWF$/ do |arg1|
2
+ @school.print :consts
3
+ @school.print :teachers
4
+ et = @school.ts[:e][0]
5
+ @et_id = et.id
6
+ et.work_days = [:mon, :wed, :fri]
7
+ end
8
+
9
+ Given /^each HT must have (\d+) ECs\/wk$/ do |arg1|
10
+ @school.cs[:ecs_per_week] = arg1.to_i
11
+ end
12
+
13
+ Given /^each HT may have at most (\d+) ECs\/dy$/ do |arg1|
14
+ @school.cs[:max_ecs_per_day] = arg1.to_i
15
+ end
16
+
17
+ When /^the schedule is generated$/ do
18
+ @school.add :schedules, 1
19
+ @school.print :consts
20
+ @school.generate_schedule(0)
21
+ @school.print :schedules
22
+ end
23
+
24
+ Then /^there wil be one EC on each of MWF for ET (\d+)$/ do |arg1|
25
+ @school.ht_ids.each do |ht_id|
26
+ week_count = 0
27
+ [:mon, :wed, :fri].each do |day|
28
+ day_count = 0
29
+ @school.times.each do |time|
30
+ if @school.scheds[0][day, time, @et_id] == ht_id then day_count += 1 end
31
+ end
32
+ day_count.should be <= @school.cs[:max_ecs_per_day]
33
+ week_count += day_count
34
+ end
35
+ week_count.should == @school.cs[:ecs_per_week]
36
+ end
37
+ end
38
+
39
+ Then /^there will be three total ET (\d+) entries$/ do |arg1|
40
+ count = 0
41
+ @school.days.each do |day|
42
+ @school.times.each do |time|
43
+ if @school.scheds[0][day, time, @et_id+1] == @school.ht_ids[0]
44
+ count += 1
45
+ end
46
+ end
47
+ end
48
+ count.should == 3
49
+ end
@@ -0,0 +1,10 @@
1
+ Given /^I have an initialized School$/ do
2
+ @school = Schedsolver2::School.new @si, @ts, @cs
3
+ @school.log = Logger.new( File.expand_path("../../../log/logfile.log",__FILE__))
4
+ @school.log.error("\n"*15 + "BEGINNING CUCUMBER ACCEPTANCE TEST RUN OF SS2")
5
+ end
6
+
7
+ Given /^It has "(.*?)" schedule(s?)$/ do |arg1, arg2|
8
+ x = arg1.to_i
9
+ @school.add :schedules, x
10
+ end
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH << File.expand_path('~/schedsolver2/lib', __FILE__)
2
+ require 'cucumber/rspec/doubles'
3
+ require 'schedsolver2'
4
+
@@ -0,0 +1,47 @@
1
+ Before('~@big_example') do
2
+ @si = {:start_time => 8,
3
+ :end_time => 15,
4
+ :num_blocks => 7,
5
+ :block_size => 1}
6
+
7
+ ets, et_ns = [], [:Art, :Music]
8
+ et_ns.each do |name|
9
+ ets << Schedsolver2::Teacher.new(name)
10
+ end
11
+ hts, ht_ns = [], [:One]
12
+ ht_ns.each do |name|
13
+ hts << Schedsolver2::Teacher.new(name)
14
+ end
15
+ @ts = {:e => ets,
16
+ :h => hts}
17
+ @cs = {:h => [],
18
+ :s => []}
19
+ @school2 = Schedsolver2::School.new @si, @ts, @cs
20
+ @all_days_slot_descriptor = {}
21
+ @school2.days.each do |day|
22
+ @all_days_slot_descriptor[day] ||= {}
23
+ @school2.times.each do |time|
24
+ @all_days_slot_descriptor[day][time] = @school2.et_ids
25
+ end
26
+ end
27
+ end
28
+
29
+ Before('@heuristic') do
30
+ @si = {:start_time => 8,
31
+ :end_time => 15,
32
+ :num_blocks => 7,
33
+ :block_size => 1}
34
+
35
+ @ets = [ Schedsolver2::Teacher.new(:Art), Schedsolver2::Teacher.new(:Music) ]
36
+ @hts = [ Schedsolver2::Teacher.new(:One)]
37
+
38
+ @ts = {:e => @ets, :h => @hts}
39
+ @cs = {}
40
+
41
+ @school = Schedsolver2::School.new @si, @ts, @cs
42
+
43
+ @school.cs[:ecs_per_week] = 2
44
+ @school.cs[:max_ecs_per_day] = 2
45
+ end
46
+
47
+
@@ -0,0 +1,84 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'pp'
5
+ require 'text-table'
6
+ require 'logger'
7
+
8
+ module Schedsolver2
9
+
10
+ # :stopdoc:
11
+ LIBPATH = ::File.expand_path('..', __FILE__) + ::File::SEPARATOR
12
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
13
+ VERSION = "0.0.1"
14
+ # :startdoc:
15
+
16
+ #Setup the module-scoped log
17
+ #
18
+ def self.log
19
+ @log ||= Logger.new(STDOUT)
20
+ end
21
+
22
+ def self.log=(arg)
23
+ @log = arg
24
+ end
25
+
26
+ def log
27
+ Schedsolver2.log
28
+ end
29
+
30
+ def log=(arg)
31
+ Schedsolver2.log = arg
32
+ end
33
+
34
+ # Returns the library path for the module. If any arguments are given,
35
+ # they will be joined to the end of the libray path using
36
+ # <tt>File.join</tt>.
37
+ #
38
+ def self.libpath( *args )
39
+ rv = args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
40
+ if block_given?
41
+ begin
42
+ $LOAD_PATH.unshift LIBPATH
43
+ rv = yield
44
+ ensure
45
+ $LOAD_PATH.shift
46
+ end
47
+ end
48
+ return rv
49
+ end
50
+
51
+ # Returns the lpath for the module. If any arguments are given,
52
+ # they will be joined to the end of the path using
53
+ # <tt>File.join</tt>.
54
+ #
55
+ def self.path( *args )
56
+ rv = args.empty? ? PATH : ::File.join(PATH, args.flatten)
57
+ if block_given?
58
+ begin
59
+ $LOAD_PATH.unshift PATH
60
+ rv = yield
61
+ ensure
62
+ $LOAD_PATH.shift
63
+ end
64
+ end
65
+ return rv
66
+ end
67
+
68
+ # Utility method used to require all files ending in .rb that lie in the
69
+ # directory below this file that has the same name as the filename passed
70
+ # in. Optionally, a specific _directory_ name can be passed in such that
71
+ # the _filename_ does not have to be equivalent to the directory.
72
+ #
73
+ def self.require_all_libs_relative_to( fname, dir = nil )
74
+ dir ||= ::File.basename(fname, '.*')
75
+ search_me = ::File.expand_path(
76
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
77
+
78
+ Dir.glob(search_me).sort.each {|rb| require rb}
79
+ end
80
+
81
+
82
+ end # module Schedsolver2
83
+
84
+ Schedsolver2.require_all_libs_relative_to(__FILE__)
@@ -0,0 +1,42 @@
1
+ module Schedsolver2
2
+
3
+ class ClassCounter
4
+ attr_reader :counts
5
+
6
+ def initialize et_ids
7
+ @counts = {}
8
+ [:mon, :tue, :wed, :thu, :fri].each do |day|
9
+ @counts[day] = Hash.new
10
+ et_ids.each do |et_id|
11
+ @counts[day][et_id] = 0
12
+ end
13
+ end
14
+ end
15
+
16
+ def [](arg1, arg2)
17
+ @counts[arg1][arg2]
18
+ end
19
+
20
+ def []=(arg1, arg2, val)
21
+ @counts[arg1][arg2] = val
22
+ end
23
+
24
+ def by_et_id id
25
+ count = 0
26
+ @counts.each_key do |day|
27
+ count += @counts[day][id]
28
+ end
29
+ return count
30
+ end
31
+
32
+ def by_day day
33
+ count = 0
34
+ @counts[day].each_value { |v| count += v }
35
+ return count
36
+ end
37
+
38
+ def to_s
39
+ @counts.to_s
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,133 @@
1
+ module Schedsolver2
2
+
3
+ class Constraint
4
+ include Schedsolver2
5
+ attr_reader :descriptors, :test, :assess
6
+ @@days = [:mon, :tue, :wed, :thu, :fri]
7
+
8
+ def initialize hsh
9
+ raise ArgumentError, "No type specified" unless hsh.has_key?(:type)
10
+ raise ArgumentError, "No slot descriptors given" unless hsh.has_key?(:s_ds)
11
+
12
+ if hsh.has_key?(:name)
13
+ @name = hsh[:name]
14
+ else
15
+ @name = 'Unnamed constraint'
16
+ end
17
+
18
+ @descriptors = []
19
+ hsh[:s_ds].each do |s_d|
20
+ @descriptors << Constraint.make_slots_ary(s_d)
21
+ end
22
+
23
+ unless hsh[:type] == :custom
24
+ raise ArgumentError, "No et specified" unless hsh.has_key?(:et_ids)
25
+ @test = get_test_for(hsh)
26
+ @assess = get_assess_for(hsh)
27
+ else
28
+ @test = hsh[:test]
29
+ @assess = hsh[:assess]
30
+ end
31
+ end
32
+
33
+ def to_s
34
+ @name
35
+ end
36
+
37
+ # # # # # # Class Methods # # # # # # # #
38
+
39
+ # slot_descriptors are: hash[day][time][et_id] that specifiy groups
40
+ # of slots that can be used when enforcing constraints.
41
+
42
+ # slot_arys are: an array like [ [day, time, et_id], ...] that is
43
+ # actually used by the Proc of the constraint.
44
+
45
+ # In the constructor, we get one or more descriptors which are
46
+ # converted into an array of slots_arys. Confusing variable names are
47
+ # everywhere. I suck.
48
+
49
+ def self.make_slots_ary s_d
50
+ raise ArgumentError, "No slots given in slot descriptor" if s_d == nil
51
+ ary = []
52
+ s_d.each do |day, hour_hash|
53
+ hour_hash.each do |hour, et_ids|
54
+ et_ids.each do |et_id|
55
+ ary << [day, hour, et_id]
56
+ end
57
+ end
58
+ end
59
+ return ary
60
+ end
61
+
62
+ def self.day_descriptor day, times, et_ids
63
+ raise ArgumentError, "et_ids not an array" unless (et_ids.class == Array)
64
+ h = {}
65
+ h[day.to_sym] = {}
66
+ times.each do |time|
67
+ h[day.to_sym][time] = et_ids
68
+ end
69
+ return h
70
+ end
71
+
72
+ def self.days_descriptor days, times, et_ids
73
+ s_d = {}
74
+ days.each do |day|
75
+ s_d.merge! Constraint.day_descriptor(day, times, et_ids)
76
+ end
77
+ return s_d
78
+ end
79
+
80
+ def self.week_descriptor times, et_ids
81
+ s_d = {}
82
+ @@days.each do |day|
83
+ s_d.merge! Constraint.day_descriptor(day, times, et_ids)
84
+ end
85
+ return s_d
86
+ end
87
+
88
+ def get_test_for(hsh)
89
+ case hsh[:type]
90
+ when :part_time
91
+ raise ArgumentError, "et_ids.size > 1 for part_time type" unless(hsh[:et_ids].size <= 1)
92
+ p = Proc.new { |slot, value| if value != nil then true else false end}
93
+ return p
94
+ end
95
+ end
96
+
97
+ def get_assess_for(hsh)
98
+ case hsh[:type]
99
+ when :part_time
100
+ p = Proc.new {|count| if count > 0 then false else true end }
101
+ return p
102
+ end
103
+ end
104
+
105
+
106
+ def self.hard_assert schedule, constraint
107
+
108
+ unless schedule.class == Schedule
109
+ raise ArgumentError, "Schedule is of wrong class #{schedule.class}"
110
+ end
111
+
112
+ unless constraint.class == Constraint
113
+ raise ArgumentError, "Constraint is of wrong class #{constraint.class}"
114
+ end
115
+
116
+ constraint.descriptors.each do |descriptor|
117
+ count = 0
118
+ descriptor.each do |slot|
119
+ d,t,id = slot
120
+ value = schedule[d,t,id]
121
+ if (constraint.test.call(slot, value) == true)
122
+ count += 1
123
+ #Schedsolver2.log.info("Constraint#hard_assert #{d},#{t},#{id} gives true value: #{value}")
124
+ else
125
+ #Schedsolver2.log.info("#Constraint#hard_assert #{d},#{t},#{id} gives false value: #{value}")
126
+ end
127
+ end
128
+ if constraint.assess.call(count) != true then return false end
129
+ end
130
+ return true
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,107 @@
1
+ module Schedsolver2
2
+ class Schedule
3
+ include Schedsolver2
4
+ attr_accessor :sched
5
+
6
+ def initialize consts
7
+ @@consts = consts
8
+ @sched = init_sched
9
+ end
10
+
11
+ def init_sched
12
+ n_days = @@consts[:days].size
13
+ n_ts = @@consts[:times].size
14
+ n_ets = @@consts[:et_ids].size
15
+ Array.new(n_days){ Array.new(n_ts){ Array.new(n_ets, nil)}}
16
+ end
17
+
18
+ def self.parse_getter_setter_args day, time, et_id
19
+ if day == nil
20
+ raise ArgumentError, "Day is nil, cannot proceed"
21
+ end
22
+
23
+ i = @@consts[:days].index(day)
24
+ j = @@consts[:times].index(time.to_f)
25
+ k = @@consts[:et_ids].index(et_id)
26
+
27
+ if (day != nil and i == nil) then raise ArgumentError, "day #{day} isn't is days" end
28
+ if (time != nil and j == nil) then raise ArgumentError, "time #{time} isn't is times" end
29
+ if (et_id != nil and k == nil) then raise ArgumentError, "et_id #{et_id} isn't is ed_ids" end
30
+
31
+ return [i,j,k]
32
+ end
33
+
34
+ def []=(day=nil, time=nil, et_id=nil, entry=nil)
35
+ ijk = Schedule.parse_getter_setter_args(day, time, et_id)
36
+ i, j, k = ijk
37
+
38
+ if j == nil
39
+ @sched[i] = entry
40
+ elsif k == nil
41
+ @sched[i][j] = entry
42
+ else
43
+ sanity_check ijk, entry
44
+ @sched[i][j][k] = entry
45
+ end
46
+ end
47
+
48
+ def [](day=nil, time=nil, et_id=nil)
49
+ ijk = Schedule.parse_getter_setter_args(day, time, et_id)
50
+ i, j, k = ijk
51
+
52
+ if j == nil
53
+ @sched[i]
54
+ elsif k == nil
55
+ @sched[i][j]
56
+ else
57
+ @sched[i][j][k]
58
+ end
59
+ end
60
+
61
+ def sanity_check ijk, entry
62
+ i, j, k = ijk
63
+ #check for simultaneous ECs assigned to HC
64
+ num_ets = @@consts[:et_ids].size
65
+ (0..num_ets-1).each do |n|
66
+ if n == k then next end
67
+ if @sched[i][j][n] == entry
68
+ raise(InsanityError,"Can't assign HT #{entry} to sched[#{i}, #{j}, #{k}] because already assigned to sched[#{i}, #{j}, #{n}]")
69
+ end
70
+ end
71
+ # Check for simultaneous HCs assigned to EC?
72
+ # Should this be carried out by algorithm instead?
73
+ end
74
+
75
+
76
+ def to_s
77
+ head = [nil] + (@@consts[:et_ids] + [nil]) * 5
78
+ rows = []
79
+ #create col1
80
+ @@consts[:times].each { |t| rows << [t] }
81
+ #add body
82
+ @@consts[:days].each_index do |d|
83
+ @@consts[:times].each_index do |t|
84
+ rows[t] += (@sched[d][t] << "^")
85
+ end
86
+ end
87
+ return Text::Table.new(:head => head, :rows => rows).to_s
88
+ end
89
+ end
90
+
91
+ class InsanityError < StandardError
92
+ end
93
+
94
+ end
95
+
96
+ # def check_args *argv
97
+ # 2.times do |i|
98
+ # if argv[i] == nil
99
+ # return true
100
+ # unless (@@consts[:days].include?(argv[i]) || @@consts[:times].include?(argv[i]) || @@consts[:et_ids].include?(argv[i]))
101
+ # raise ArgumentError, "getter or setter had bad paramaters (not in @@consts)"
102
+ # else
103
+ # end
104
+ # end
105
+ # end
106
+ # end
107
+