rrschedule 0.1.8 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,65 @@
1
+ # RRSchedule #
2
+
3
+ RRSchedule makes it easier to generate round-robin schedules for sport leagues.
4
+
5
+ It takes into consideration the number of available playing surfaces and game times and split
6
+ games into gamedays that respect these contraints.
7
+
8
+ ## Installation ##
9
+ gem install rrschedule
10
+ require 'rrschedule'
11
+
12
+ ## Prepare the schedule ##
13
+ schedule=RRSchedule::Schedule.new(
14
+ #array of teams that will compete against each other. If you group teams into multiple flights (divisions),
15
+ #a separate round-robin is generated in each of them but the "physical constraints" are shared
16
+ :teams => [
17
+ %w(A1 A2 A3 A4 A5 A6 A7 A8),
18
+ %w(B1 B2 B3 B4 B5 B6 B7 B8)
19
+ ],
20
+
21
+ #Setup some scheduling rules
22
+ :rules => [
23
+ RRSchedule::Rule.new(:wday => 3, :gt => ["7:00PM","9:00PM"], :ps => ["field #1", "field #2"]),
24
+ RRSchedule::Rule.new(:wday => 5, :gt => ["7:00PM"], :ps => ["field #1"])
25
+ ],
26
+
27
+ #First games are played on...
28
+ :start_date => Date.parse("2010/10/13"),
29
+
30
+ #array of dates to exclude
31
+ :exclude_dates => [Date.parse("2010/11/24"),Date.parse("2010/12/15")],
32
+
33
+ #Number of times each team must play against each other (default is 1)
34
+ :cycles => 1,
35
+
36
+ #Shuffle team order before each cycle. Default is true
37
+ :shuffle => true
38
+ )
39
+
40
+ ## Generate the schedule ##
41
+ schedule.generate
42
+
43
+ ## Playing with the output ##
44
+
45
+ ### human readable schedule ###
46
+ puts schedule.to_s
47
+
48
+ ### Iterate through schedule ###
49
+ schedule.gamedays.each do |gd|
50
+ puts gd.date.strftime("%Y/%m/%d")
51
+ puts "===================="
52
+ gd.games.each do |g|
53
+ puts g.team_a.to_s + " Vs " + g.team_b.to_s + " on playing surface ##{g.playing_surface} at #{g.game_time.strftime("%I:%M %p")}"
54
+ end
55
+ puts "\n"
56
+ end
57
+
58
+ ### Display each round of the round-robin(s) without any date/time or playing location info ###
59
+ puts s.rounds.collect{|r| r.to_s}
60
+
61
+ ## Issues / Other ##
62
+
63
+ Hope this gem will be useful to some people!
64
+
65
+ You can read my [blog](http://www.rubyfleebie.com)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.8
1
+ 0.2.0
@@ -0,0 +1,55 @@
1
+ # Branches summary
2
+
3
+ The objective of this file is to share the intention behind every new branches that are created for RRSchedule. Since each branch
4
+ generally represents a "development effort", I think it is useful to describe and explain them.
5
+
6
+ ## Rules and multiple round-robins (branch created on 2011/01/21)
7
+
8
+ ### Rules
9
+
10
+ The idea behind having "rules" is to allow more flexibility in the schedule generation.
11
+
12
+ At the moment, we can tell RRSchedule that the games are played (for example) every monday and wednesday at 7:00PM and 9:00PM on four different
13
+ playing surfaces. But what if games played on monday are held at 7:00PM while games played on wednesday are held at 7:00PM and 9:00PM?
14
+ You just cannot configure it this way at the moment. And what if on monday every playing surfaces are available while only 1 is available on wednesday?
15
+ This is why I had the idea of replacing the current "one size fits all" system with a more flexible system.
16
+
17
+ Thus, we will be able to have rules like:
18
+
19
+ Day of week | Game Time | playing surfaces |
20
+ Monday | 7:00PM | Field #1, Field #2 |
21
+ Wednesday | 7:00PM | Field #1 |
22
+ Wednesday | 9:00PM | Field #1
23
+
24
+ In the code it might look something like this:
25
+
26
+ schedule.rules << Rule.new(:wday => 1, :gt => "7:00PM", :ps => ["Field #1", "Field #2"])
27
+ schedule.rules << Rule.new(:wday => 3, :gt => "7:00PM", :ps => "Field #1")
28
+ schedule.rules << Rule.new(:wday => 3, :gt => "9:00PM", :ps => "Field #1")
29
+
30
+ schedule.generate
31
+
32
+ ...
33
+
34
+ ### Multiple round-robins
35
+
36
+ The other development in this branch involves the possibility to create multiple round-robins in the same method call while sharing the same
37
+ physical constraints (game times and playing surfaces)
38
+
39
+ Several sport leagues use a "division" system where teams are seeded in different groups based on their performance. Each group plays
40
+ a round-robin *inside* its own division. Suppose a league of 4 divisions containing 8 teams each. A1 will play against A2, A3 and so on but not
41
+ against B1, C3 or D6. What we need in this case is 4 different round-robins. However, these 4 round-robins share the same playing surfaces and
42
+ game times.
43
+
44
+ In other words, you will be able to do something like this:
45
+
46
+ schedule.teams = [
47
+ [A1,A2,A3,A4,A5,A6,A7,A8],
48
+ [B1,B2,B3,B4,B5,B6,B7,B8],
49
+ [C1,C2,C3,C4,C5,C6,C7,C8],
50
+ [D1,D2,D3,D4,D5,D6,D7,D8]
51
+ ]
52
+
53
+ schedule.rules << Rule.new(
54
+ ... #same rules apply for all 32 teams
55
+ )
data/lib/rrschedule.rb CHANGED
@@ -3,155 +3,89 @@
3
3
  ############################################################################################################################
4
4
  module RRSchedule
5
5
  class Schedule
6
- attr_reader :playing_surfaces, :game_times, :cycles, :wdays, :start_date, :exclude_dates,
7
- :shuffle_initial_order, :optimize, :teams, :rounds, :gamedays
8
-
9
-
10
- #Array of teams that will compete against each other. You can pass it any kind of object
11
- def teams=(arr)
12
- @teams = arr ? arr.clone : [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
13
- raise ":dummy is a reserved team name. Please use something else" if @teams.member?(:dummy)
14
- raise "at least 2 teams are required" if @teams.size == 1
15
- raise "teams have to be unique" if @teams.uniq.size < @teams.size
16
- @teams << :dummy if @teams.size.odd?
17
- end
18
-
19
- #Array of available playing surfaces. You can pass it any kind of object
20
- def playing_surfaces=(ps)
21
- @playing_surfaces = Array(ps).empty? ? ["Surface A", "Surface B"] : Array(ps)
22
- end
23
-
24
- #Number of times each team plays against each other
25
- def cycles=(cycles)
26
- @cycles = cycles || 1
27
- end
28
-
29
- #Array of game times where games are played. Must be valid DateTime objects in the string form
30
- def game_times=(gt)
31
- @game_times = Array(gt).empty? ? ["7:00 PM", "9:00 PM"] : Array(gt)
32
- @game_times.collect! do |gt|
33
- begin
34
- DateTime.parse(gt)
35
- rescue
36
- raise "game times must be valid time representations in the string form (e.g. 3:00 PM, 11:00 AM, 18:20, etc)"
37
- end
38
- end
39
- end
40
-
41
- #Setting this to true will fill all the available playing surfaces and game times for a given gameday no matter if
42
- #one team has to play several games on the same gameday. Setting it to false make sure that teams won't play
43
- #more than one game per day.
44
- def optimize=(opt)
45
- @optimize = opt.nil? ? true : opt
46
- end
47
-
48
- #Shuffle the team order at the beginning of every cycles.
49
- def shuffle_initial_order=(shuffle)
50
- @shuffle_initial_order = shuffle.nil? ? true : shuffle
51
- end
52
-
53
- #Array of dates without games
54
- def exclude_dates=(dates)
55
- @exclude_dates=dates || []
56
- end
57
-
58
- #When the season starts? Since we generate the game dates based on weekdays, you need to pass it
59
- #a start date in the correct timezone to get accurate game dates for the whole season. Otherwise
60
- #you might
61
- def start_date=(date)
62
- @start_date=date || Date.today
63
- end
64
-
65
- #Array of weekdays where games are played (0 is sunday)
66
- def wdays=(wdays)
67
- @wdays = Array(wdays).empty? ? [1] : Array(wdays)
68
- raise "each value in wdays must be between 0 and 6" if @wdays.reject{|w| (0..6).member?(w)}.size > 0
69
- end
70
-
6
+ attr_reader :flights, :rounds, :gamedays
7
+ attr_accessor :teams, :rules, :cycles, :start_date, :exclude_dates,:shuffle
8
+
71
9
  def initialize(params={})
72
10
  @gamedays = []
73
- self.teams = params[:teams]
74
- self.playing_surfaces = params[:playing_surfaces]
75
- self.cycles = params[:cycles]
76
- self.game_times = params[:game_times]
77
- self.optimize = params[:optimize]
78
- self.shuffle_initial_order = params[:shuffle_initial_order]
79
- self.exclude_dates = params[:exclude_dates]
80
- self.start_date = params[:start_date]
81
- self.wdays = params[:wdays]
11
+ self.teams = params[:teams] if params[:teams]
12
+ self.cycles = params[:cycles] || 1
13
+ self.shuffle = params[:shuffle].nil? ? true : params[:shuffle]
14
+ self.exclude_dates = params[:exclude_dates] || []
15
+ self.start_date = params[:start_date] || Date.today
16
+ self.rules = params[:rules] || []
82
17
  self
83
18
  end
84
-
85
-
19
+
86
20
  #This will generate the schedule based on the various parameters
87
- #TODO: consider refactoring with a recursive algorithm
88
21
  def generate(params={})
22
+ raise "You need to specify at least 1 team" if @teams.nil? || @teams.empty?
23
+ raise "You need to specify at least 1 rule" if @rules.nil? || @rules.empty?
24
+ arrange_flights
89
25
  @gamedays = []
90
26
  @rounds = []
91
- @teams = @teams.sort_by{rand} if self.shuffle_initial_order
92
- initial_order = @teams.clone
93
- current_cycle = current_round = 0
94
- all_games = []
95
-
96
- #Cycle loop (A cycle is completed when every teams have played one game against each other)
97
- begin
98
- games = []
99
- t = @teams.clone
100
-
101
- #Round loop
102
- while !t.empty? do
103
- team_a = t.shift
104
- team_b = t.reverse!.shift
105
- t.reverse!
106
-
107
- matchup = {:team_a => team_a, :team_b => team_b}
108
- games << matchup; all_games << matchup
109
- end
110
-
111
- current_round += 1
112
-
113
- @rounds ||= []
114
- @rounds << Round.new(
115
- :round => current_round,
116
- :games => games.collect { |g| Game.new(
117
- :team_a => g[:team_a],
118
- :team_b => g[:team_b])
119
- })
120
-
121
- reject_dummy = lambda {|g| g[:team_a] == :dummy || g[:team_b] == :dummy}
122
- games.reject! {|g| reject_dummy.call(g)}
123
- all_games.reject! {|g| reject_dummy.call(g)}
124
-
125
- @teams = @teams.insert(1,@teams.delete_at(@teams.size-1))
126
-
127
- #If we have completed a cycle
128
- if @teams == initial_order
129
- current_cycle += 1
130
- #Shuffle the teams at each cycle
131
- if current_cycle <= self.cycles && self.shuffle_initial_order
132
- @teams = @teams.sort_by{rand}
133
- initial_order = @teams.clone
27
+
28
+ @flights.each_with_index do |teams,flight_id|
29
+ current_cycle = current_round = 0
30
+ teams = teams.sort_by{rand} if @shuffle
31
+
32
+ #loop to generate the whole round-robin(s) for the current flight
33
+ begin
34
+ t = teams.clone
35
+ games = []
36
+
37
+ #process one round
38
+ while !t.empty? do
39
+ team_a = t.shift
40
+ team_b = t.reverse!.shift
41
+ t.reverse!
42
+
43
+ matchup = {:team_a => team_a, :team_b => team_b}
44
+ games << matchup
134
45
  end
135
- end
136
- end until @teams == initial_order && current_cycle==self.cycles
46
+ #done processing round
137
47
 
138
- slice(all_games)
139
- self
140
- end
48
+ current_round += 1
141
49
 
142
- #returns an array of Game instances where team_a and team_b are facing each other
143
- def face_to_face(team_a,team_b)
144
- res=[]
145
- self.gamedays.each do |gd|
146
- res << gd.games.select {|g| (g.team_a == team_a && g.team_b == team_b) || (g.team_a == team_b && g.team_b == team_a)}
50
+ #Team rotation
51
+ teams = teams.insert(1,teams.delete_at(teams.size-1))
52
+
53
+ #add the round in memory
54
+ @rounds ||= []
55
+ @rounds[flight_id] ||= []
56
+ @rounds[flight_id] << Round.new(
57
+ :round => current_round,
58
+ :flight => flight_id,
59
+ :games => games.collect { |g|
60
+ Game.new(
61
+ :team_a => g[:team_a],
62
+ :team_b => g[:team_b]
63
+ )
64
+ }
65
+ )
66
+ #done adding round
67
+
68
+ #have we completed a full round-robin for the current flight?
69
+ if current_round == teams.size-1
70
+ current_cycle += 1
71
+
72
+ if current_cycle < self.cycles
73
+ current_round = 0
74
+ teams = teams.sort_by{rand} if @shuffle
75
+ end
76
+ end
77
+
78
+ end until current_round == teams.size-1 && current_cycle==self.cycles
147
79
  end
148
- res.flatten
80
+
81
+ dispatch_games(@rounds)
82
+ self
149
83
  end
150
-
84
+
151
85
  #human readable schedule
152
86
  def to_s
153
87
  res = ""
154
- res << "#{self.gamedays.size.to_s} gamedays\n"
88
+ res << "#{self.gamedays.size.to_s} gamedays\n"
155
89
  self.gamedays.each do |gd|
156
90
  res << gd.date.strftime("%Y-%m-%d") + "\n"
157
91
  res << "==========\n"
@@ -163,92 +97,196 @@ module RRSchedule
163
97
  res
164
98
  end
165
99
 
166
- #return an array of Game instances where 'team' is playing
167
- def by_team(team)
168
- gms=[]
169
- self.gamedays.each do |gd|
170
- gms << gd.games.select{|g| g.team_a == team || g.team_b == team}
171
- end
172
- gms.flatten
173
- end
174
-
175
100
  #returns true if the generated schedule is a valid round-robin (for testing purpose)
176
- def round_robin?
101
+ def round_robin?(flight_id=nil)
177
102
  #each round-robin round should contains n-1 games where n is the nbr of teams (:dummy included if odd)
178
- return false if self.rounds.size != (@teams.size*self.cycles)-self.cycles
179
-
103
+ return false if self.rounds[flight_id].size != (@flights[flight_id].size*self.cycles)-self.cycles
104
+
180
105
  #check if each team plays the same number of games against each other
181
- self.teams.each do |t1|
182
- self.teams.reject{|t| t == t1}.each do |t2|
183
- return false unless self.face_to_face(t1,t2).size == self.cycles || [t1,t2].include?(:dummy)
106
+ @flights[flight_id].each do |t1|
107
+ @flights[flight_id].reject{|t| t == t1}.each do |t2|
108
+ return false unless face_to_face(t1,t2).size == self.cycles || [t1,t2].include?(:dummy)
184
109
  end
185
110
  end
186
111
  return true
187
112
  end
188
-
189
- private
190
- #Slice games according to playing surfaces available and game times
191
- def slice(games)
192
- slices = games.each_slice(games_per_day)
193
- wdays_stack = self.wdays.clone
194
- cur_date = self.start_date
195
- slices.each_with_index do |slice,i|
196
- gt_stack = self.game_times.clone.sort_by{rand}
197
- ps_stack = self.playing_surfaces.clone.sort_by{rand}
198
- wdays_stack = self.wdays.clone if wdays_stack.empty?
199
-
200
- cur_wday = wdays_stack.shift
201
- cur_date = next_game_date(cur_date,cur_wday)
202
- cur_gt = gt_stack.shift
203
-
204
- gameday = Gameday.new(:date => cur_date)
205
-
206
- slice.each_with_index do |g,game_index|
207
- cur_ps = ps_stack.shift
208
- gameday.games << Game.new(
209
- :team_a => g[:team_a],
210
- :team_b => g[:team_b],
211
- :playing_surface => cur_ps,
212
- :game_time => cur_gt,
213
- :game_date => cur_date)
214
-
215
- cur_gt = gt_stack.shift if ps_stack.empty?
216
- gt_stack = self.game_times.clone if gt_stack.empty?
217
- ps_stack = self.playing_surfaces.clone if ps_stack.empty?
113
+
114
+ private
115
+
116
+ def arrange_flights
117
+ #a flight is a division where teams play round-robin against each other
118
+ @flights = Marshal.load(Marshal.dump(@teams)) #deep clone
119
+
120
+ #If teams aren't in flights, we create a single flight and put all teams in it
121
+ @flights = [@flights] unless @flights.first.respond_to?(:to_ary)
122
+
123
+ @flights.each_with_index do |flight,i|
124
+ raise ":dummy is a reserved team name. Please use something else" if flight.member?(:dummy)
125
+ raise "at least 2 teams are required" if flight.size < 2
126
+ raise "teams have to be unique" if flight.uniq.size < flight.size
127
+ @flights[i] << :dummy if flight.size.odd?
128
+ end
129
+ end
130
+
131
+ #Dispatch games according to available playing surfaces and game times
132
+ #The flat schedule contains "place holders" for the actual games. Each row contains
133
+ #a game date, a game time and a playing surface. We then process our rounds one by one
134
+ #and we put each matchup in the next available slot of the flat schedule
135
+ def dispatch_games(rounds)
136
+ flat_schedule = generate_flat_schedule
137
+
138
+ rounds_copy = Marshal.load(Marshal.dump(rounds)) #deep clone
139
+ cur_flight_index = i = 0
140
+
141
+ while !rounds_copy.flatten.empty? do
142
+ cur_round = rounds_copy[cur_flight_index].shift
143
+
144
+ #process the next round in the current flight
145
+ if cur_round
146
+ cur_round.games.each do |game|
147
+ unless [game.team_a,game.team_b].include?(:dummy)
148
+ flat_schedule[i][:team_a] = game.team_a
149
+ flat_schedule[i][:team_b] = game.team_b
150
+ i+=1
151
+ end
152
+ end
153
+ end
154
+
155
+
156
+ if cur_flight_index == @flights.size-1
157
+ cur_flight_index = 0
158
+ else
159
+ cur_flight_index += 1
160
+ end
161
+ end
162
+
163
+ #We group our flat schedule by gameday
164
+ s=flat_schedule.group_by{|fs| fs[:gamedate]}.sort
165
+ s.each do |gamedate,gms|
166
+ games = []
167
+ gms.each do |gm|
168
+ games << Game.new(
169
+ :team_a => gm[:team_a],
170
+ :team_b => gm[:team_b],
171
+ :playing_surface => gm[:ps],
172
+ :game_time => gm [:gt]
173
+ )
174
+ end
175
+ self.gamedays << Gameday.new(:date => gamedate, :games => games)
176
+ end
177
+ self.gamedays.each { |gd| gd.games.reject! {|g| g.team_a.nil?}}
178
+ end
179
+
180
+
181
+ def generate_flat_schedule
182
+ flat_schedule = []
183
+ games_left = max_games_per_day = day_game_ctr = rule_ctr = 0
184
+
185
+ #determine first rule based on the nearest gameday
186
+ cur_rule = @rules.select{|r| r.wday >= self.start_date.wday}.first || @rules.first
187
+ cur_rule_index = @rules.index(cur_rule)
188
+ cur_date = next_game_date(self.start_date,cur_rule.wday)
189
+
190
+ @flights.each do |flight|
191
+ games_left += @cycles * (flight.include?(:dummy) ? ((flight.size-1)/2.0)*(flight.size-2) : (flight.size/2)*(flight.size-1))
192
+ max_games_per_day += (flight.include?(:dummy) ? (flight.size-2)/2.0 : (flight.size-1)/2.0).ceil
193
+ end
194
+
195
+ #process all games
196
+ while games_left > 0 do
197
+ cur_rule.gt.each do |gt|
198
+ cur_rule.ps.each do |ps|
199
+
200
+ #if there are more physical resources (playing surfaces and game times) for a given day than
201
+ #we need, we don't use them all (or else some teams would play twice on a single day)
202
+ if day_game_ctr <= max_games_per_day-1
203
+ flat_schedule << {:gamedate => cur_date, :gt => gt, :ps => ps}
204
+ games_left -= 1; day_game_ctr += 1
205
+ end
206
+ end
207
+ end
208
+
209
+ last_rule = cur_rule
210
+ last_date = cur_date
211
+
212
+ #Advance to the next rule (if we're at the last one, we go back to the first)
213
+ cur_rule_index = (cur_rule_index == @rules.size-1) ? 0 : cur_rule_index + 1
214
+ cur_rule = @rules[cur_rule_index]
215
+
216
+ #Go to the next date (except if the new rule is for the same weekday)
217
+ if cur_rule.wday != last_rule.wday || cur_rule_index == 0
218
+ cur_date = next_game_date(cur_date+=1,cur_rule.wday)
219
+ day_game_ctr = 0
218
220
  end
219
-
220
- gameday.games = gameday.games.sort_by {|g| [g.game_time,g.playing_surface]}
221
- self.gamedays << gameday
222
- cur_date += 1
223
221
  end
222
+ flat_schedule
224
223
  end
225
-
224
+
226
225
  #get the next gameday
227
226
  def next_game_date(dt,wday)
228
227
  dt += 1 until wday == dt.wday && !self.exclude_dates.include?(dt)
229
228
  dt
230
229
  end
231
-
232
- #how many games can we play per day?
233
- def games_per_day
234
- if self.teams.size/2 >= (self.playing_surfaces.size * self.game_times.size)
235
- (self.playing_surfaces.size * self.game_times.size)
236
- else
237
- self.optimize ? (self.playing_surfaces.size * self.game_times.size) : self.teams.size/2
230
+
231
+ #return matchups between two teams
232
+ def face_to_face(team_a,team_b)
233
+ res=[]
234
+ self.gamedays.each do |gd|
235
+ res << gd.games.select {|g| (g.team_a == team_a && g.team_b == team_b) || (g.team_a == team_b && g.team_b == team_a)}
238
236
  end
237
+ res.flatten
239
238
  end
240
- end
239
+ end
241
240
 
242
241
  class Gameday
243
242
  attr_accessor :date, :games
244
-
243
+
245
244
  def initialize(params)
246
245
  self.date = params[:date]
247
246
  self.games = params[:games] || []
248
247
  end
249
-
248
+
249
+ end
250
+
251
+ class Rule
252
+ attr_accessor :wday, :gt, :ps
253
+
254
+
255
+ def initialize(params)
256
+ self.wday = params[:wday]
257
+ self.gt = params[:gt]
258
+ self.ps = params[:ps]
259
+ end
260
+
261
+ def wday=(wday)
262
+ @wday = wday ? wday : 1
263
+ raise "Rule#wday must be between 0 and 6" unless (0..6).include?(@wday)
264
+ end
265
+
266
+ #Array of available playing surfaces. You can pass it any kind of object
267
+ def ps=(ps)
268
+ @ps = Array(ps).empty? ? ["Field #1", "Field #2"] : Array(ps)
269
+ end
270
+
271
+ #Array of game times where games are played. Must be valid DateTime objects in the string form
272
+ def gt=(gt)
273
+ @gt = Array(gt).empty? ? ["7:00 PM"] : Array(gt)
274
+ @gt.collect! do |gt|
275
+ begin
276
+ DateTime.parse(gt)
277
+ rescue
278
+ raise "game times must be valid time representations in the string form (e.g. 3:00 PM, 11:00 AM, 18:20, etc)"
279
+ end
280
+ end
281
+ end
282
+
283
+ def <=>(other)
284
+ self.wday == other.wday ?
285
+ DateTime.parse(self.gt.first.to_s) <=> DateTime.parse(other.gt.first.to_s) :
286
+ self.wday <=> other.wday
287
+ end
250
288
  end
251
-
289
+
252
290
  class Game
253
291
  attr_accessor :team_a, :team_b, :playing_surface, :game_time, :game_date
254
292
  alias :ta :team_a
@@ -256,23 +294,37 @@ module RRSchedule
256
294
  alias :ps :playing_surface
257
295
  alias :gt :game_time
258
296
  alias :gd :game_date
259
-
297
+
260
298
  def initialize(params={})
261
299
  self.team_a = params[:team_a]
262
300
  self.team_b = params[:team_b]
263
301
  self.playing_surface = params[:playing_surface]
264
- self.game_time = params[:game_time]
302
+ self.game_time = params[:game_time]
265
303
  self.game_date = params[:game_date]
266
304
  end
267
305
  end
268
-
306
+
269
307
  class Round
270
- attr_accessor :round, :games
271
-
308
+ attr_accessor :round, :games,:flight
309
+
272
310
  def initialize(params={})
273
311
  self.round = params[:round]
312
+ self.flight = params[:flight]
274
313
  self.games = params[:games] || []
275
314
  end
315
+
316
+ def to_s
317
+ str = "FLIGHT #{@flight.to_s} - Round ##{@round.to_s}\n"
318
+ str += "=====================\n"
319
+
320
+ self.games.each do |g|
321
+ if [g.team_a,g.team_b].include?(:dummy)
322
+ str+= g.team_a == :dummy ? g.team_b.to_s : g.team_a.to_s + " has a BYE\n"
323
+ else
324
+ str += g.team_a.to_s + " Vs " + g.team_b.to_s + "\n"
325
+ end
326
+ end
327
+ str += "\n"
328
+ end
276
329
  end
277
330
  end
278
-
@@ -1,163 +1,178 @@
1
1
  require 'helper'
2
-
3
2
  class TestRrschedule < Test::Unit::TestCase
4
- context "A Schedule instance" do
5
- should "have default values for every options" do
6
- schedule = RRSchedule::Schedule.new
7
-
8
- assert schedule.teams.size > 2
9
- assert_equal 1, schedule.cycles
10
- assert schedule.game_times.respond_to?(:to_ary)
11
- assert schedule.playing_surfaces.respond_to?(:to_ary)
12
- assert schedule.start_date.is_a?(Date)
13
- assert schedule.shuffle_initial_order
14
- assert schedule.optimize
15
- assert schedule.wdays.select{|w| (0..6).member? w} == schedule.wdays
16
- assert schedule.exclude_dates.empty?
17
- end
18
-
19
- should "have a dummy team when number of teams is odd" do
20
- schedule = RRSchedule::Schedule.new(:teams => Array(1..9))
21
- assert schedule.teams.size == 10
22
- assert schedule.teams.member?(:dummy), "There should always be a :dummy team when the nbr of teams is odd"
23
- end
24
-
25
- should "not have a dummy team when number of teams is even" do
26
- schedule = RRSchedule::Schedule.new(:teams => Array(1..6))
27
- assert schedule.teams.size == 6
28
- assert !schedule.teams.member?(:dummy), "There should never be a :dummy team when the nbr of teams is even"
29
- end
30
-
31
- should "not have a team named :dummy in the initial array" do
32
- assert_raise RuntimeError do
33
- schedule = RRSchedule::Schedule.new(
34
- :teams => Array(1..4) << :dummy
35
- )
36
- end
3
+ include RRSchedule
4
+ context "new instance without params" do
5
+ setup {@s= Schedule.new}
6
+ should "have default values for some options" do
7
+ assert_equal 1, @s.cycles
8
+ assert @s.shuffle
9
+ assert_equal Date.today, @s.start_date
10
+ assert_equal [], @s.exclude_dates
37
11
  end
38
-
39
- should "not have game times that cannot convert to valid DateTime objects" do
40
- assert_raise RuntimeError do
41
- schedule = RRSchedule::Schedule.new(
42
- :teams => Array(1..4),
43
- :game_times => ["10:00 AM", "13:00", "bonjour"]
44
- )
45
- end
46
- end
47
-
48
- should "not have wdays that are not between 0 and 6" do
49
- assert_raise RuntimeError do
50
- schedule = RRSchedule::Schedule.new(
51
- :wdays => [2,7]
52
- )
53
- end
54
- end
55
-
56
- should "automatically convert game times and playing surface to arrays" do
57
- schedule = RRSchedule::Schedule.new(
58
- :teams => Array(1..4),
59
- :game_times => "10:00 AM",
60
- :playing_surfaces => "the only one"
61
- )
62
-
63
- assert_equal [DateTime.parse("10:00 AM")], schedule.game_times
64
- assert_equal ["the only one"], schedule.playing_surfaces
65
- end
66
-
67
- should "have at least two teams" do
68
- assert_raise RuntimeError do
69
- schedule = RRSchedule::Schedule.new(:teams => [1])
70
- end
12
+ end
13
+
14
+ context "no teams" do
15
+ setup {@s = Schedule.new(:rules => [Rule.new(:wday => 1, :gt => ["7:00PM"], :ps => %w(one two))])}
16
+ should "raise an exception" do
17
+ exception = assert_raise(RuntimeError){@s.generate}
18
+ assert_equal "You need to specify at least 1 team", exception.message
19
+ end
20
+ end
21
+
22
+ context "no flight" do
23
+ setup{@s=Schedule.new(:teams => %w(1 2 3 4 5 6), :rules => some_rules)}
24
+ should "be wrapped into a single flight in the normalized array" do
25
+ @s.generate
26
+ assert_equal [%w(1 2 3 4 5 6)], @s.flights
27
+ end
28
+
29
+ should "not modify the original array" do
30
+ assert_equal %w(1 2 3 4 5 6), @s.teams
31
+ end
32
+ end
33
+
34
+ context "odd number of teams without flight" do
35
+ setup {@s=Schedule.new(:teams => %w(1 2 3 4 5),:rules => some_rules).generate}
36
+ should "add a dummy competitor in the created flight" do
37
+ assert_equal 1, @s.flights.size
38
+ assert_equal 6, @s.flights.first.size
39
+ assert @s.flights.first.include?(:dummy)
40
+ end
41
+
42
+ should "not modify the original array" do
43
+ assert_equal 5, @s.teams.size
44
+ assert !@s.teams.include?(:dummy)
71
45
  end
72
-
73
- should "have default teams if non was specified" do
74
- schedule = RRSchedule::Schedule.new
75
- assert schedule.teams.size > 1
76
- end
77
-
78
- should "not have a team that is specified twice" do
79
- assert_raise RuntimeError do
80
- schedule = RRSchedule::Schedule.new(:teams => %w(a a b c d e f g h i))
81
- end
82
-
83
- end
84
46
  end
85
-
86
- context "Any valid schedule" do
47
+
48
+
49
+ context "extra available resources" do
87
50
  setup do
88
- @s = RRSchedule::Schedule.new(
89
- :teams => %w(a b c d e f g h i j l m),
90
- :playing_surfaces => %w(one two),
91
- :game_times => ["10:00 AM", "13:00 PM"]
92
- )
51
+ @s = Schedule.new(
52
+ :teams => %w(a1 a2 a3 a4 a5),
53
+ :rules => [
54
+ Rule.new(
55
+ :wday => 3,
56
+ :gt => ["7:00PM", "9:00PM"],
57
+ :ps => %w(one two three four)
58
+ )
59
+ ]
60
+ ).generate
93
61
  end
94
62
 
95
- should "have gamedays that respect the wdays attribute" do
96
- @s.wdays = [3,5]
97
- @s.generate
98
-
63
+ should "have a maximum of (teams/2) games per day" do
99
64
  @s.gamedays.each do |gd|
100
- assert [3,5].include?(gd.date.wday), "wday is #{gd.date.wday.to_s} but should be 3 or 5"
101
- end
102
- end
103
-
104
- context "with the option optimize set to true" do
105
- should "have at most (playing_surfaces*game_times) games per gameday" do
106
- @s.generate
107
- assert @s.gamedays.first.games.size == (@s.playing_surfaces.size * @s.game_times.size)
65
+ assert gd.games.size <= @s.teams.size/2
108
66
  end
109
67
  end
110
-
111
- context "with the option optimize set to false" do
112
- setup do
113
- @s.optimize = false
114
- end
115
-
116
- should "never have more than (number of teams / 2) games per gameday" do
117
- @s.teams = %w(only four teams here)
118
- @s.generate
119
- assert @s.gamedays.first.games.size == @s.teams.size / 2
68
+
69
+ should "not have a team that play more than once on a single day" do
70
+ @s.gamedays.each do |gd|
71
+ day_teams = gd.games.collect{|g| [g.team_a,g.team_b]}.flatten
72
+ unique_day_teams = day_teams.uniq
73
+ assert_equal day_teams.size, unique_day_teams.size
120
74
  end
121
75
  end
122
-
123
- context "with an odd number of teams" do
124
- setup do
125
- @s = RRSchedule::Schedule.new(
126
- :teams => %w(a b c d e f g h i j l),
127
- :playing_surfaces => %w(one two),
128
- :game_times => ["10:00 AM", "13:00 PM"]
129
- ).generate
130
- end
131
-
132
- should "be a valid round-robin" do
133
- assert @s.round_robin?
134
- end
135
-
136
- should "not have any :dummy teams in the final schedule" do
137
- assert @s.gamedays.collect{|gd| gd.games}.flatten.select{
138
- |g| [g.team_a,g.team_b].include?(:dummy)
139
- }.size == 0
140
- end
76
+ end
77
+
78
+
79
+ context "multi flights" do
80
+ setup do
81
+ @s = Schedule.new(
82
+ :teams => [
83
+ %w(A1 A2 A3 A4 A5 A6 A7 A8),
84
+ %w(B1 B2 B3 B4 B5 B6 B7 B8),
85
+ %w(C1 C2 C3 C4 C5 C6 C7 C8),
86
+ %w(D1 D2 D3 D4 D5 D6 D7 D8)
87
+ ],
88
+
89
+ :rules => [
90
+ Rule.new(
91
+ :wday => 3,
92
+ :gt => ["7:00PM", "9:00PM"],
93
+ :ps => ["one","two"]
94
+ )
95
+ ],
96
+
97
+ :start_date => Date.parse("2011/01/26"),
98
+ :exclude_dates => [
99
+ Date.parse("2011/02/02")
100
+ ]
101
+ ).generate
102
+ end
103
+
104
+ should "generate separate round-robins" do
105
+ assert_equal 4, @s.flights.size
106
+ 4.times { |i| assert @s.round_robin?(i)}
107
+ end
108
+
109
+ should "have a correct total number of games" do
110
+ assert_equal 112, @s.gamedays.collect{|gd| gd.games.size}.inject{|x,sum| x+sum}
111
+ end
112
+
113
+ should "not have games for a date that is excluded" do
114
+ assert !@s.gamedays.collect{|gd| gd.date}.include?(Date.parse("2011/02/02"))
115
+ assert @s.gamedays.collect{|gd| gd.date}.include?(Date.parse("2011/02/09"))
141
116
  end
142
-
143
- context "with an even number of teams" do
144
- setup do
145
- @s = RRSchedule::Schedule.new(
146
- :teams => %w(a b c d e f g h i j l m),
147
- :playing_surfaces => %w(one two),
148
- :game_times => ["10:00 AM", "13:00 PM"]
149
- ).generate
150
- end
151
-
152
- should "be a valid round-robin" do
153
- assert @s.round_robin?
117
+ end
118
+
119
+ ##### RULES #######
120
+ should "auto create array for gt and ps" do
121
+ @s = Schedule.new(
122
+ :teams => %w(a1 a2 a4 a5),
123
+ :rules => [
124
+ Rule.new(:wday => 1, :gt => "7:00PM", :ps => "The Field")
125
+ ]
126
+ ).generate
127
+
128
+ assert_equal [DateTime.parse("7:00PM")], @s.rules.first.gt
129
+ assert_equal ["The Field"], @s.rules.first.ps
130
+ end
131
+
132
+ context "no rules specified" do
133
+ setup {@s = Schedule.new(:teams => %w(a1 a2 a4 a5))}
134
+ should "raise an exception" do
135
+ exception = assert_raise(RuntimeError){@s.generate}
136
+ assert_equal "You need to specify at least 1 rule", exception.message
137
+ end
138
+ end
139
+
140
+ context "multiple rules on the same weekday" do
141
+ setup do
142
+ @s = Schedule.new
143
+ @s.teams = [%w(a1 a2 a3 a4 a5), %w(b1 b2 b3 b4 b5 b6 b7 b8)]
144
+ @s.rules = [
145
+ Rule.new(:wday => 4, :gt => ["7:00PM"], :ps => %w(field#1 field#2)),
146
+ Rule.new(:wday => 4, :gt => ["9:00PM"], :ps => %w(field#1 field#2 field#3))
147
+ ]
148
+ @s.start_date = Date.parse("2011/01/27")
149
+ @s.generate
150
+ end
151
+
152
+ should "keep games on the same day" do
153
+ cur_date = @s.start_date
154
+ @s.gamedays.each_with_index do |gd,i|
155
+ assert_equal cur_date, gd.date
156
+
157
+ #check all days to make sure that our rules are respected. We don't check
158
+ #the last one because it might not be full (round-robin over)
159
+ if i<@s.gamedays.size-1
160
+ assert_equal 5, gd.games.size
161
+ assert_equal 1, gd.games.select{|g| g.game_time == DateTime.parse("7:00PM") && g.playing_surface == "field#1"}.size
162
+ assert_equal 1, gd.games.select{|g| g.game_time == DateTime.parse("7:00PM") && g.playing_surface == "field#2"}.size
163
+ assert_equal 1, gd.games.select{|g| g.game_time == DateTime.parse("9:00PM") && g.playing_surface == "field#1"}.size
164
+ assert_equal 1, gd.games.select{|g| g.game_time == DateTime.parse("9:00PM") && g.playing_surface == "field#2"}.size
165
+ assert_equal 1, gd.games.select{|g| g.game_time == DateTime.parse("9:00PM") && g.playing_surface == "field#3"}.size
166
+ cur_date += 7
167
+ end
154
168
  end
155
-
156
- should "not have any :dummy teams in the final schedule" do
157
- assert @s.gamedays.collect{|gd| gd.games}.flatten.select{
158
- |g| [g.team_a,g.team_b].include?(:dummy)
159
- }.size == 0
160
- end
161
- end
162
- end
169
+ end
170
+ end
171
+
172
+ def some_rules
173
+ [
174
+ Rule.new(:wday => 1, :gt => "7:00PM", :ps => "one"),
175
+ Rule.new(:wday => 1, :gt => "8:00PM", :ps => %w(one two))
176
+ ]
177
+ end
163
178
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rrschedule
3
3
  version: !ruby/object:Gem::Version
4
- hash: 11
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 8
10
- version: 0.1.8
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - flamontagne
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-11-18 00:00:00 -05:00
18
+ date: 2011-01-25 00:00:00 -05:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -40,14 +40,15 @@ extensions: []
40
40
 
41
41
  extra_rdoc_files:
42
42
  - LICENSE
43
- - README.rdoc
43
+ - README.markdown
44
44
  files:
45
45
  - .document
46
46
  - .gitignore
47
47
  - LICENSE
48
- - README.rdoc
48
+ - README.markdown
49
49
  - Rakefile
50
50
  - VERSION
51
+ - branches_info.markdown
51
52
  - lib/rrschedule.rb
52
53
  - test/helper.rb
53
54
  - test/test_rrschedule.rb
data/README.rdoc DELETED
@@ -1,125 +0,0 @@
1
- = rrschedule
2
-
3
- rrschedule makes it easier to generate round-robin schedules for sport leagues. To generate a schedule, it needs a team list, a season
4
- start date, the day(s) of the week where the games are played and some other options.
5
-
6
- It takes into consideration physical constraints such as the number of playing surfaces availables and game times.
7
- Each round of the round-robin is splitted into multiple gamedays that respect these constraints.
8
-
9
- Say for example that you want to generate a round-robin schedule for your 15-teams volleyball league.
10
- If there are only 3 volleyball fields available and that games are played each monday at 6PM and 8PM, this is technically
11
- impossible to complete one round in a single day. So rrschedule will put the remaining games of this round on the next gameday
12
- and will start a new round right after.
13
-
14
-
15
- == Demo
16
- Online round-robin generator using RRSchedule: http://rrschedule.azanka.ca
17
-
18
- == Installation
19
- gem install rrschedule
20
- require 'rrschedule'
21
-
22
- == Prepare the schedule
23
- teams = ["Rockets","Jetpacks","Snakes","Cobras","Wolves","Huskies","Tigers","Lions",
24
- "Moose","Sprinklers","Pacers","Cyclops","Munchkins","Magicians","French Fries"]
25
-
26
- schedule=RRSchedule::Schedule.new(
27
- #array of teams that will compete against each other in the season
28
- :teams => teams,
29
-
30
- #list of available playing surfaces (volleyball fields, curling sheets, tennis courts, etc)
31
- :playing_surfaces => ["A","B","C","D"],
32
-
33
- #day(s) of the week where games are played
34
- :wdays => [3],
35
-
36
- #Season will start on...
37
- :start_date => Date.parse("2010/10/13"),
38
-
39
- #array of dates WITHOUT games
40
- :exclude_dates => [
41
- Date.parse("2010/11/24"),
42
- Date.parse("2010/12/15"),
43
- Date.parse("2010/12/22"),
44
- Date.parse("2010/12/29")
45
- ],
46
-
47
- #1 for Round Robin, 2 for Double Round Robin and so on. Default is 1
48
- :cycles => 1,
49
-
50
- #Shuffle team order before each cycle. Default is true
51
- :shuffle_initial_order => true,
52
-
53
- #Times of the day where the games are played
54
- :game_times => ["10:00 AM", "1:00 PM"]
55
- )
56
-
57
- == Generate the schedule
58
- schedule.generate
59
-
60
- == Playing with the output
61
-
62
- === human readable schedule
63
- puts schedule.to_s
64
-
65
- === Iterate through schedule
66
- schedule.gamedays.each do |gd|
67
- puts gd.date.strftime("%Y/%m/%d")
68
- puts "===================="
69
- gd.games.each do |g|
70
- puts g.team_a.to_s + " Vs " + g.team_b.to_s + " on playing surface ##{g.playing_surface} at #{g.game_time.strftime("%I:%M %p")}"
71
- end
72
- puts "\n"
73
- end
74
-
75
- === Team schedule
76
- test_team = "Sprinklers"
77
- games=schedule.by_team(test_team)
78
- puts "Schedule for team ##{test_team.to_s}"
79
- games.each do |g|
80
- puts "#{g.game_date.strftime("%Y-%m-%d")}: against #{g.team_a == test_team ? g.team_b.to_s : g.team_a.to_s} on playing surface ##{g.playing_surface} at #{g.game_time.strftime("%I:%M %p")}"
81
- end
82
-
83
- === Face to Face
84
- games=schedule.face_to_face("Lions","Moose")
85
- puts "FACE TO FACE: Lions Vs Moose"
86
- games.each do |g|
87
- puts g.game_date.strftime("%Y/%m/%d") + " on playing surface " + g.playing_surface.to_s + " at " + g.game_time.strftime("%I:%M %p")
88
- end
89
-
90
- === Each round of the roun-robin without any date/time or playing location info
91
- #If you have an ODD number of teams you will see a "dummy" opponent in each round
92
- schedule.rounds.each do |round|
93
- puts "Round ##{round.round}"
94
- round.games.each do |g|
95
- puts g.team_a.to_s + " Vs " + g.team_b.to_s
96
- end
97
- puts "\n"
98
- end
99
-
100
- == Issues / Other
101
-
102
- Starting from version 0.1.5, calling Schedule#gamedays will returns an array of Gameday instances.
103
- If you upgrade to this version you will need to change your code accordingly.
104
-
105
- #this won't work anymore
106
- schedule.gamedays.each do |gd,games|
107
- puts gd
108
-
109
- games.each do |g|
110
- end
111
- #...
112
- end
113
-
114
- #do this instead
115
- schedule.gamedays.each do |gd|
116
- puts gd.date
117
- gd.games.each do |g|
118
- end
119
- #...
120
- end
121
-
122
-
123
- Hope this gem will be useful to some people!
124
-
125
- You can read my blog here: www.rubyfleebie.com