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.
@@ -0,0 +1,2 @@
1
+ doc/
2
+ pkg/
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 'ruby-1.9.2-p180@handy_capper'
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'minitest'
4
+ gem 'ruby-debug19', group: :development
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -0,0 +1,2 @@
1
+ require File.dirname(__FILE__) + '/handy_capper/handy_capper'
2
+ require File.dirname(__FILE__) + '/models/preliminary_result'
@@ -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,4 @@
1
+ module HandyCapper
2
+ # Public: Version number for this release
3
+ VERSION = '0.1.2'.freeze unless defined?(::HandyCapper::VERSION)
4
+ 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
@@ -0,0 +1,8 @@
1
+ gem 'minitest'
2
+ require 'minitest/spec'
3
+ require 'minitest/autorun'
4
+ require 'minitest/pride'
5
+ require File.dirname(__FILE__) + '/../lib/handy_capper'
6
+
7
+ include HandyCapper
8
+
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: handy_capper
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: "0.1"
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-05 00:00:00 -05:00
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