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.
- data/.gitignore +23 -0
- data/Gemfile +4 -0
- data/LICENCE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/features/schedsolver_T1_initializes.feature +11 -0
- data/features/schedsolver_T2_seed.feature +16 -0
- data/features/schedsolver_T3_constraint_checker.feature +29 -0
- data/features/schedsolver_T3v2_constraints.feature +8 -0
- data/features/schedsolver_T4_print_attributes.feature +19 -0
- data/features/schedsolver_T5_crud_attributes.feature +26 -0
- data/features/schedsolver_T6_generate_schedules.feature +14 -0
- data/features/schedsolver_T7_log_output.feature +9 -0
- data/features/schedsolver_T8_big_example.feature +10 -0
- data/features/schedsolver_heuristic.feature +10 -0
- data/features/step_definitions/T1_steps.rb +19 -0
- data/features/step_definitions/T3_steps.rb +97 -0
- data/features/step_definitions/T3v2_steps.rb +14 -0
- data/features/step_definitions/T4_steps.rb +19 -0
- data/features/step_definitions/T6_steps.rb +62 -0
- data/features/step_definitions/heuristic_steps.rb +49 -0
- data/features/step_definitions/shared_steps.rb +10 -0
- data/features/support/env.rb +4 -0
- data/features/support/shared_contexts.rb +47 -0
- data/lib/schedsolver2.rb +84 -0
- data/lib/schedsolver2/ClassCounter.rb +42 -0
- data/lib/schedsolver2/constraint.rb +133 -0
- data/lib/schedsolver2/schedule.rb +107 -0
- data/lib/schedsolver2/school.rb +176 -0
- data/lib/schedsolver2/teacher.rb +49 -0
- data/schedsolver2.gemspec +23 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/ClassCounter_spec.rb +38 -0
- data/spec/constraint_spec.rb +83 -0
- data/spec/schedsolver2_spec.rb +4 -0
- data/spec/schedule_spec.rb +53 -0
- data/spec/school_spec.rb +56 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/masters/2ETs_8_to_14_by_1.schedule +12 -0
- data/spec/support/shared_contexts.rb +41 -0
- data/spec/teacher_spec.rb +43 -0
- 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,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
|
+
|
data/lib/schedsolver2.rb
ADDED
@@ -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
|
+
|