rrschedule 0.1.8 → 0.2.0

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/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