handy_capper 0.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +25 -0
- data/LICENSE.md +20 -0
- data/README.md +115 -0
- data/Rakefile +12 -0
- data/handy_capper.gemspec +21 -0
- data/lib/handy_capper.rb +2 -0
- data/lib/handy_capper/handy_capper.rb +243 -0
- data/lib/handy_capper/version.rb +4 -0
- data/lib/models/preliminary_result.rb +31 -0
- data/test/handy_capper/handy_capper_test.rb +225 -0
- data/test/models/preliminary_result_test.rb +29 -0
- data/test/test_helper.rb +8 -0
- metadata +22 -6
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 'ruby-1.9.2-p180@handy_capper'
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
archive-tar-minitar (0.5.2)
|
5
|
+
columnize (0.3.5)
|
6
|
+
linecache19 (0.5.12)
|
7
|
+
ruby_core_source (>= 0.1.4)
|
8
|
+
minitest (2.8.1)
|
9
|
+
ruby-debug-base19 (0.11.25)
|
10
|
+
columnize (>= 0.3.1)
|
11
|
+
linecache19 (>= 0.5.11)
|
12
|
+
ruby_core_source (>= 0.1.4)
|
13
|
+
ruby-debug19 (0.11.6)
|
14
|
+
columnize (>= 0.3.1)
|
15
|
+
linecache19 (>= 0.5.11)
|
16
|
+
ruby-debug-base19 (>= 0.11.19)
|
17
|
+
ruby_core_source (0.1.5)
|
18
|
+
archive-tar-minitar (>= 0.5.2)
|
19
|
+
|
20
|
+
PLATFORMS
|
21
|
+
ruby
|
22
|
+
|
23
|
+
DEPENDENCIES
|
24
|
+
minitest
|
25
|
+
ruby-debug19
|
data/LICENSE.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Claude Nix
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# HandyCapper
|
2
|
+
This is alpha software. It currently only supports PHRF Time on Distance and
|
3
|
+
PHRF Time on Time scoring.
|
4
|
+
|
5
|
+
## Requirements
|
6
|
+
- Ruby 1.9.2 or greater
|
7
|
+
- minitest gem (if you want to run the test suite)
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
### Calculating corrected time
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class YourApp
|
15
|
+
include HandyCapper
|
16
|
+
|
17
|
+
class Result
|
18
|
+
# installs attr_accessors for required result attributes
|
19
|
+
include HandyCapper::Models::PreliminaryResult
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
result = YourApp::Result.new({
|
24
|
+
rating: 222,
|
25
|
+
start_time: '10:00:00',
|
26
|
+
finish_time: '11:30:30',
|
27
|
+
distance: 10.5
|
28
|
+
})
|
29
|
+
|
30
|
+
# If no options are passed to #phrf, the Time on Distance system will be used
|
31
|
+
result.phrf
|
32
|
+
# => #<Result ...>
|
33
|
+
result.elapsed_time
|
34
|
+
# => '01:30:30'
|
35
|
+
result.corrected_time
|
36
|
+
# => '00:51:39'
|
37
|
+
```
|
38
|
+
|
39
|
+
#### PHRF Time on Time Scoring
|
40
|
+
The PHRF Time on Time scoring method calculates a Time Correction Factor (TCF)
|
41
|
+
which is multiplied by the elapsed time to get 'corrected time'. The TCF is
|
42
|
+
calculated thusly:
|
43
|
+
|
44
|
+
```
|
45
|
+
A
|
46
|
+
TCF = -------------
|
47
|
+
B + Rating
|
48
|
+
```
|
49
|
+
|
50
|
+
Adjusting the _A_ numerator will have no impact on finishing order. It's used
|
51
|
+
to generate a pretty coeffecient. It is set to 650 as a default.
|
52
|
+
|
53
|
+
Adjusting the _B_ denominator will potentially impact finishing order. It's used
|
54
|
+
to represent the conditions for a given race. It is set to 550 as a default, for
|
55
|
+
average conditions. Use 480 for heavy air or all off the wind. Use 600 for light
|
56
|
+
air or all windward work.
|
57
|
+
|
58
|
+
Corrected time is then calculated as:
|
59
|
+
|
60
|
+
```
|
61
|
+
corrected = TCF * elapsed time
|
62
|
+
```
|
63
|
+
|
64
|
+
For more information on PHRF Time on Time scoring, see
|
65
|
+
[http://offshore.ussailing.org/PHRF/Time-On-Time_Scoring.htm][]
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
result.phrf(formula: :tot, a: 650, b: 550)
|
69
|
+
# => #<Result ...>
|
70
|
+
result.elapsed_time
|
71
|
+
# => '01:30:30'
|
72
|
+
result.corrected_time
|
73
|
+
# => '01:16:12'
|
74
|
+
```
|
75
|
+
|
76
|
+
### Scoring a race
|
77
|
+
Now that we can correct times, we need to be able to sort a group of corrected
|
78
|
+
results and apply points. Currently, HandyCapper only supports scoring a
|
79
|
+
single event. Scoring a series, including applying throwouts will be supported
|
80
|
+
in a future release.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
# get some result objects from a database or something
|
84
|
+
results = Result.where('race_id = ?', 1)
|
85
|
+
# => [ #<Result ...>, #<Result ...>]
|
86
|
+
results.score
|
87
|
+
# returns results with position and points set
|
88
|
+
# => [ #<Result ...>, #<Result ...>]
|
89
|
+
results.first.position
|
90
|
+
# => 1
|
91
|
+
results.first.points
|
92
|
+
# => 1
|
93
|
+
```
|
94
|
+
|
95
|
+
|
96
|
+
Run the Tests
|
97
|
+
-------------
|
98
|
+
```bash
|
99
|
+
rake test
|
100
|
+
```
|
101
|
+
|
102
|
+
Utilities
|
103
|
+
---------
|
104
|
+
Launch an irb session with HandyCapper loaded
|
105
|
+
|
106
|
+
```bash
|
107
|
+
rake console
|
108
|
+
```
|
109
|
+
|
110
|
+
Copyright
|
111
|
+
---------
|
112
|
+
Copyright (c) 2011 Claude Nix. See [LICENSE][] for details.
|
113
|
+
|
114
|
+
[license]: https://github.com/cnix/handy_capper/blob/master/LICENSE.md
|
115
|
+
[http://offshore.ussailing.org/PHRF/Time-On-Time_Scoring.htm]: http://offshore.ussailing.org/PHRF/Time-On-Time_Scoring.htm
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
Rake::TestTask.new do |t|
|
4
|
+
t.libs.push "lib"
|
5
|
+
t.test_files = FileList['test/**/*_test.rb']
|
6
|
+
t.verbose = true
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Open an irb session preloaded with this library"
|
10
|
+
task :console do
|
11
|
+
sh "irb -rubygems -r ./lib/handy_capper.rb"
|
12
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/handy_capper/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.add_development_dependency('ruby-debug19')
|
6
|
+
s.add_development_dependency('minitest')
|
7
|
+
s.authors = ["Claude Nix"]
|
8
|
+
s.description = %q{A Ruby library for calculating corrected scores for common sailboat racing scoring systems}
|
9
|
+
s.email = ['claude@seadated.com']
|
10
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.homepage = 'https://github.com/cnix/handy_capper'
|
13
|
+
s.name = 'handy_capper'
|
14
|
+
s.platform = Gem::Platform::RUBY
|
15
|
+
s.require_paths = ['lib']
|
16
|
+
s.required_rubygems_version = Gem::Requirement.new('>= 1.3.6') if s.respond_to? :required_rubygems_version=
|
17
|
+
s.rubyforge_project = s.name
|
18
|
+
s.summary = %q{A Ruby library for calculating corrected scores for common sailboat racing scoring systems}
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.version = HandyCapper::VERSION.dup
|
21
|
+
end
|
data/lib/handy_capper.rb
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
# Public: Various methods for scoring sailing regattas.
|
2
|
+
# All methods are instance methods.
|
3
|
+
# Currently only PHRF Time on Time and PHRF Time on Distance are supported.
|
4
|
+
module HandyCapper
|
5
|
+
|
6
|
+
# Public: Applies position and points to a group of results
|
7
|
+
#
|
8
|
+
# sort - Result attribute to sort by. (default: :corrected_time)
|
9
|
+
# :one_design - sort by elapsed_time
|
10
|
+
# :corrected_time - sort by corrected_time
|
11
|
+
#
|
12
|
+
# Examples
|
13
|
+
#
|
14
|
+
# # get some result objects from a database or something
|
15
|
+
# results = Result.where('race_id = ?', 1)
|
16
|
+
# # => [ #<Result ...>, #<Result ...>]
|
17
|
+
# results.score(:corrected_time)
|
18
|
+
# # returns results with position and points set
|
19
|
+
# # => [ #<Result ...>, #<Result ...>]
|
20
|
+
# results.first.position
|
21
|
+
# # => 1
|
22
|
+
# results.first.points
|
23
|
+
# # => 1
|
24
|
+
#
|
25
|
+
# Returns receiver(Array) with position and points set for each item in the array
|
26
|
+
def score(sort = :corrected_time)
|
27
|
+
sort = (sort == :one_design) ? :elapsed_time : :corrected_time
|
28
|
+
sorted_results = self.sort_by { |h| h[sort] }
|
29
|
+
|
30
|
+
sorted_results.each_with_index do |result, index|
|
31
|
+
result.position = index + 1
|
32
|
+
calculate_points(result, sorted_results.length)
|
33
|
+
end
|
34
|
+
|
35
|
+
sorted_results
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Corects a result with the PHRF scoring system.
|
39
|
+
# See http://http://offshore.ussailing.org/PHRF.htm
|
40
|
+
#
|
41
|
+
# options - Hash for setting a different PHRF scoring method (default: {})
|
42
|
+
# :formula - If you wish to use Time on Time, pass the Symbol :tot
|
43
|
+
# Additionally, you can set the numerator and
|
44
|
+
# denominator for the Time on Time formula by setting
|
45
|
+
# values for :a & :b
|
46
|
+
# :a - Set :a to a Fixnum to set the numerator of the formula
|
47
|
+
# :b - Set :b to a Fixnum to set the denominator of the formula
|
48
|
+
#
|
49
|
+
# Examples
|
50
|
+
#
|
51
|
+
# # Assuming a class named Result in your application
|
52
|
+
# result = Result.new({
|
53
|
+
# rating: 222,
|
54
|
+
# start_time: '10:00:00',
|
55
|
+
# finish_time: '11:30:30',
|
56
|
+
# distance: 10.5
|
57
|
+
# })
|
58
|
+
#
|
59
|
+
# result.phrf
|
60
|
+
# # => #<Result ...>
|
61
|
+
# result.elapsed_time
|
62
|
+
# # => '01:30:30'
|
63
|
+
# result.corrected_time
|
64
|
+
# # => '00:59:50'
|
65
|
+
#
|
66
|
+
# # Using default settings for Time on Time formula
|
67
|
+
# result.phrf(formula: :tot)
|
68
|
+
# # => #<Result ...>
|
69
|
+
# result.corrected_time
|
70
|
+
# # => '01:16:12'
|
71
|
+
#
|
72
|
+
# # Change the denominator to accommodate conditions
|
73
|
+
# result.phrf(formula: :tot, b: 480) # heavy air
|
74
|
+
# # => #<Result ...>
|
75
|
+
# result.corrected_time
|
76
|
+
# # => '01:23:48'
|
77
|
+
#
|
78
|
+
# Returns receiver with elapsed_time and corrected_time set
|
79
|
+
# Raises AttributeError if a required attribute is missing from the receiver
|
80
|
+
def phrf(options={})
|
81
|
+
|
82
|
+
unless rating && start_time && finish_time
|
83
|
+
raise AttributeError, "You're missing a required attribute to process this result"
|
84
|
+
end
|
85
|
+
|
86
|
+
self.elapsed_time = calculate_elapsed_time(self)
|
87
|
+
|
88
|
+
if options[:formula] == :tot
|
89
|
+
a = options[:a] || 650 # generic numerator
|
90
|
+
b = options[:b] || 550 # average conditions
|
91
|
+
ct_in_seconds = score_with_phrf_time_on_time(a,b)
|
92
|
+
else
|
93
|
+
ct_in_seconds = score_with_phrf_time_on_distance
|
94
|
+
end
|
95
|
+
|
96
|
+
self.corrected_time = convert_seconds_to_time(ct_in_seconds)
|
97
|
+
self.elapsed_time = convert_seconds_to_time(self.elapsed_time)
|
98
|
+
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# Internal: Calculate corrected time with PHRF Time on Distance
|
105
|
+
#
|
106
|
+
# result - corrected time in seconds
|
107
|
+
#
|
108
|
+
# Examples
|
109
|
+
#
|
110
|
+
# Result = Struct.new(:elapsed_time, :rating, :distance)
|
111
|
+
# result = Result.new(5400, 222, 10.5)
|
112
|
+
# result.score_with_phrf_time_on_distance
|
113
|
+
# # => 3069
|
114
|
+
#
|
115
|
+
# Returns a Fixnum
|
116
|
+
def score_with_phrf_time_on_distance
|
117
|
+
cf = self.rating
|
118
|
+
et = self.elapsed_time
|
119
|
+
d = self.distance
|
120
|
+
|
121
|
+
(et - (d * cf)).round
|
122
|
+
end
|
123
|
+
|
124
|
+
# Internal: Calculate corrected time in seconds with PHRF Time on Time
|
125
|
+
#
|
126
|
+
# a - Numerator for TOT formula. Does not affect position.
|
127
|
+
# b - Denominator for TOT formula. This one affects position.
|
128
|
+
#
|
129
|
+
# Examples
|
130
|
+
#
|
131
|
+
# Result = Struct.new(:elapsed_time, :rating)
|
132
|
+
# result = Result.new(5400, 222)
|
133
|
+
# result.score_with_phrf_time_on_time(b: 480)
|
134
|
+
# # => 5000
|
135
|
+
# result.score_with_phrf_time_on_time(b: 600)
|
136
|
+
# # => 4270
|
137
|
+
#
|
138
|
+
# Returns a Fixnum representing corrected time in seconds
|
139
|
+
def score_with_phrf_time_on_time(a, b)
|
140
|
+
tcf = a.to_f / ( b.to_f + self.rating.to_f )
|
141
|
+
(self.elapsed_time * tcf).round
|
142
|
+
end
|
143
|
+
|
144
|
+
# Internal: Calculate delta in seconds between two time objects
|
145
|
+
#
|
146
|
+
# result - an object with a start_time and a finish_time attribute
|
147
|
+
#
|
148
|
+
# Examples
|
149
|
+
#
|
150
|
+
# Result = Struct.new(:start_time, :finish_time)
|
151
|
+
# result = Result.new("10:00:00", "11:30:00")
|
152
|
+
# calculate_elapsed_time(result)
|
153
|
+
# # => 5400
|
154
|
+
#
|
155
|
+
# Returns a Fixnum
|
156
|
+
def calculate_elapsed_time(result)
|
157
|
+
Time.parse(result.finish_time).to_i - Time.parse(result.start_time).to_i
|
158
|
+
end
|
159
|
+
|
160
|
+
# Internal: Covert seconds to a string of seconds
|
161
|
+
#
|
162
|
+
# seconds - a Fixnum representing time in seconds
|
163
|
+
#
|
164
|
+
# Examples
|
165
|
+
#
|
166
|
+
# convert_seconds_to_time(5400)
|
167
|
+
# # => '01:30:00'
|
168
|
+
#
|
169
|
+
# Returns a string
|
170
|
+
def convert_seconds_to_time(seconds)
|
171
|
+
Time.at(seconds).gmtime.strftime('%R:%S')
|
172
|
+
end
|
173
|
+
|
174
|
+
# Internal: Calculate the points for a scored result
|
175
|
+
#
|
176
|
+
# result - A Result object
|
177
|
+
# total_results - A Fixnum representing the number of results in the set
|
178
|
+
#
|
179
|
+
# Examples
|
180
|
+
#
|
181
|
+
# first_place_boat = Result.new({
|
182
|
+
# corrected_time: '01:30:41',
|
183
|
+
# position: 1,
|
184
|
+
# code: nil
|
185
|
+
# })
|
186
|
+
# calculate_points(first_place_boat, 10)
|
187
|
+
# # => #<Result ...>
|
188
|
+
# first_place_boat.points
|
189
|
+
# # => 1
|
190
|
+
#
|
191
|
+
# dnf_boat = Result.new({
|
192
|
+
# corrected_time: nil,
|
193
|
+
# position: 10,
|
194
|
+
# code: 'DNF'
|
195
|
+
# })
|
196
|
+
# calculate_points(dnf_boat, 10)
|
197
|
+
# # => #<Result ...>
|
198
|
+
# dnf_boat.points
|
199
|
+
# # => 11
|
200
|
+
#
|
201
|
+
# Returns the receiver with the points attribute set to a Fixnum
|
202
|
+
def calculate_points(result, total_results)
|
203
|
+
if result.code
|
204
|
+
calculate_points_with_penalty(result, total_results)
|
205
|
+
else
|
206
|
+
result.points = result.position
|
207
|
+
end
|
208
|
+
result
|
209
|
+
end
|
210
|
+
|
211
|
+
# Internal: Calculate points based on a penalty code
|
212
|
+
def calculate_points_with_penalty(result, total_results)
|
213
|
+
if ONE_POINT_PENALTY_CODES.include?(result.code)
|
214
|
+
result.points = total_results + 1
|
215
|
+
elsif TWENTY_PERCENT_PENALTY_CODES.include?(result.code)
|
216
|
+
penalty_points = (total_results * 0.20).round
|
217
|
+
if (penalty_points + result.position) > (total_results + 1)
|
218
|
+
result.points = total_results + 1
|
219
|
+
else
|
220
|
+
result.points = result.position + penalty_points
|
221
|
+
end
|
222
|
+
else
|
223
|
+
result.points = result.position
|
224
|
+
end
|
225
|
+
result
|
226
|
+
end
|
227
|
+
|
228
|
+
# Internal: Array of String penalty codes that apply the n + 1 penalty where
|
229
|
+
# n = the total number of entries for a fleet
|
230
|
+
ONE_POINT_PENALTY_CODES = [
|
231
|
+
'DSQ', 'DNS', 'DNC', 'DNF',
|
232
|
+
'OCS', 'BFD', 'DGM', 'DNE',
|
233
|
+
'RAF'
|
234
|
+
]
|
235
|
+
|
236
|
+
# Internal: Array of String penalty codes that apply the 20% penalty
|
237
|
+
TWENTY_PERCENT_PENALTY_CODES = [ 'ZFP', 'SCP' ]
|
238
|
+
|
239
|
+
# Internal: Error that is raised when required attributes are missing from a
|
240
|
+
# receiver for PHRF scoring
|
241
|
+
class AttributeError < StandardError; end
|
242
|
+
|
243
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module HandyCapper
|
2
|
+
# Public: Namespace for model-like modules
|
3
|
+
module Models
|
4
|
+
# Public: Include this module in your result class to get required attributes
|
5
|
+
# for scoring the results. Alternatively, you can alias these to whatever
|
6
|
+
# you have called them in your application
|
7
|
+
module PreliminaryResult
|
8
|
+
|
9
|
+
# Public: Array of Symbols passed attr_accessor on base class
|
10
|
+
DEFAULT_PROPERTIES = [
|
11
|
+
:rating,
|
12
|
+
:start_time,
|
13
|
+
:finish_time,
|
14
|
+
:elapsed_time,
|
15
|
+
:corrected_time,
|
16
|
+
:distance,
|
17
|
+
:penalty,
|
18
|
+
:code,
|
19
|
+
:avg_speed
|
20
|
+
]
|
21
|
+
|
22
|
+
# Public: Installs attributes on a class where this module is included
|
23
|
+
def self.included(base)
|
24
|
+
DEFAULT_PROPERTIES.each do |p|
|
25
|
+
base.send(:attr_accessor, p)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
require 'ruby-debug'
|
3
|
+
|
4
|
+
describe HandyCapper do
|
5
|
+
before do
|
6
|
+
Result = Struct.new(
|
7
|
+
:rating,
|
8
|
+
:start_time,
|
9
|
+
:finish_time,
|
10
|
+
:distance,
|
11
|
+
:elapsed_time,
|
12
|
+
:corrected_time,
|
13
|
+
:position,
|
14
|
+
:points,
|
15
|
+
:code
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
after do
|
20
|
+
# silence warnings for already initialized constant
|
21
|
+
# this could probably be done better if it wasn't a Struct. =\
|
22
|
+
Object.send(:remove_const, :Result)
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#score" do
|
26
|
+
before do
|
27
|
+
@corrected_results = []
|
28
|
+
10.times do
|
29
|
+
result = Result.new
|
30
|
+
# TODO: Address potential race condition
|
31
|
+
time = Time.parse("#{[*0..3].sample}:#{[*0..59].sample}:#{[*0..59].sample}").strftime('%R:%S')
|
32
|
+
result.corrected_time = time
|
33
|
+
@corrected_results << result
|
34
|
+
end
|
35
|
+
|
36
|
+
@one_design_results = []
|
37
|
+
10.times do
|
38
|
+
result = Result.new
|
39
|
+
# TODO: Address potential race condition
|
40
|
+
time = Time.parse("#{[*0..3].sample}:#{[*0..59].sample}:#{[*0..59].sample}").strftime('%R:%S')
|
41
|
+
result.elapsed_time = time
|
42
|
+
@one_design_results << result
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "one design" do
|
47
|
+
it "should sort results by elapsed_time" do
|
48
|
+
scored_results = @one_design_results.score(:one_design)
|
49
|
+
|
50
|
+
previous = '0'
|
51
|
+
scored_results.each do |r|
|
52
|
+
this = r.elapsed_time
|
53
|
+
(this > previous).must_equal true
|
54
|
+
previous = this
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "corrected time" do
|
60
|
+
it "should sort the results by corrected_time" do
|
61
|
+
scored_results = @corrected_results.score
|
62
|
+
|
63
|
+
previous = '0'
|
64
|
+
scored_results.each do |r|
|
65
|
+
this = r.corrected_time
|
66
|
+
(this > previous).must_equal true
|
67
|
+
previous = this
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should add position to the result" do
|
73
|
+
scored_results = @corrected_results.score
|
74
|
+
|
75
|
+
scored_results.each do |r|
|
76
|
+
r.position.wont_be_nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "#phrf" do
|
82
|
+
before do
|
83
|
+
@result = Result.new( 222, '10:00:00', '11:30:30', 10.6)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should set the elapsed time" do
|
87
|
+
@result.phrf.elapsed_time.wont_be_nil
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should set the corrected time" do
|
91
|
+
@result.phrf.corrected_time.wont_be_nil
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should require a rating" do
|
95
|
+
@result.rating = nil
|
96
|
+
-> { @result.phrf }.must_raise AttributeError
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should require a start time" do
|
100
|
+
@result.start_time = nil
|
101
|
+
-> { @result.phrf }.must_raise AttributeError
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should require a finish time" do
|
105
|
+
@result.finish_time = nil
|
106
|
+
-> { @result.phrf }.must_raise AttributeError
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should default to PHRF Time On Distance" do
|
110
|
+
@result.phrf.corrected_time.must_equal '00:51:17'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "#score_with_phrf_time_on_distance" do
|
115
|
+
before do
|
116
|
+
# set elapsed time to seconds so we don't need to convert to test this
|
117
|
+
@result = Result.new(222, '10:00:00', '11:30:30', 10.6, 5430)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should calculate corrected time with Time on Distance formula" do
|
121
|
+
corrected_time_in_seconds = @result.send(:score_with_phrf_time_on_distance)
|
122
|
+
corrected_time_in_seconds.must_be_instance_of Fixnum
|
123
|
+
corrected_time_in_seconds.must_equal 3077
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "#score_with_phrf_time_on_time" do
|
128
|
+
before do
|
129
|
+
@result = Result.new(222, '10:00:00', '11:30:30', nil, 5430)
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should calculate corrected time with Time on Time formula" do
|
133
|
+
corrected_time_in_seconds = @result.send(:score_with_phrf_time_on_time, 650,550)
|
134
|
+
corrected_time_in_seconds.must_be_instance_of Fixnum
|
135
|
+
corrected_time_in_seconds.must_equal 4572
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should allow configuration of time on time numerator" do
|
139
|
+
corrected_time_in_seconds = @result.send(:score_with_phrf_time_on_time, 550,550)
|
140
|
+
corrected_time_in_seconds.must_equal 3869
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should allow configuration of time on time denominator" do
|
144
|
+
corrected_time_in_seconds = @result.send(:score_with_phrf_time_on_time, 650,480)
|
145
|
+
corrected_time_in_seconds.must_equal 5028
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "#calculate_elapsed_time" do
|
150
|
+
before do
|
151
|
+
@result = Result.new( 222, '10:00:00', '11:30:30', 10.6)
|
152
|
+
end
|
153
|
+
|
154
|
+
it "should return a Fixnum representing time in seconds" do
|
155
|
+
seconds = calculate_elapsed_time(@result)
|
156
|
+
seconds.must_be_instance_of Fixnum
|
157
|
+
seconds.must_equal 5430
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "#convert_seconds_to_time" do
|
162
|
+
before do
|
163
|
+
@result = Result.new( 222, '10:00:00', '11:30:30', 10.6)
|
164
|
+
end
|
165
|
+
|
166
|
+
it "should accept a Fixnum" do
|
167
|
+
-> { convert_seconds_to_time("90") }.
|
168
|
+
must_raise TypeError
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should return a Time" do
|
172
|
+
time = convert_seconds_to_time(90)
|
173
|
+
time.must_be_instance_of String
|
174
|
+
time.must_equal "00:01:30"
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe "#calculate_points" do
|
179
|
+
before do
|
180
|
+
@result = Result.new
|
181
|
+
@result.position = 2
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should set the points for a result" do
|
185
|
+
calculate_points(@result, 10)
|
186
|
+
@result.points.must_equal 2
|
187
|
+
end
|
188
|
+
|
189
|
+
describe "penalty scoring" do
|
190
|
+
|
191
|
+
describe "when the penalty code is a n + 1 code" do
|
192
|
+
|
193
|
+
it "should set points based on ISAF penalty code" do
|
194
|
+
['DSQ', 'DNS', 'DNF', 'DNC', 'OCS', 'BFD', 'DGM', 'DNE', 'RAF'].each do |c|
|
195
|
+
@result.code = c
|
196
|
+
calculate_points(@result, 10)
|
197
|
+
@result.points.must_equal 11
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
describe "when the penalty code is a 20% penalty" do
|
204
|
+
|
205
|
+
it "should apply the 20% penalty" do
|
206
|
+
[ 'ZFP', 'SCP' ].each do |c|
|
207
|
+
@result.code = c
|
208
|
+
calculate_points(@result, 10)
|
209
|
+
@result.points.must_equal 4
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should not apply a penalty greater than n + 1" do
|
214
|
+
[ 'ZFP', 'SCP' ].each do |c|
|
215
|
+
@result.code = c
|
216
|
+
@result.position = 10
|
217
|
+
calculate_points(@result, 10)
|
218
|
+
@result.points.must_equal 11
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
|
3
|
+
describe HandyCapper::Models::PreliminaryResult do
|
4
|
+
describe ".included" do
|
5
|
+
before :each do
|
6
|
+
class Result; include HandyCapper::Models::PreliminaryResult; end
|
7
|
+
|
8
|
+
DEFAULT_PROPERTIES = [
|
9
|
+
:rating,
|
10
|
+
:start_time,
|
11
|
+
:finish_time,
|
12
|
+
:elapsed_time,
|
13
|
+
:corrected_time,
|
14
|
+
:distance,
|
15
|
+
:penalty,
|
16
|
+
:code,
|
17
|
+
:avg_speed
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should install the default attributes when included in a class" do
|
22
|
+
result = Result.new
|
23
|
+
|
24
|
+
DEFAULT_PROPERTIES.each do |p|
|
25
|
+
result.must_respond_to p
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: handy_capper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version:
|
5
|
+
version: 0.1.2
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Claude Nix
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-12-
|
13
|
+
date: 2011-12-08 00:00:00 -05:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -44,8 +44,22 @@ extensions: []
|
|
44
44
|
|
45
45
|
extra_rdoc_files: []
|
46
46
|
|
47
|
-
files:
|
48
|
-
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- .rvmrc
|
50
|
+
- Gemfile
|
51
|
+
- Gemfile.lock
|
52
|
+
- LICENSE.md
|
53
|
+
- README.md
|
54
|
+
- Rakefile
|
55
|
+
- handy_capper.gemspec
|
56
|
+
- lib/handy_capper.rb
|
57
|
+
- lib/handy_capper/handy_capper.rb
|
58
|
+
- lib/handy_capper/version.rb
|
59
|
+
- lib/models/preliminary_result.rb
|
60
|
+
- test/handy_capper/handy_capper_test.rb
|
61
|
+
- test/models/preliminary_result_test.rb
|
62
|
+
- test/test_helper.rb
|
49
63
|
has_rdoc: true
|
50
64
|
homepage: https://github.com/cnix/handy_capper
|
51
65
|
licenses: []
|
@@ -74,5 +88,7 @@ rubygems_version: 1.6.2
|
|
74
88
|
signing_key:
|
75
89
|
specification_version: 3
|
76
90
|
summary: A Ruby library for calculating corrected scores for common sailboat racing scoring systems
|
77
|
-
test_files:
|
78
|
-
|
91
|
+
test_files:
|
92
|
+
- test/handy_capper/handy_capper_test.rb
|
93
|
+
- test/models/preliminary_result_test.rb
|
94
|
+
- test/test_helper.rb
|