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,176 @@
1
+ module Schedsolver2
2
+
3
+ class School
4
+ include Schedsolver2
5
+ attr_accessor :cs
6
+ attr_reader :scheds, :consts, :si, :ts
7
+
8
+ def initialize si, ts, cs
9
+ @si, @ts, @cs = si, ts, cs
10
+ @consts = generate_constants
11
+ @scheds = []
12
+ verify_initialize_args
13
+ parse_part_time_constraints
14
+ end
15
+
16
+ def print attr
17
+ case attr
18
+ when :schedules
19
+ @scheds.each {|s| log.info("\n" + s.to_s) }
20
+ when :school_info
21
+ log.info("SCHOOL INFO #{@si}")
22
+ when :consts
23
+ log.info("CONSTS #{@consts}")
24
+ when :teachers
25
+ log.info("TEACHERS #{@ts}")
26
+ when :constraints
27
+ log.info("CONSTRAINTS #{@cs}")
28
+ end
29
+ end
30
+
31
+ def parse_part_time_constraints
32
+ @cs[:part_time] ||= {}
33
+ @ts[:e].each do |et|
34
+ not_work_days = (days - et.work_days)
35
+ if not_work_days == [] then next end
36
+ s_d = Constraint.days_descriptor(not_work_days, times, [et.id])
37
+ #log.debug("School#parse_part_time constraints\nS_D = #{s_d}\n")
38
+ new_pt_c = Constraint.new(:type => :part_time,
39
+ :et_ids => [et.id],
40
+ :s_ds => [s_d],
41
+ :name => "et#{et.id} part time")
42
+ @cs[:part_time][et.id] = new_pt_c
43
+ end
44
+ end
45
+
46
+ def verify_initialize_args
47
+ check = ((@si[:end_time] - @si[:start_time]) == (@si[:num_blocks] * @si[:block_size]))
48
+ raise ArgumentError, "si: end-start != num*size" unless check == true
49
+
50
+ @ts[:e].each do |t|
51
+ raise ArgumentError, "ts:e: has non-teacher" unless t.class == Teacher
52
+ end
53
+
54
+ @ts[:h].each do |t|
55
+ raise ArgumentError, "ts:h: has non-teacher" unless t.class == Teacher
56
+ end
57
+ #add check for cs?
58
+ end
59
+
60
+ def generate_constants
61
+ h = Hash.new
62
+ h[:days] = [:mon, :tue, :wed, :thu, :fri]
63
+ h[:times] = generate_times
64
+ h[:et_ids] = @ts[:e].map {|t| t.id}
65
+ h[:ht_ids] = @ts[:h].map {|t| t.id}
66
+ return h
67
+ end
68
+
69
+ def generate_times
70
+ ts, t = [], @si[:start_time]
71
+ @si[:num_blocks].times do
72
+ ts << t
73
+ t += @si[:block_size]
74
+ end
75
+ return ts
76
+ end
77
+
78
+ def days
79
+ @consts[:days]
80
+ end
81
+
82
+ def times
83
+ @consts[:times]
84
+ end
85
+
86
+ def et_ids
87
+ @consts[:et_ids]
88
+ end
89
+
90
+ def ht_ids
91
+ @consts[:ht_ids]
92
+ end
93
+
94
+ def add type, *args
95
+ case type
96
+ when :schedules
97
+ raise ArgumentError unless (args.size == 1 and args[0] > 0)
98
+ args[0].times { @scheds << Schedule.new(@consts) }
99
+ when :hard_constraint
100
+ @cs[:h] << args[0]
101
+ end
102
+ end
103
+
104
+ def generate_schedule(index)
105
+ #log.debug("GENERATE SCHEDULE")
106
+ i = index
107
+ @ts[:h].shuffle.each do |ht|
108
+ ht_id = ht.id
109
+ #log.debug("Assigning for ht #{ht_id}")
110
+ count = nil
111
+ count = ClassCounter.new(@consts[:et_ids])
112
+
113
+ @ts[:e].shuffle.each do |et|
114
+ et_id = et.id
115
+ w_remaining = @cs[:ecs_per_week] - count.by_et_id(et_id)
116
+ #log.debug("\tLOOKING at et #{et_id} with #{w_remaining} remaining this week")
117
+ if w_remaining == 0
118
+ #log.debug("\tBREAK because remaining is 0 (top of days loop) ")
119
+ break
120
+ end
121
+
122
+ @consts[:times].shuffle.each do |time|
123
+ #log.debug("\tTIME: #{time}")
124
+ w_remaining = @cs[:ecs_per_week] - count.by_et_id(et_id)
125
+
126
+ if w_remaining == 0
127
+ #log.debug("\tBREAK because w_remaining is 0 (top of #{time} loop) ")
128
+ break
129
+ end
130
+
131
+ @consts[:days].shuffle.each do |day|
132
+
133
+ d_remaining = @cs[:max_ecs_per_day] - count.by_day(day)
134
+ w_remaining = @cs[:ecs_per_week] - count.by_et_id(et_id)
135
+
136
+ if d_remaining == 0
137
+ #log.debug("\tBREAK because d_remaining is 0 (top of #{day} loop) ")
138
+ next
139
+ elsif w_remaining == 0
140
+ #log.debug("\tBREAK because w_remaining is 0 (top of #{day} loop) ")
141
+ break
142
+ elsif @scheds[i][day, time, et_id] != nil
143
+ #log.debug("\tNEXT because slot (#{day}, #{time}) is full")
144
+ next
145
+ elsif count[day, et_id] > 0
146
+ #log.debug("\tNEXT because this EC is already assigned to #{day}")
147
+ next
148
+ elsif not et.working?(day)
149
+ #log.debug("\tNEXT because et #{et_id} doesn't work on #{day}")
150
+ next
151
+ end
152
+
153
+ begin
154
+ @scheds[i][day, time, et_id] = ht_id
155
+ rescue InsanityError
156
+ #log.debug("\tNEXT because of insanity")
157
+ next
158
+ else
159
+ count[day, et_id] += 1
160
+ d_remaining = @cs[:max_ecs_per_day] - count.by_day(day)
161
+ w_remaining = @cs[:ecs_per_week] - count.by_et_id(et_id)
162
+ #log.debug("\tASSIGNED to (#{day},#{time}) with #{d_remaining} remaining today and #{w_remaining} for week")
163
+ end
164
+
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ def db
172
+ @scheds[0][:mon, 8, :Art] = 'TEST'
173
+ @scheds[1][:wed, 12, :Music] = 'TEST'
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,49 @@
1
+ module Schedsolver2
2
+ class Teacher
3
+ attr_accessor :name, :id, :work_days
4
+
5
+ @@ids ||= []
6
+ def Teacher::ids
7
+ @@ids
8
+ end
9
+
10
+ def Teacher::reset_ids
11
+ @@ids = []
12
+ end
13
+
14
+ def initialize name, id=nil
15
+ @name = name.to_s
16
+ @work_days = [:mon, :tue, :wed, :thu, :fri]
17
+ unless id == nil then
18
+ raise ArgumentError, "Dupicate id" unless Teacher.uniq_id?(id)
19
+ @id = id
20
+ else
21
+ @id = Teacher.generate_id
22
+ end
23
+ @@ids << @id
24
+ end
25
+
26
+ def to_s
27
+ @name.to_s + "(#{self.id})"
28
+ end
29
+
30
+ def self.generate_id
31
+ id = 0
32
+ until Teacher.uniq_id?(id); id += 1; end
33
+ return id
34
+ end
35
+
36
+ def self.uniq_id? id
37
+ if @@ids.include?(id)
38
+ return false
39
+ else
40
+ return true
41
+ end
42
+ end
43
+
44
+ def working? day
45
+ @work_days.include? day
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'schedsolver2/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "schedsolver2"
8
+ gem.version = Schedsolver2::VERSION
9
+ gem.authors = ["Julian Irwin"]
10
+ gem.email = ["julian.irwin@gmail.com"]
11
+ gem.description = %q{Create schedules for elementary schools}
12
+ gem.summary = ""
13
+ gem.homepage = ""
14
+
15
+ gem.add_dependency('pp')
16
+ gem.add_dependency('text-table')
17
+ gem.add_dependency('logger')
18
+
19
+ gem.files = `git ls-files`.split($/)
20
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
21
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
+ gem.require_paths = ["lib"]
23
+ end
data/script/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/schedsolver2.rb'}"
9
+ puts "Loading schedsolver2 gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
data/script/destroy ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
data/script/generate ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,38 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ module Schedsolver2
4
+
5
+ describe ClassCounter do
6
+ include_context "main"
7
+
8
+ before(:each) do
9
+ @count = ClassCounter.new [0 ,1, 2]
10
+ end
11
+
12
+ describe "#by_et_id" do
13
+ it "counts all ets of a certain type" do
14
+ @count[:mon, 0] = 1
15
+ @count[:fri, 0] = 2
16
+ @count[:mon, 1] = 4
17
+ @count[:wed, 1] = 8
18
+ @count.by_et_id(0).should == 3
19
+ @count.by_et_id(1).should == 12
20
+ @count.by_et_id(2).should == 0
21
+ end
22
+ end
23
+
24
+ describe "#by_day" do
25
+ it "counts all days of a certain type" do
26
+ @count[:mon, 0] = 1
27
+ @count[:fri, 0] = 2
28
+ @count[:mon, 1] = 4
29
+ @count[:wed, 1] = 8
30
+ @count.by_day(:mon).should == 5
31
+ @count.by_day(:tue).should == 0
32
+ @count.by_day(:wed).should == 8
33
+ end
34
+ end
35
+ end
36
+
37
+
38
+ end
@@ -0,0 +1,83 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ module Schedsolver2
4
+
5
+ describe Constraint do
6
+ include_context "main"
7
+
8
+
9
+ describe "#hard_assert" do
10
+ it "takes a constraint and a schedule" do
11
+ @school.add :schedules, 1
12
+ @school.add :hard_constraint, @c_true
13
+ expect {
14
+ Constraint.hard_assert @school.sched[0], @school.cs[:h][0]
15
+ }.to_not raise_error(ArgumentError)
16
+ end
17
+
18
+ it "throws an arg error upon getting bad input" do
19
+ expect {
20
+ Constraint.hard_assert :not_schec, @c_true
21
+ }.to raise_error(ArgumentError)
22
+ @school.add :schedules, 1
23
+ expect {
24
+ Constraint.hard_assert @school.scheds[0], :not_constraint
25
+ }.to raise_error(ArgumentError)
26
+ end
27
+
28
+ it "returns false for an impossible-to-meet constraint" do
29
+ @school.add :schedules, 1
30
+ Constraint.hard_assert(@school.scheds[0], @c_false).should == false
31
+ end
32
+
33
+ it "returns true for an always-met constraint" do
34
+ @school.add :schedules, 1
35
+ Constraint.hard_assert(@school.scheds[0], @c_true).should == true
36
+ end
37
+ end
38
+
39
+ describe "Constraint.make_slots_ary", :wip=>true do
40
+ it "returns nil for nil input" do
41
+ expect {
42
+ Constraint.make_slots_ary(nil)
43
+ }.to raise_error(ArgumentError)
44
+ end
45
+
46
+ it "turns {:mon => {8 => [:Art]}} into [:mon, 8, :Art]" do
47
+ h = {:mon => {8 => [:Art]}}
48
+ a = [[:mon, 8, :Art]]
49
+ Constraint.make_slots_ary(h).should == a
50
+ end
51
+
52
+ it "works for a bigger hash too" do
53
+ h = {:mon => {8 => [:Art, :Music], 9 => [:Music]}, :wed => {13 => [:Art]}}
54
+ a = [[:mon, 8, :Art], [:mon, 8, :Music], [:mon, 9, :Music], [:wed, 13, :Art]]
55
+ Constraint.make_slots_ary(h).should == a
56
+ end
57
+ end
58
+
59
+ describe "Constraint.day_descriptor", :wip => true do
60
+ it "gives a slot_descriptor for a whole day" do
61
+ s_d = {:mon => {8 => [:One],
62
+ 9 => [:One],
63
+ 10 => [:One],
64
+ 11 => [:One],
65
+ 12 => [:One],
66
+ 13 => [:One],
67
+ 14 => [:One]}}
68
+ Constraint.day_descriptor(:mon, @school.times, [:One]).should == s_d
69
+ end
70
+ end
71
+
72
+ describe "Constraint.week_descriptor", :wip => true do
73
+ it "gives a slot_descriptor for a whole week" do
74
+ s_d = {:mon => {8 => [0]},
75
+ :tue => {8 => [0]},
76
+ :wed => {8 => [0]},
77
+ :thu => {8 => [0]},
78
+ :fri => {8 => [0]}}
79
+ Constraint.week_descriptor([8], [0]).should == s_d
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,4 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe "Schedsolver2" do
4
+ end
@@ -0,0 +1,53 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ module Schedsolver2
4
+
5
+ describe Schedule do
6
+ include_context "main"
7
+
8
+ describe "#[]=, #[]" do
9
+ before(:all) do
10
+ @et = @ts[:e][0]
11
+ @et2 = @ts[:e][1]
12
+ @ht = @ts[:h][0]
13
+ @ht2 = @ts[:h][1]
14
+ @school.add :schedules, 1
15
+ @sched_class = @school.scheds[0]
16
+ end
17
+
18
+ it "adds teachers to slots in the schedule" do
19
+ @sched_class[:mon, 8, @et.id] = @ht.id
20
+ @sched_class.sched[0][0][0].should == @ht.id
21
+
22
+ @sched_class[:wed, 12, @et.id] = @ht.id
23
+ @sched_class.sched[0][0][0].should == @ht.id
24
+ end
25
+
26
+ it "blows up when it is given bad arguments" do
27
+ expect{@sched_class[:not_a_day, 8, @et.id]}.to raise_error(ArgumentError)
28
+ expect{@sched_class[:mon, -1, @et.id]}.to raise_error(ArgumentError)
29
+ expect{@sched_class[:mon, 8, :Not_a_teacher]}.to raise_error(ArgumentError)
30
+
31
+ expect{@sched_class[:not_a_day, 8, @et.id]=nil}.to raise_error(ArgumentError)
32
+ expect{@sched_class[:mon, -1, @et.id]=nil}.to raise_error(ArgumentError)
33
+ expect{@sched_class[:mon, 8, :Not_a_teacher]=nil}.to raise_error(ArgumentError)
34
+ end
35
+
36
+ it "throws InsanityError if one HC gets simultaneous ECs" do
37
+ @sched_class[:mon, 8, @et.id] = @ht.id
38
+ expect{
39
+ @sched_class[:mon, 8, @et2.id] = @ht.id
40
+ }.to raise_error(InsanityError)
41
+ end
42
+
43
+ it "doesn't throw Insanity error if distinct HCs get simultaneous ECs" do
44
+ @sched_class[:mon, 8, @et.id] = @ht.id
45
+ expect{
46
+ @sched_class[:mon, 8, @et2.id] = @ht2.id
47
+ }.to_not raise_error(InsanityError)
48
+ end
49
+ end
50
+ end
51
+
52
+ end
53
+