calcpace 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/tests.yml +13 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +9 -1
- data/CHANGELOG.md +51 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +45 -21
- data/README.md +133 -2
- data/calcpace.gemspec +2 -2
- data/lib/calcpace/calculator.rb +87 -0
- data/lib/calcpace/converter_chain.rb +1 -1
- data/lib/calcpace/pace_calculator.rb +4 -1
- data/lib/calcpace/pace_converter.rb +92 -0
- data/lib/calcpace/race_predictor.rb +126 -0
- data/lib/calcpace/race_splits.rb +188 -0
- data/lib/calcpace/version.rb +1 -1
- data/lib/calcpace.rb +6 -0
- metadata +9 -6
- /data/{Rakefile.rb → Rakefile} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8e3c9a491b95568c00e5fabb1a9ba33be46c07035f72a24e51155b60bd6ecaa
|
|
4
|
+
data.tar.gz: 7c644cc582c27f1204a98bf86238c45b5a236520350c97786d04d119698a2840
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a503fad2dcb76fc61459315f9f6d866dc47cad64fff8459c5ea94ba3b82f22664e2b04a2e7a105de4130e169661de7c01b7b6dc54950bdf2b5179279d30e75d4
|
|
7
|
+
data.tar.gz: 28e0d2c909e0a7c811d0f992a397b63de2be086ed76a64e29839884a5597d6f1ce161dca9b3092a308947fb49c0bf7d7aca4fdfb351c51fe56253a5096e92efa
|
data/.github/workflows/tests.yml
CHANGED
|
@@ -8,12 +8,24 @@ on:
|
|
|
8
8
|
branches: [ "main" ]
|
|
9
9
|
|
|
10
10
|
jobs:
|
|
11
|
+
lint:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout code
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
- name: Set up Ruby
|
|
17
|
+
uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: .ruby-version
|
|
20
|
+
bundler-cache: true
|
|
21
|
+
- name: Run RuboCop
|
|
22
|
+
run: bundle exec rubocop
|
|
11
23
|
test:
|
|
12
24
|
runs-on: ubuntu-latest
|
|
13
25
|
strategy:
|
|
14
26
|
fail-fast: false
|
|
15
27
|
matrix:
|
|
16
|
-
ruby: ['
|
|
28
|
+
ruby: ['3.2', '3.3', '3.4', '4.0']
|
|
17
29
|
steps:
|
|
18
30
|
- name: Checkout code
|
|
19
31
|
uses: actions/checkout@v4
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Following Ruby best practices and style guidelines
|
|
5
5
|
|
|
6
6
|
AllCops:
|
|
7
|
-
TargetRubyVersion: 2
|
|
7
|
+
TargetRubyVersion: 3.2
|
|
8
8
|
NewCops: enable
|
|
9
9
|
Exclude:
|
|
10
10
|
- 'vendor/**/*'
|
|
@@ -55,6 +55,14 @@ Metrics/BlockLength:
|
|
|
55
55
|
- 'test/**/*'
|
|
56
56
|
- '*.gemspec'
|
|
57
57
|
|
|
58
|
+
Metrics/ClassLength:
|
|
59
|
+
Exclude:
|
|
60
|
+
- 'test/**/*'
|
|
61
|
+
|
|
62
|
+
Metrics/ModuleLength:
|
|
63
|
+
Exclude:
|
|
64
|
+
- 'lib/calcpace/race_splits.rb'
|
|
65
|
+
|
|
58
66
|
# Allow both single and double quotes for strings
|
|
59
67
|
Style/StringLiteralsInInterpolation:
|
|
60
68
|
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,57 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [1.
|
|
8
|
+
## [1.8.1] - 2026-03-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- SimpleCov integration for code coverage measurement
|
|
12
|
+
- RuboCop lint job to CI pipeline
|
|
13
|
+
- YARD documentation for all previously undocumented public methods in `Calculator` (`checked_velocity`, `clock_velocity`, `checked_pace`, `clock_pace`, `time`, `checked_time`, `clock_time`, `distance`, `checked_distance`)
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Minimum required Ruby version bumped from 2.7 to 3.2
|
|
17
|
+
- CI matrix updated: removed EOL Ruby versions (2.7, 3.0, 3.1), added Ruby 4.0
|
|
18
|
+
- CI lint job uses `.ruby-version` file instead of a hardcoded version
|
|
19
|
+
- Bundler updated to 4.0.6
|
|
20
|
+
- `Rakefile.rb` renamed to `Rakefile` (standard convention)
|
|
21
|
+
- `PaceConverter` constants `MI_TO_KM` and `KM_TO_MI` consolidated into `Converter::Distance`
|
|
22
|
+
- Negative and positive split calculations refactored to share common logic
|
|
23
|
+
- Test files refactored to inherit from shared `CalcpaceTest` base class
|
|
24
|
+
|
|
25
|
+
## [1.8.0] - 2026-02-14
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Pace conversion module for converting running pace between kilometers and miles
|
|
29
|
+
- `convert_pace` method with support for both symbol and string format conversions
|
|
30
|
+
- `pace_km_to_mi` convenience method for kilometers to miles conversion
|
|
31
|
+
- `pace_mi_to_km` convenience method for miles to kilometers conversion
|
|
32
|
+
- Support for both numeric (seconds) and string (MM:SS) input formats
|
|
33
|
+
- Race splits calculator for pacing strategies
|
|
34
|
+
- `race_splits` method to calculate cumulative split times for races
|
|
35
|
+
- Support for even pace, negative splits (progressive), and positive splits (conservative) strategies
|
|
36
|
+
- Flexible split distances: standard race distances ('5k', '1mile') or custom distances (numeric km)
|
|
37
|
+
- Works with all standard race distances including marathon, half marathon, 10K, 5K, and mile races
|
|
38
|
+
- Race time predictor using Riegel formula
|
|
39
|
+
- `predict_time` and `predict_time_clock` methods to predict race times at different distances
|
|
40
|
+
- `predict_pace` and `predict_pace_clock` methods to calculate predicted pace for target races
|
|
41
|
+
- `equivalent_performance` method to compare performances across different race distances
|
|
42
|
+
- Based on proven Riegel formula: T2 = T1 × (D2/D1)^1.06
|
|
43
|
+
- Detailed explanation of the formula and its applications in README
|
|
44
|
+
- Additional race distances for international races
|
|
45
|
+
- `1mile` - 1.60934 kilometers
|
|
46
|
+
- `5mile` - 8.04672 kilometers
|
|
47
|
+
- `10mile` - 16.0934 kilometers
|
|
48
|
+
- Comprehensive test suites
|
|
49
|
+
- 30+ test cases for pace conversions
|
|
50
|
+
- 30+ test cases for race splits covering all strategies and edge cases
|
|
51
|
+
- 35+ test cases for race predictions covering various scenarios
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- Expanded `RACE_DISTANCES` to include popular US/UK race distances
|
|
55
|
+
- Updated README with pace conversion, race splits, and race prediction examples
|
|
56
|
+
- Improved documentation with practical examples, use cases, and formula explanations
|
|
57
|
+
|
|
58
|
+
## [1.7.0] - Released
|
|
9
59
|
|
|
10
60
|
### Added
|
|
11
61
|
- RuboCop configuration for code style consistency
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,45 +1,68 @@
|
|
|
1
1
|
GEM
|
|
2
2
|
remote: https://rubygems.org/
|
|
3
3
|
specs:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
addressable (2.8.9)
|
|
5
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
6
|
+
ast (2.4.3)
|
|
7
|
+
bigdecimal (4.0.1)
|
|
8
|
+
date (3.5.1)
|
|
9
|
+
docile (1.4.1)
|
|
10
|
+
erb (6.0.2)
|
|
11
|
+
json (2.19.0)
|
|
12
|
+
json-schema (6.2.0)
|
|
13
|
+
addressable (~> 2.8)
|
|
14
|
+
bigdecimal (>= 3.1, < 5)
|
|
15
|
+
language_server-protocol (3.17.0.5)
|
|
8
16
|
lint_roller (1.1.0)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
mcp (0.8.0)
|
|
18
|
+
json-schema (>= 4.1)
|
|
19
|
+
minitest (5.27.0)
|
|
20
|
+
parallel (1.27.0)
|
|
21
|
+
parser (3.3.10.2)
|
|
12
22
|
ast (~> 2.4.1)
|
|
13
23
|
racc
|
|
14
|
-
|
|
24
|
+
prism (1.9.0)
|
|
25
|
+
psych (5.3.1)
|
|
15
26
|
date
|
|
16
27
|
stringio
|
|
28
|
+
public_suffix (7.0.5)
|
|
17
29
|
racc (1.8.1)
|
|
18
30
|
rainbow (3.1.1)
|
|
19
|
-
rake (13.
|
|
20
|
-
rake-compiler (1.
|
|
31
|
+
rake (13.3.1)
|
|
32
|
+
rake-compiler (1.3.1)
|
|
21
33
|
rake
|
|
22
|
-
rdoc (6.
|
|
34
|
+
rdoc (6.17.0)
|
|
35
|
+
erb
|
|
23
36
|
psych (>= 4.0.0)
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
tsort
|
|
38
|
+
regexp_parser (2.11.3)
|
|
39
|
+
rubocop (1.85.1)
|
|
26
40
|
json (~> 2.3)
|
|
27
41
|
language_server-protocol (~> 3.17.0.2)
|
|
28
42
|
lint_roller (~> 1.1.0)
|
|
43
|
+
mcp (~> 0.6)
|
|
29
44
|
parallel (~> 1.10)
|
|
30
45
|
parser (>= 3.3.0.2)
|
|
31
46
|
rainbow (>= 2.2.2, < 4.0)
|
|
32
47
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
33
|
-
rubocop-ast (>= 1.
|
|
48
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
34
49
|
ruby-progressbar (~> 1.7)
|
|
35
50
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
36
|
-
rubocop-ast (1.
|
|
37
|
-
parser (>= 3.3.
|
|
51
|
+
rubocop-ast (1.49.0)
|
|
52
|
+
parser (>= 3.3.7.2)
|
|
53
|
+
prism (~> 1.7)
|
|
38
54
|
ruby-progressbar (1.13.0)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
simplecov (0.22.0)
|
|
56
|
+
docile (~> 1.1)
|
|
57
|
+
simplecov-html (~> 0.11)
|
|
58
|
+
simplecov_json_formatter (~> 0.1)
|
|
59
|
+
simplecov-html (0.13.2)
|
|
60
|
+
simplecov_json_formatter (0.1.4)
|
|
61
|
+
stringio (3.2.0)
|
|
62
|
+
tsort (0.2.0)
|
|
63
|
+
unicode-display_width (3.2.0)
|
|
64
|
+
unicode-emoji (~> 4.1)
|
|
65
|
+
unicode-emoji (4.2.0)
|
|
43
66
|
|
|
44
67
|
PLATFORMS
|
|
45
68
|
ruby
|
|
@@ -50,6 +73,7 @@ DEPENDENCIES
|
|
|
50
73
|
rake-compiler (~> 1.0)
|
|
51
74
|
rdoc (~> 6.2)
|
|
52
75
|
rubocop (~> 1.69)
|
|
76
|
+
simplecov (~> 0.22)
|
|
53
77
|
|
|
54
78
|
BUNDLED WITH
|
|
55
|
-
|
|
79
|
+
4.0.6
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Calcpace [](https://badge.fury.io/rb/calcpace)
|
|
2
2
|
|
|
3
3
|
Calcpace is a Ruby gem designed for calculations and conversions related to distance and time. It can calculate velocity, pace, total time, and distance, accepting time in various formats, including HH:MM:SS. The gem supports conversion to 42 different units, including kilometers, miles, meters, and feet. It also provides methods to validate input.
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ Calcpace is a Ruby gem designed for calculations and conversions related to dist
|
|
|
7
7
|
### Add to your Gemfile
|
|
8
8
|
|
|
9
9
|
```ruby
|
|
10
|
-
gem 'calcpace', '~> 1.
|
|
10
|
+
gem 'calcpace', '~> 1.8.1'
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Then run:
|
|
@@ -176,6 +176,137 @@ Supported race distances:
|
|
|
176
176
|
- `10k` - 10 kilometers
|
|
177
177
|
- `half_marathon` - 21.0975 kilometers
|
|
178
178
|
- `marathon` - 42.195 kilometers
|
|
179
|
+
- `1mile` - 1.60934 kilometers
|
|
180
|
+
- `5mile` - 8.04672 kilometers
|
|
181
|
+
- `10mile` - 16.0934 kilometers
|
|
182
|
+
|
|
183
|
+
### Pace Conversions
|
|
184
|
+
|
|
185
|
+
Convert running pace between kilometers and miles:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
calc = Calcpace.new
|
|
189
|
+
|
|
190
|
+
# Convert pace from km to miles
|
|
191
|
+
calc.pace_km_to_mi('05:00') # => '00:08:02' (5:00/km = 8:02/mi)
|
|
192
|
+
calc.convert_pace('05:00', :km_to_mi) # => '00:08:02' (same as above)
|
|
193
|
+
|
|
194
|
+
# Convert pace from miles to km
|
|
195
|
+
calc.pace_mi_to_km('08:00') # => '00:04:58' (8:00/mi ≈ 4:58/km)
|
|
196
|
+
calc.convert_pace('08:00', :mi_to_km) # => '00:04:58' (same as above)
|
|
197
|
+
|
|
198
|
+
# Works with numeric input (seconds) as well
|
|
199
|
+
calc.pace_km_to_mi(300) # => '00:08:02' (300 seconds/km)
|
|
200
|
+
calc.pace_mi_to_km(480) # => '00:04:58' (480 seconds/mi)
|
|
201
|
+
|
|
202
|
+
# String format also supported
|
|
203
|
+
calc.convert_pace('05:00', 'km to mi') # => '00:08:02'
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The pace conversions are particularly useful when:
|
|
207
|
+
- Planning races in different countries (metric vs imperial)
|
|
208
|
+
- Comparing training paces with international runners
|
|
209
|
+
- Converting workout plans between pace formats
|
|
210
|
+
|
|
211
|
+
### Race Splits
|
|
212
|
+
|
|
213
|
+
Calculate split times for races to help pace your race strategy:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
calc = Calcpace.new
|
|
217
|
+
|
|
218
|
+
# Even pace splits for half marathon (every 5k)
|
|
219
|
+
calc.race_splits('half_marathon', target_time: '01:30:00', split_distance: '5k')
|
|
220
|
+
# => ["00:21:20", "00:42:40", "01:03:59", "01:25:19", "01:30:00"]
|
|
221
|
+
|
|
222
|
+
# Kilometer splits for 10K
|
|
223
|
+
calc.race_splits('10k', target_time: '00:40:00', split_distance: '1k')
|
|
224
|
+
# => ["00:04:00", "00:08:00", "00:12:00", ..., "00:40:00"]
|
|
225
|
+
|
|
226
|
+
# Mile splits for marathon
|
|
227
|
+
calc.race_splits('marathon', target_time: '03:30:00', split_distance: '1mile')
|
|
228
|
+
# => ["00:08:02", "00:16:04", ..., "03:30:00"]
|
|
229
|
+
|
|
230
|
+
# Negative splits strategy (second half faster)
|
|
231
|
+
calc.race_splits('10k', target_time: '00:40:00', split_distance: '5k', strategy: :negative)
|
|
232
|
+
# => ["00:20:48", "00:40:00"] # First 5k slower, second 5k faster
|
|
233
|
+
|
|
234
|
+
# Positive splits strategy (first half faster)
|
|
235
|
+
calc.race_splits('10k', target_time: '00:40:00', split_distance: '5k', strategy: :positive)
|
|
236
|
+
# => ["00:19:12", "00:40:00"] # First 5k faster, second 5k slower
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Supported strategies:
|
|
240
|
+
- `:even` - Constant pace throughout (default)
|
|
241
|
+
- `:negative` - Second half ~4% faster (progressive race)
|
|
242
|
+
- `:positive` - First half ~4% faster (conservative start)
|
|
243
|
+
|
|
244
|
+
Split distances can be:
|
|
245
|
+
- Standard race distances: `'5k'`, `'10k'`, `'1mile'`, etc.
|
|
246
|
+
- Custom distances: `2.5` (in kilometers), `'3k'`, etc.
|
|
247
|
+
|
|
248
|
+
### Race Time Predictions
|
|
249
|
+
|
|
250
|
+
Predict your race times at different distances based on a recent performance using the **Riegel formula**:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
calc = Calcpace.new
|
|
254
|
+
|
|
255
|
+
# Predict marathon time from a 5K result
|
|
256
|
+
calc.predict_time_clock('5k', '00:20:00', 'marathon')
|
|
257
|
+
# => "03:11:49" (predicts 3:11:49 marathon from 20:00 5K)
|
|
258
|
+
|
|
259
|
+
# Predict 10K time from half marathon
|
|
260
|
+
calc.predict_time_clock('half_marathon', '01:30:00', '10k')
|
|
261
|
+
# => "00:40:47"
|
|
262
|
+
|
|
263
|
+
# Get predicted pace for target race
|
|
264
|
+
calc.predict_pace_clock('5k', '00:20:00', 'marathon')
|
|
265
|
+
# => "00:04:32" (4:32/km pace for predicted marathon)
|
|
266
|
+
|
|
267
|
+
# Get complete equivalent performance info
|
|
268
|
+
calc.equivalent_performance('10k', '00:42:00', '5k')
|
|
269
|
+
# => {
|
|
270
|
+
# time: 1209.0,
|
|
271
|
+
# time_clock: "00:20:09",
|
|
272
|
+
# pace: 241.8,
|
|
273
|
+
# pace_clock: "00:04:02"
|
|
274
|
+
# }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### How the Riegel Formula Works
|
|
278
|
+
|
|
279
|
+
The Riegel formula is a mathematical model that predicts race performance across different distances:
|
|
280
|
+
|
|
281
|
+
**Formula:** `T2 = T1 × (D2/D1)^1.06`
|
|
282
|
+
|
|
283
|
+
Where:
|
|
284
|
+
- **T1** = your known time at distance D1
|
|
285
|
+
- **T2** = predicted time at distance D2
|
|
286
|
+
- **D1** = known race distance (in km)
|
|
287
|
+
- **D2** = target race distance (in km)
|
|
288
|
+
- **1.06** = fatigue/endurance factor
|
|
289
|
+
|
|
290
|
+
**The 1.06 exponent** represents how pace slows as distance increases. If endurance was perfect (1.0), doubling distance would simply double time. But in reality, you slow down slightly - the 1.06 factor accounts for this accumulated fatigue.
|
|
291
|
+
|
|
292
|
+
**Example:** From a 5K in 20:00, predict marathon time:
|
|
293
|
+
- T1 = 1200 seconds (20:00)
|
|
294
|
+
- D1 = 5 km
|
|
295
|
+
- D2 = 42.195 km (marathon)
|
|
296
|
+
- T2 = 1200 × (42.195/5)^1.06
|
|
297
|
+
- T2 = 1200 × 9.591 = 11,509 seconds
|
|
298
|
+
- T2 = **3:11:49**
|
|
299
|
+
|
|
300
|
+
The formula works best for:
|
|
301
|
+
- Distances from 5K to marathon
|
|
302
|
+
- Recent race results (within 6-8 weeks)
|
|
303
|
+
- Similar terrain and weather conditions
|
|
304
|
+
- Well-trained runners with consistent pacing
|
|
305
|
+
|
|
306
|
+
**Important notes:**
|
|
307
|
+
- Predictions assume equal training and effort across distances
|
|
308
|
+
- Results are estimates - actual performance varies by individual fitness, training focus, and race conditions
|
|
309
|
+
- The formula is most accurate when predicting between similar distance ranges (e.g., 10K to half marathon)
|
|
179
310
|
|
|
180
311
|
### Other Useful Methods
|
|
181
312
|
|
data/calcpace.gemspec
CHANGED
|
@@ -9,11 +9,11 @@ Gem::Specification.new do |spec|
|
|
|
9
9
|
spec.email = ['joaogilberto@tuta.io']
|
|
10
10
|
|
|
11
11
|
spec.summary = 'A Ruby gem for pace, distance, and time calculations.'
|
|
12
|
-
spec.description = '
|
|
12
|
+
spec.description = 'Ruby gem for pace, distance, time, and speed calculations. Supports multiple units and formats.'
|
|
13
13
|
spec.homepage = 'https://github.com/0jonjo/calcpace'
|
|
14
14
|
spec.metadata['source_code_uri'] = spec.homepage
|
|
15
15
|
spec.license = 'MIT'
|
|
16
|
-
spec.required_ruby_version = '>= 2.
|
|
16
|
+
spec.required_ruby_version = '>= 3.2.0'
|
|
17
17
|
|
|
18
18
|
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
19
19
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
data/lib/calcpace/calculator.rb
CHANGED
|
@@ -21,11 +21,31 @@ module Calculator
|
|
|
21
21
|
distance.to_f / time
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# Calculates velocity from a time string
|
|
25
|
+
#
|
|
26
|
+
# @param time [String] time in HH:MM:SS or MM:SS format
|
|
27
|
+
# @param distance [Numeric] distance in any unit (e.g., meters, kilometers)
|
|
28
|
+
# @return [Float] velocity (distance/time)
|
|
29
|
+
# @raise [Calcpace::InvalidTimeFormatError] if time string is invalid
|
|
30
|
+
# @raise [Calcpace::NonPositiveInputError] if distance is not positive
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# checked_velocity('01:00:00', 10_000) #=> 2.778 (10000 meters / 3600 seconds)
|
|
24
34
|
def checked_velocity(time, distance)
|
|
25
35
|
seconds = convert_to_seconds(validate_time(time))
|
|
26
36
|
velocity(seconds, distance)
|
|
27
37
|
end
|
|
28
38
|
|
|
39
|
+
# Calculates velocity from a time string and returns result as a clock time string
|
|
40
|
+
#
|
|
41
|
+
# @param time [String] time in HH:MM:SS or MM:SS format
|
|
42
|
+
# @param distance [Numeric] distance in any unit
|
|
43
|
+
# @return [String] velocity in HH:MM:SS format
|
|
44
|
+
# @raise [Calcpace::InvalidTimeFormatError] if time string is invalid
|
|
45
|
+
# @raise [Calcpace::NonPositiveInputError] if distance is not positive
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# clock_velocity('01:00:00', 10_000) #=> '00:00:00'
|
|
29
49
|
def clock_velocity(time, distance)
|
|
30
50
|
convert_to_clocktime(checked_velocity(time, distance))
|
|
31
51
|
end
|
|
@@ -44,34 +64,101 @@ module Calculator
|
|
|
44
64
|
time.to_f / distance
|
|
45
65
|
end
|
|
46
66
|
|
|
67
|
+
# Calculates pace from a time string
|
|
68
|
+
#
|
|
69
|
+
# @param time [String] time in HH:MM:SS or MM:SS format
|
|
70
|
+
# @param distance [Numeric] distance in any unit (e.g., kilometers, miles)
|
|
71
|
+
# @return [Float] pace (time/distance) in seconds
|
|
72
|
+
# @raise [Calcpace::InvalidTimeFormatError] if time string is invalid
|
|
73
|
+
# @raise [Calcpace::NonPositiveInputError] if distance is not positive
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# checked_pace('00:50:00', 10) #=> 300.0 (300 seconds/km = 5:00/km)
|
|
47
77
|
def checked_pace(time, distance)
|
|
48
78
|
seconds = convert_to_seconds(validate_time(time))
|
|
49
79
|
pace(seconds, distance)
|
|
50
80
|
end
|
|
51
81
|
|
|
82
|
+
# Calculates pace from a time string and returns result as a clock time string
|
|
83
|
+
#
|
|
84
|
+
# @param time [String] time in HH:MM:SS or MM:SS format
|
|
85
|
+
# @param distance [Numeric] distance in any unit
|
|
86
|
+
# @return [String] pace in HH:MM:SS format
|
|
87
|
+
# @raise [Calcpace::InvalidTimeFormatError] if time string is invalid
|
|
88
|
+
# @raise [Calcpace::NonPositiveInputError] if distance is not positive
|
|
89
|
+
#
|
|
90
|
+
# @example
|
|
91
|
+
# clock_pace('00:50:00', 10) #=> '00:05:00'
|
|
52
92
|
def clock_pace(time, distance)
|
|
53
93
|
convert_to_clocktime(checked_pace(time, distance))
|
|
54
94
|
end
|
|
55
95
|
|
|
96
|
+
# Calculates time given velocity and distance
|
|
97
|
+
#
|
|
98
|
+
# @param velocity [Numeric] velocity in any unit
|
|
99
|
+
# @param distance [Numeric] distance in any unit
|
|
100
|
+
# @return [Float] time (velocity * distance)
|
|
101
|
+
# @raise [Calcpace::NonPositiveInputError] if velocity or distance is not positive
|
|
102
|
+
#
|
|
103
|
+
# @example
|
|
104
|
+
# time(300, 10) #=> 3000.0 (300 s/km * 10 km = 3000 seconds)
|
|
56
105
|
def time(velocity, distance)
|
|
57
106
|
validate_positive({ velocity: velocity, distance: distance })
|
|
58
107
|
velocity * distance
|
|
59
108
|
end
|
|
60
109
|
|
|
110
|
+
# Calculates time from a velocity string and distance
|
|
111
|
+
#
|
|
112
|
+
# @param velocity [String] velocity in HH:MM:SS or MM:SS format
|
|
113
|
+
# @param distance [Numeric] distance in any unit
|
|
114
|
+
# @return [Float] time in seconds
|
|
115
|
+
# @raise [Calcpace::InvalidTimeFormatError] if velocity string is invalid
|
|
116
|
+
# @raise [Calcpace::NonPositiveInputError] if distance is not positive
|
|
117
|
+
#
|
|
118
|
+
# @example
|
|
119
|
+
# checked_time('05:00', 10) #=> 3000.0 (5:00/km * 10 km = 3000 seconds)
|
|
61
120
|
def checked_time(velocity, distance)
|
|
62
121
|
velocity_seconds = convert_to_seconds(validate_time(velocity))
|
|
63
122
|
time(velocity_seconds, distance)
|
|
64
123
|
end
|
|
65
124
|
|
|
125
|
+
# Calculates time from a velocity string and returns result as a clock time string
|
|
126
|
+
#
|
|
127
|
+
# @param velocity [String] velocity in HH:MM:SS or MM:SS format
|
|
128
|
+
# @param distance [Numeric] distance in any unit
|
|
129
|
+
# @return [String] time in HH:MM:SS format
|
|
130
|
+
# @raise [Calcpace::InvalidTimeFormatError] if velocity string is invalid
|
|
131
|
+
# @raise [Calcpace::NonPositiveInputError] if distance is not positive
|
|
132
|
+
#
|
|
133
|
+
# @example
|
|
134
|
+
# clock_time('05:00', 10) #=> '00:50:00'
|
|
66
135
|
def clock_time(velocity, distance)
|
|
67
136
|
convert_to_clocktime(checked_time(velocity, distance))
|
|
68
137
|
end
|
|
69
138
|
|
|
139
|
+
# Calculates distance given time and velocity
|
|
140
|
+
#
|
|
141
|
+
# @param time [Numeric] time in any unit
|
|
142
|
+
# @param velocity [Numeric] velocity in any unit
|
|
143
|
+
# @return [Float] distance (time/velocity)
|
|
144
|
+
# @raise [Calcpace::NonPositiveInputError] if time or velocity is not positive
|
|
145
|
+
#
|
|
146
|
+
# @example
|
|
147
|
+
# distance(3000, 300) #=> 10.0 (3000 seconds / 300 s/km = 10 km)
|
|
70
148
|
def distance(time, velocity)
|
|
71
149
|
validate_positive({ time: time, velocity: velocity })
|
|
72
150
|
time.to_f / velocity
|
|
73
151
|
end
|
|
74
152
|
|
|
153
|
+
# Calculates distance from time and velocity strings
|
|
154
|
+
#
|
|
155
|
+
# @param time [String] time in HH:MM:SS or MM:SS format
|
|
156
|
+
# @param velocity [String] velocity in HH:MM:SS or MM:SS format
|
|
157
|
+
# @return [Float] distance
|
|
158
|
+
# @raise [Calcpace::InvalidTimeFormatError] if any string is invalid
|
|
159
|
+
#
|
|
160
|
+
# @example
|
|
161
|
+
# checked_distance('00:50:00', '05:00') #=> 10.0 (50 min / 5:00/km = 10 km)
|
|
75
162
|
def checked_distance(time, velocity)
|
|
76
163
|
time_seconds = convert_to_seconds(validate_time(time))
|
|
77
164
|
velocity_seconds = convert_to_seconds(validate_time(velocity))
|
|
@@ -39,7 +39,7 @@ module ConverterChain
|
|
|
39
39
|
def convert_chain_with_description(value, conversions)
|
|
40
40
|
initial_value = value
|
|
41
41
|
result = convert_chain(value, conversions)
|
|
42
|
-
conversion_names = conversions.
|
|
42
|
+
conversion_names = conversions.join(' → ')
|
|
43
43
|
description = "#{initial_value} → #{conversion_names} → #{result.round(4)}"
|
|
44
44
|
|
|
45
45
|
{ result: result, description: description }
|
|
@@ -10,7 +10,10 @@ module PaceCalculator
|
|
|
10
10
|
'5k' => 5.0,
|
|
11
11
|
'10k' => 10.0,
|
|
12
12
|
'half_marathon' => 21.0975,
|
|
13
|
-
'marathon' => 42.195
|
|
13
|
+
'marathon' => 42.195,
|
|
14
|
+
'1mile' => 1.60934,
|
|
15
|
+
'5mile' => 8.04672,
|
|
16
|
+
'10mile' => 16.0934
|
|
14
17
|
}.freeze
|
|
15
18
|
|
|
16
19
|
# Calculates the finish time for a race given a pace per kilometer
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Module for converting pace between different distance units
|
|
4
|
+
#
|
|
5
|
+
# This module provides methods to convert running pace between kilometers
|
|
6
|
+
# and miles, maintaining the time per distance unit format.
|
|
7
|
+
module PaceConverter
|
|
8
|
+
# Converts pace from one unit to another
|
|
9
|
+
#
|
|
10
|
+
# @param pace [Numeric, String] pace in seconds per unit or time string (MM:SS)
|
|
11
|
+
# @param conversion [Symbol, String] conversion type (:km_to_mi, :mi_to_km, 'km to mi', 'mi to km')
|
|
12
|
+
# @return [String] converted pace in MM:SS format
|
|
13
|
+
# @raise [ArgumentError] if conversion type is not supported
|
|
14
|
+
# @raise [Calcpace::NonPositiveInputError] if pace is not positive
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# convert_pace('05:00', :km_to_mi) #=> '08:02' (5:00/km = 8:02/mi)
|
|
18
|
+
# convert_pace('08:00', :mi_to_km) #=> '04:58' (8:00/mi ≈ 4:58/km)
|
|
19
|
+
# convert_pace(300, 'km to mi') #=> '08:02' (300s/km = 482s/mi)
|
|
20
|
+
def convert_pace(pace, conversion)
|
|
21
|
+
pace_seconds = pace.is_a?(String) ? convert_to_seconds(pace) : pace
|
|
22
|
+
check_positive(pace_seconds, 'Pace')
|
|
23
|
+
|
|
24
|
+
conversion_type = normalize_conversion(conversion)
|
|
25
|
+
converted_seconds = apply_pace_conversion(pace_seconds, conversion_type)
|
|
26
|
+
|
|
27
|
+
convert_to_clocktime(converted_seconds)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Converts pace from kilometers to miles
|
|
31
|
+
#
|
|
32
|
+
# @param pace_per_km [Numeric, String] pace in seconds per km or time string (MM:SS)
|
|
33
|
+
# @return [String] pace per mile in MM:SS format
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# pace_km_to_mi('05:00') #=> '08:02' (5:00/km = 8:02/mi)
|
|
37
|
+
# pace_km_to_mi(300) #=> '08:02' (300s/km = 482s/mi)
|
|
38
|
+
def pace_km_to_mi(pace_per_km)
|
|
39
|
+
convert_pace(pace_per_km, :km_to_mi)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Converts pace from miles to kilometers
|
|
43
|
+
#
|
|
44
|
+
# @param pace_per_mi [Numeric, String] pace in seconds per mile or time string (MM:SS)
|
|
45
|
+
# @return [String] pace per kilometer in MM:SS format
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# pace_mi_to_km('08:00') #=> '04:58' (8:00/mi ≈ 4:58/km)
|
|
49
|
+
# pace_mi_to_km(480) #=> '04:58' (480s/mi = 298s/km)
|
|
50
|
+
def pace_mi_to_km(pace_per_mi)
|
|
51
|
+
convert_pace(pace_per_mi, :mi_to_km)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Normalizes conversion string/symbol to standard format
|
|
57
|
+
#
|
|
58
|
+
# @param conversion [Symbol, String] conversion type
|
|
59
|
+
# @return [Symbol] normalized conversion symbol
|
|
60
|
+
# @raise [ArgumentError] if conversion type is not supported
|
|
61
|
+
def normalize_conversion(conversion)
|
|
62
|
+
normalized = if conversion.is_a?(String)
|
|
63
|
+
conversion.downcase.gsub(/\s+/, '_').to_sym
|
|
64
|
+
else
|
|
65
|
+
conversion.to_sym
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
unless %i[km_to_mi mi_to_km].include?(normalized)
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"Unsupported pace conversion: #{conversion}. " \
|
|
71
|
+
"Supported conversions: km_to_mi, mi_to_km"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
normalized
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Applies the pace conversion
|
|
78
|
+
#
|
|
79
|
+
# @param pace_seconds [Numeric] pace in seconds
|
|
80
|
+
# @param conversion_type [Symbol] conversion type (:km_to_mi or :mi_to_km)
|
|
81
|
+
# @return [Float] converted pace in seconds
|
|
82
|
+
def apply_pace_conversion(pace_seconds, conversion_type)
|
|
83
|
+
case conversion_type
|
|
84
|
+
when :km_to_mi
|
|
85
|
+
# If running at X seconds per km, pace per mile = X * (miles to km ratio)
|
|
86
|
+
pace_seconds * Converter::Distance::MI_TO_KM
|
|
87
|
+
when :mi_to_km
|
|
88
|
+
# If running at X seconds per mile, pace per km = X / (miles to km ratio)
|
|
89
|
+
pace_seconds * Converter::Distance::KM_TO_MI
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Module for predicting race times based on performance at other distances
|
|
4
|
+
#
|
|
5
|
+
# This module uses the Riegel formula to predict race times based on a known
|
|
6
|
+
# performance at a different distance. The formula accounts for the endurance
|
|
7
|
+
# fatigue factor that occurs as race distance increases.
|
|
8
|
+
module RacePredictor
|
|
9
|
+
# Riegel formula exponent (represents endurance/fatigue factor)
|
|
10
|
+
RIEGEL_EXPONENT = 1.06
|
|
11
|
+
|
|
12
|
+
# Predicts race time for a target distance based on a known performance
|
|
13
|
+
#
|
|
14
|
+
# Uses the Riegel formula: T2 = T1 × (D2/D1)^1.06
|
|
15
|
+
# where:
|
|
16
|
+
# - T1 = time at known distance
|
|
17
|
+
# - D1 = known distance
|
|
18
|
+
# - T2 = predicted time at target distance
|
|
19
|
+
# - D2 = target distance
|
|
20
|
+
# - 1.06 = endurance/fatigue factor (longer races require proportionally more time)
|
|
21
|
+
#
|
|
22
|
+
# @param from_race [String, Symbol] known race distance ('5k', '10k', 'half_marathon', 'marathon', etc.)
|
|
23
|
+
# @param from_time [String, Numeric] time achieved at known distance (HH:MM:SS or seconds)
|
|
24
|
+
# @param to_race [String, Symbol] target race distance to predict
|
|
25
|
+
# @return [Float] predicted time in seconds
|
|
26
|
+
# @raise [ArgumentError] if races are invalid or distances are the same
|
|
27
|
+
#
|
|
28
|
+
# @example Predict marathon time from 5K
|
|
29
|
+
# predict_time('5k', '00:20:00', 'marathon')
|
|
30
|
+
# #=> 11123.4 (approximately 3:05:23)
|
|
31
|
+
#
|
|
32
|
+
# @example Predict 10K time from half marathon
|
|
33
|
+
# predict_time('half_marathon', '01:30:00', '10k')
|
|
34
|
+
# #=> 2565.8 (approximately 42:46)
|
|
35
|
+
def predict_time(from_race, from_time, to_race)
|
|
36
|
+
from_distance = race_distance(from_race)
|
|
37
|
+
to_distance = race_distance(to_race)
|
|
38
|
+
|
|
39
|
+
if from_distance == to_distance
|
|
40
|
+
raise ArgumentError,
|
|
41
|
+
"From and to races must be different distances (both are #{from_distance}km)"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
time_seconds = from_time.is_a?(String) ? convert_to_seconds(from_time) : from_time
|
|
45
|
+
check_positive(time_seconds, 'Time')
|
|
46
|
+
|
|
47
|
+
# Riegel formula: T2 = T1 × (D2/D1)^1.06
|
|
48
|
+
time_seconds * ((to_distance / from_distance)**RIEGEL_EXPONENT)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Predicts race time and returns it as a clock time string
|
|
52
|
+
#
|
|
53
|
+
# @param from_race [String, Symbol] known race distance
|
|
54
|
+
# @param from_time [String, Numeric] time achieved at known distance
|
|
55
|
+
# @param to_race [String, Symbol] target race distance to predict
|
|
56
|
+
# @return [String] predicted time in HH:MM:SS format
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# predict_time_clock('5k', '00:20:00', 'marathon')
|
|
60
|
+
# #=> '03:05:23'
|
|
61
|
+
def predict_time_clock(from_race, from_time, to_race)
|
|
62
|
+
predicted_seconds = predict_time(from_race, from_time, to_race)
|
|
63
|
+
convert_to_clocktime(predicted_seconds)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Predicts the pace per kilometer for a target race
|
|
67
|
+
#
|
|
68
|
+
# @param from_race [String, Symbol] known race distance
|
|
69
|
+
# @param from_time [String, Numeric] time achieved at known distance
|
|
70
|
+
# @param to_race [String, Symbol] target race distance to predict
|
|
71
|
+
# @return [Float] predicted pace in seconds per kilometer
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# predict_pace('5k', '00:20:00', 'marathon')
|
|
75
|
+
# #=> 263.6 (approximately 4:24/km)
|
|
76
|
+
def predict_pace(from_race, from_time, to_race)
|
|
77
|
+
predicted_seconds = predict_time(from_race, from_time, to_race)
|
|
78
|
+
to_distance = race_distance(to_race)
|
|
79
|
+
predicted_seconds / to_distance
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Predicts the pace per kilometer and returns it as a clock time string
|
|
83
|
+
#
|
|
84
|
+
# @param from_race [String, Symbol] known race distance
|
|
85
|
+
# @param from_time [String, Numeric] time achieved at known distance
|
|
86
|
+
# @param to_race [String, Symbol] target race distance to predict
|
|
87
|
+
# @return [String] predicted pace in MM:SS format
|
|
88
|
+
#
|
|
89
|
+
# @example
|
|
90
|
+
# predict_pace_clock('5k', '00:20:00', 'marathon')
|
|
91
|
+
# #=> '00:04:24' (4:24/km)
|
|
92
|
+
def predict_pace_clock(from_race, from_time, to_race)
|
|
93
|
+
pace_seconds = predict_pace(from_race, from_time, to_race)
|
|
94
|
+
convert_to_clocktime(pace_seconds)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Calculates the equivalent performance at a different distance
|
|
98
|
+
#
|
|
99
|
+
# This is useful for comparing performances across different race distances.
|
|
100
|
+
# For example, "My 10K time is equivalent to what 5K time?"
|
|
101
|
+
#
|
|
102
|
+
# @param from_race [String, Symbol] known race distance
|
|
103
|
+
# @param from_time [String, Numeric] time achieved at known distance
|
|
104
|
+
# @param to_race [String, Symbol] target race distance for comparison
|
|
105
|
+
# @return [Hash] hash with :time (seconds), :time_clock (HH:MM:SS), :pace (/km), :pace_clock
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# equivalent_performance('10k', '00:42:00', '5k')
|
|
109
|
+
# #=> {
|
|
110
|
+
# time: 1228.5,
|
|
111
|
+
# time_clock: "00:20:28",
|
|
112
|
+
# pace: 245.7,
|
|
113
|
+
# pace_clock: "00:04:06"
|
|
114
|
+
# }
|
|
115
|
+
def equivalent_performance(from_race, from_time, to_race)
|
|
116
|
+
predicted_time = predict_time(from_race, from_time, to_race)
|
|
117
|
+
predicted_pace = predict_pace(from_race, from_time, to_race)
|
|
118
|
+
|
|
119
|
+
{
|
|
120
|
+
time: predicted_time,
|
|
121
|
+
time_clock: convert_to_clocktime(predicted_time),
|
|
122
|
+
pace: predicted_pace,
|
|
123
|
+
pace_clock: convert_to_clocktime(predicted_pace)
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Module for calculating race splits (partial times)
|
|
4
|
+
#
|
|
5
|
+
# This module provides methods to calculate split times for races,
|
|
6
|
+
# supporting different pacing strategies like even pace, negative splits, etc.
|
|
7
|
+
module RaceSplits
|
|
8
|
+
# Calculates split times for a race
|
|
9
|
+
#
|
|
10
|
+
# @param race [String, Symbol] race distance ('5k', '10k', 'half_marathon', 'marathon', etc.)
|
|
11
|
+
# @param target_time [String] target finish time in HH:MM:SS or MM:SS format
|
|
12
|
+
# @param split_distance [String, Numeric] distance for each split ('5k', '1k', '1mile', or numeric in km)
|
|
13
|
+
# @param strategy [Symbol] pacing strategy - :even (default), :negative, or :positive
|
|
14
|
+
# @return [Array<String>] array of cumulative split times in HH:MM:SS format
|
|
15
|
+
# @raise [ArgumentError] if race or split_distance is invalid
|
|
16
|
+
#
|
|
17
|
+
# @example Even pace splits for half marathon
|
|
18
|
+
# race_splits('half_marathon', target_time: '01:30:00', split_distance: '5k')
|
|
19
|
+
# #=> ["00:21:18", "00:42:35", "01:03:53", "01:30:00"]
|
|
20
|
+
#
|
|
21
|
+
# @example Negative splits (second half faster)
|
|
22
|
+
# race_splits('10k', target_time: '00:40:00', split_distance: '5k', strategy: :negative)
|
|
23
|
+
# #=> ["00:20:48", "00:40:00"] (first 5k slower, second 5k faster)
|
|
24
|
+
def race_splits(race, target_time:, split_distance:, strategy: :even)
|
|
25
|
+
total_distance = race_distance(race)
|
|
26
|
+
target_seconds = target_time.is_a?(String) ? convert_to_seconds(target_time) : target_time
|
|
27
|
+
check_positive(target_seconds, 'Target time')
|
|
28
|
+
|
|
29
|
+
split_km = normalize_split_distance(split_distance)
|
|
30
|
+
validate_split_distance(split_km, total_distance)
|
|
31
|
+
|
|
32
|
+
calculate_splits(total_distance, target_seconds, split_km, strategy)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Normalizes split distance to kilometers
|
|
38
|
+
#
|
|
39
|
+
# @param split_distance [String, Numeric] distance for each split
|
|
40
|
+
# @return [Float] split distance in kilometers
|
|
41
|
+
def normalize_split_distance(split_distance)
|
|
42
|
+
if split_distance.is_a?(Numeric)
|
|
43
|
+
split_distance.to_f
|
|
44
|
+
elsif split_distance.is_a?(String)
|
|
45
|
+
# Try to get from RACE_DISTANCES first (includes standard distances like '5k', '1mile', etc.)
|
|
46
|
+
distance_key = split_distance.to_s.downcase
|
|
47
|
+
|
|
48
|
+
# Check if it's a standard race distance
|
|
49
|
+
begin
|
|
50
|
+
return race_distance(distance_key)
|
|
51
|
+
rescue ArgumentError
|
|
52
|
+
# Not a race distance, try to parse as numeric
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Try to parse as number with optional 'k' or 'km'
|
|
56
|
+
normalized = distance_key.gsub(/km?$/, '').strip
|
|
57
|
+
begin
|
|
58
|
+
Float(normalized)
|
|
59
|
+
rescue StandardError
|
|
60
|
+
raise(ArgumentError, "Invalid split distance: #{split_distance}")
|
|
61
|
+
end
|
|
62
|
+
else
|
|
63
|
+
raise ArgumentError, "Split distance must be a number or string"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Validates that split distance is reasonable for the race
|
|
68
|
+
#
|
|
69
|
+
# @param split_km [Float] split distance in kilometers
|
|
70
|
+
# @param total_distance [Float] total race distance in kilometers
|
|
71
|
+
# @raise [ArgumentError] if split distance is invalid
|
|
72
|
+
def validate_split_distance(split_km, total_distance)
|
|
73
|
+
raise ArgumentError, "Split distance must be positive" if split_km <= 0
|
|
74
|
+
|
|
75
|
+
if split_km > total_distance
|
|
76
|
+
raise ArgumentError,
|
|
77
|
+
"Split distance (#{split_km}km) cannot be greater than race distance (#{total_distance}km)"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Allow some flexibility for non-exact divisions
|
|
81
|
+
return unless (total_distance / split_km) > 100
|
|
82
|
+
|
|
83
|
+
raise ArgumentError,
|
|
84
|
+
"Split distance too small - would generate more than 100 splits"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Calculates split times based on strategy
|
|
88
|
+
#
|
|
89
|
+
# @param total_distance [Float] total race distance in kilometers
|
|
90
|
+
# @param target_seconds [Float] target finish time in seconds
|
|
91
|
+
# @param split_km [Float] split distance in kilometers
|
|
92
|
+
# @param strategy [Symbol] pacing strategy
|
|
93
|
+
# @return [Array<String>] array of cumulative split times
|
|
94
|
+
def calculate_splits(total_distance, target_seconds, split_km, strategy)
|
|
95
|
+
case strategy
|
|
96
|
+
when :even
|
|
97
|
+
calculate_even_splits(total_distance, target_seconds, split_km)
|
|
98
|
+
when :negative
|
|
99
|
+
calculate_negative_splits(total_distance, target_seconds, split_km)
|
|
100
|
+
when :positive
|
|
101
|
+
calculate_positive_splits(total_distance, target_seconds, split_km)
|
|
102
|
+
else
|
|
103
|
+
raise ArgumentError,
|
|
104
|
+
"Unknown strategy: #{strategy}. Supported strategies: :even, :negative, :positive"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Calculates even pace splits (constant pace throughout)
|
|
109
|
+
#
|
|
110
|
+
# @param total_distance [Float] total race distance in kilometers
|
|
111
|
+
# @param target_seconds [Float] target finish time in seconds
|
|
112
|
+
# @param split_km [Float] split distance in kilometers
|
|
113
|
+
# @return [Array<String>] array of cumulative split times
|
|
114
|
+
def calculate_even_splits(total_distance, target_seconds, split_km)
|
|
115
|
+
pace_per_km = target_seconds / total_distance
|
|
116
|
+
splits = []
|
|
117
|
+
distance_covered = 0.0
|
|
118
|
+
|
|
119
|
+
while distance_covered < total_distance - 0.001 # small tolerance for floating point
|
|
120
|
+
distance_covered += split_km
|
|
121
|
+
# Don't exceed total distance
|
|
122
|
+
distance_covered = total_distance if distance_covered > total_distance
|
|
123
|
+
|
|
124
|
+
split_time = (distance_covered * pace_per_km).round
|
|
125
|
+
splits << convert_to_clocktime(split_time)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
splits
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Calculates negative splits (second half faster than first half)
|
|
132
|
+
# First half is ~4% slower, second half is ~4% faster
|
|
133
|
+
#
|
|
134
|
+
# @param total_distance [Float] total race distance in kilometers
|
|
135
|
+
# @param target_seconds [Float] target finish time in seconds
|
|
136
|
+
# @param split_km [Float] split distance in kilometers
|
|
137
|
+
# @return [Array<String>] array of cumulative split times
|
|
138
|
+
def calculate_negative_splits(total_distance, target_seconds, split_km)
|
|
139
|
+
calculate_variable_splits(total_distance, target_seconds, split_km, first_factor: 1.04, second_factor: 0.96)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Calculates positive splits (first half faster than second half)
|
|
143
|
+
# First half is ~4% faster, second half is ~4% slower
|
|
144
|
+
#
|
|
145
|
+
# @param total_distance [Float] total race distance in kilometers
|
|
146
|
+
# @param target_seconds [Float] target finish time in seconds
|
|
147
|
+
# @param split_km [Float] split distance in kilometers
|
|
148
|
+
# @return [Array<String>] array of cumulative split times
|
|
149
|
+
def calculate_positive_splits(total_distance, target_seconds, split_km)
|
|
150
|
+
calculate_variable_splits(total_distance, target_seconds, split_km, first_factor: 0.96, second_factor: 1.04)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Shared logic for variable pace split strategies
|
|
154
|
+
#
|
|
155
|
+
# @param total_distance [Float] total race distance in kilometers
|
|
156
|
+
# @param target_seconds [Float] target finish time in seconds
|
|
157
|
+
# @param split_km [Float] split distance in kilometers
|
|
158
|
+
# @param first_factor [Float] pace multiplier for the first half
|
|
159
|
+
# @param second_factor [Float] pace multiplier for the second half
|
|
160
|
+
# @return [Array<String>] array of cumulative split times
|
|
161
|
+
def calculate_variable_splits(total_distance, target_seconds, split_km, first_factor:, second_factor:)
|
|
162
|
+
half_distance = total_distance / 2.0
|
|
163
|
+
avg_pace = target_seconds / total_distance
|
|
164
|
+
first_half_pace = avg_pace * first_factor
|
|
165
|
+
second_half_pace = avg_pace * second_factor
|
|
166
|
+
|
|
167
|
+
splits = []
|
|
168
|
+
distance_covered = 0.0
|
|
169
|
+
cumulative_time = 0.0
|
|
170
|
+
|
|
171
|
+
while distance_covered < total_distance - 0.001
|
|
172
|
+
distance_covered += split_km
|
|
173
|
+
distance_covered = total_distance if distance_covered > total_distance
|
|
174
|
+
|
|
175
|
+
if distance_covered <= half_distance
|
|
176
|
+
cumulative_time = distance_covered * first_half_pace
|
|
177
|
+
else
|
|
178
|
+
time_at_halfway = half_distance * first_half_pace
|
|
179
|
+
distance_in_second_half = distance_covered - half_distance
|
|
180
|
+
cumulative_time = time_at_halfway + (distance_in_second_half * second_half_pace)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
splits << convert_to_clocktime(cumulative_time.round)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
splits
|
|
187
|
+
end
|
|
188
|
+
end
|
data/lib/calcpace/version.rb
CHANGED
data/lib/calcpace.rb
CHANGED
|
@@ -6,6 +6,9 @@ require_relative 'calcpace/converter'
|
|
|
6
6
|
require_relative 'calcpace/converter_chain'
|
|
7
7
|
require_relative 'calcpace/errors'
|
|
8
8
|
require_relative 'calcpace/pace_calculator'
|
|
9
|
+
require_relative 'calcpace/pace_converter'
|
|
10
|
+
require_relative 'calcpace/race_predictor'
|
|
11
|
+
require_relative 'calcpace/race_splits'
|
|
9
12
|
|
|
10
13
|
# Calcpace - A Ruby gem for pace, distance, and time calculations
|
|
11
14
|
#
|
|
@@ -34,6 +37,9 @@ class Calcpace
|
|
|
34
37
|
include Converter
|
|
35
38
|
include ConverterChain
|
|
36
39
|
include PaceCalculator
|
|
40
|
+
include PaceConverter
|
|
41
|
+
include RacePredictor
|
|
42
|
+
include RaceSplits
|
|
37
43
|
|
|
38
44
|
# Creates a new Calcpace instance
|
|
39
45
|
#
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: calcpace
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- João Gilberto Saraiva
|
|
@@ -9,8 +9,8 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description:
|
|
13
|
-
|
|
12
|
+
description: Ruby gem for pace, distance, time, and speed calculations. Supports multiple
|
|
13
|
+
units and formats.
|
|
14
14
|
email:
|
|
15
15
|
- joaogilberto@tuta.io
|
|
16
16
|
executables: []
|
|
@@ -26,7 +26,7 @@ files:
|
|
|
26
26
|
- Gemfile
|
|
27
27
|
- Gemfile.lock
|
|
28
28
|
- README.md
|
|
29
|
-
- Rakefile
|
|
29
|
+
- Rakefile
|
|
30
30
|
- calcpace.gemspec
|
|
31
31
|
- lib/calcpace.rb
|
|
32
32
|
- lib/calcpace/calculator.rb
|
|
@@ -35,6 +35,9 @@ files:
|
|
|
35
35
|
- lib/calcpace/converter_chain.rb
|
|
36
36
|
- lib/calcpace/errors.rb
|
|
37
37
|
- lib/calcpace/pace_calculator.rb
|
|
38
|
+
- lib/calcpace/pace_converter.rb
|
|
39
|
+
- lib/calcpace/race_predictor.rb
|
|
40
|
+
- lib/calcpace/race_splits.rb
|
|
38
41
|
- lib/calcpace/version.rb
|
|
39
42
|
homepage: https://github.com/0jonjo/calcpace
|
|
40
43
|
licenses:
|
|
@@ -50,14 +53,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
50
53
|
requirements:
|
|
51
54
|
- - ">="
|
|
52
55
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 2.
|
|
56
|
+
version: 3.2.0
|
|
54
57
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
58
|
requirements:
|
|
56
59
|
- - ">="
|
|
57
60
|
- !ruby/object:Gem::Version
|
|
58
61
|
version: '0'
|
|
59
62
|
requirements: []
|
|
60
|
-
rubygems_version:
|
|
63
|
+
rubygems_version: 4.0.6
|
|
61
64
|
specification_version: 4
|
|
62
65
|
summary: A Ruby gem for pace, distance, and time calculations.
|
|
63
66
|
test_files: []
|
/data/{Rakefile.rb → Rakefile}
RENAMED
|
File without changes
|