calcpace 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +34 -1
- data/README.md +133 -2
- data/lib/calcpace/pace_calculator.rb +4 -1
- data/lib/calcpace/pace_converter.rb +96 -0
- data/lib/calcpace/race_predictor.rb +126 -0
- data/lib/calcpace/race_splits.rb +208 -0
- data/lib/calcpace/version.rb +1 -1
- data/lib/calcpace.rb +6 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb37c0c8cf8bb5944459915e43549229d6db504b77538729cea721fa041f7266
|
|
4
|
+
data.tar.gz: d6900ff2d3cd7e2a821d4637d96beafa69cd8e656a31f2251b68d9e7a818cefb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 683f9af0342d287411cc79fd6b2acbf3ebae0d2bf408a282f2309b36a6f62e7478d51983d9ab86bb9c5b524ffe55ab0b445ac9c6b212bd628466adba84e0e249
|
|
7
|
+
data.tar.gz: 95286680a44cceede252bb0b9429ba321be66a895246986339dd261af30c0547f8f95726d36bfc85fc2da1e8333fad9141d3836f2659b0237ef39a56ea8f1f6a
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,40 @@ 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.0] - 2026-02-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Pace conversion module for converting running pace between kilometers and miles
|
|
12
|
+
- `convert_pace` method with support for both symbol and string format conversions
|
|
13
|
+
- `pace_km_to_mi` convenience method for kilometers to miles conversion
|
|
14
|
+
- `pace_mi_to_km` convenience method for miles to kilometers conversion
|
|
15
|
+
- Support for both numeric (seconds) and string (MM:SS) input formats
|
|
16
|
+
- Race splits calculator for pacing strategies
|
|
17
|
+
- `race_splits` method to calculate cumulative split times for races
|
|
18
|
+
- Support for even pace, negative splits (progressive), and positive splits (conservative) strategies
|
|
19
|
+
- Flexible split distances: standard race distances ('5k', '1mile') or custom distances (numeric km)
|
|
20
|
+
- Works with all standard race distances including marathon, half marathon, 10K, 5K, and mile races
|
|
21
|
+
- Race time predictor using Riegel formula
|
|
22
|
+
- `predict_time` and `predict_time_clock` methods to predict race times at different distances
|
|
23
|
+
- `predict_pace` and `predict_pace_clock` methods to calculate predicted pace for target races
|
|
24
|
+
- `equivalent_performance` method to compare performances across different race distances
|
|
25
|
+
- Based on proven Riegel formula: T2 = T1 × (D2/D1)^1.06
|
|
26
|
+
- Detailed explanation of the formula and its applications in README
|
|
27
|
+
- Additional race distances for international races
|
|
28
|
+
- `1mile` - 1.60934 kilometers
|
|
29
|
+
- `5mile` - 8.04672 kilometers
|
|
30
|
+
- `10mile` - 16.0934 kilometers
|
|
31
|
+
- Comprehensive test suites
|
|
32
|
+
- 30+ test cases for pace conversions
|
|
33
|
+
- 30+ test cases for race splits covering all strategies and edge cases
|
|
34
|
+
- 35+ test cases for race predictions covering various scenarios
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Expanded `RACE_DISTANCES` to include popular US/UK race distances
|
|
38
|
+
- Updated README with pace conversion, race splits, and race prediction examples
|
|
39
|
+
- Improved documentation with practical examples, use cases, and formula explanations
|
|
40
|
+
|
|
41
|
+
## [1.7.0] - Released
|
|
9
42
|
|
|
10
43
|
### Added
|
|
11
44
|
- RuboCop configuration for code style consistency
|
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.0'
|
|
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
|
|
|
@@ -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,96 @@
|
|
|
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
|
+
# Conversion factor: 1 mile = 1.60934 kilometers
|
|
9
|
+
MI_TO_KM = 1.60934
|
|
10
|
+
KM_TO_MI = 0.621371
|
|
11
|
+
|
|
12
|
+
# Converts pace from one unit to another
|
|
13
|
+
#
|
|
14
|
+
# @param pace [Numeric, String] pace in seconds per unit or time string (MM:SS)
|
|
15
|
+
# @param conversion [Symbol, String] conversion type (:km_to_mi, :mi_to_km, 'km to mi', 'mi to km')
|
|
16
|
+
# @return [String] converted pace in MM:SS format
|
|
17
|
+
# @raise [ArgumentError] if conversion type is not supported
|
|
18
|
+
# @raise [Calcpace::NonPositiveInputError] if pace is not positive
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# convert_pace('05:00', :km_to_mi) #=> '08:02' (5:00/km = 8:02/mi)
|
|
22
|
+
# convert_pace('08:00', :mi_to_km) #=> '04:58' (8:00/mi ≈ 4:58/km)
|
|
23
|
+
# convert_pace(300, 'km to mi') #=> '08:02' (300s/km = 482s/mi)
|
|
24
|
+
def convert_pace(pace, conversion)
|
|
25
|
+
pace_seconds = pace.is_a?(String) ? convert_to_seconds(pace) : pace
|
|
26
|
+
check_positive(pace_seconds, 'Pace')
|
|
27
|
+
|
|
28
|
+
conversion_type = normalize_conversion(conversion)
|
|
29
|
+
converted_seconds = apply_pace_conversion(pace_seconds, conversion_type)
|
|
30
|
+
|
|
31
|
+
convert_to_clocktime(converted_seconds)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Converts pace from kilometers to miles
|
|
35
|
+
#
|
|
36
|
+
# @param pace_per_km [Numeric, String] pace in seconds per km or time string (MM:SS)
|
|
37
|
+
# @return [String] pace per mile in MM:SS format
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# pace_km_to_mi('05:00') #=> '08:02' (5:00/km = 8:02/mi)
|
|
41
|
+
# pace_km_to_mi(300) #=> '08:02' (300s/km = 482s/mi)
|
|
42
|
+
def pace_km_to_mi(pace_per_km)
|
|
43
|
+
convert_pace(pace_per_km, :km_to_mi)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Converts pace from miles to kilometers
|
|
47
|
+
#
|
|
48
|
+
# @param pace_per_mi [Numeric, String] pace in seconds per mile or time string (MM:SS)
|
|
49
|
+
# @return [String] pace per kilometer in MM:SS format
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# pace_mi_to_km('08:00') #=> '04:58' (8:00/mi ≈ 4:58/km)
|
|
53
|
+
# pace_mi_to_km(480) #=> '04:58' (480s/mi = 298s/km)
|
|
54
|
+
def pace_mi_to_km(pace_per_mi)
|
|
55
|
+
convert_pace(pace_per_mi, :mi_to_km)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Normalizes conversion string/symbol to standard format
|
|
61
|
+
#
|
|
62
|
+
# @param conversion [Symbol, String] conversion type
|
|
63
|
+
# @return [Symbol] normalized conversion symbol
|
|
64
|
+
# @raise [ArgumentError] if conversion type is not supported
|
|
65
|
+
def normalize_conversion(conversion)
|
|
66
|
+
normalized = if conversion.is_a?(String)
|
|
67
|
+
conversion.downcase.gsub(/\s+/, '_').to_sym
|
|
68
|
+
else
|
|
69
|
+
conversion.to_sym
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
unless %i[km_to_mi mi_to_km].include?(normalized)
|
|
73
|
+
raise ArgumentError,
|
|
74
|
+
"Unsupported pace conversion: #{conversion}. " \
|
|
75
|
+
"Supported conversions: km_to_mi, mi_to_km"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
normalized
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Applies the pace conversion
|
|
82
|
+
#
|
|
83
|
+
# @param pace_seconds [Numeric] pace in seconds
|
|
84
|
+
# @param conversion_type [Symbol] conversion type (:km_to_mi or :mi_to_km)
|
|
85
|
+
# @return [Float] converted pace in seconds
|
|
86
|
+
def apply_pace_conversion(pace_seconds, conversion_type)
|
|
87
|
+
case conversion_type
|
|
88
|
+
when :km_to_mi
|
|
89
|
+
# If running at X seconds per km, pace per mile = X * (miles to km ratio)
|
|
90
|
+
pace_seconds * MI_TO_KM
|
|
91
|
+
when :mi_to_km
|
|
92
|
+
# If running at X seconds per mile, pace per km = X / (miles to km ratio)
|
|
93
|
+
pace_seconds * KM_TO_MI
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
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,208 @@
|
|
|
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
|
+
half_distance = total_distance / 2.0
|
|
140
|
+
avg_pace = target_seconds / total_distance
|
|
141
|
+
|
|
142
|
+
# First half: 4% slower, second half: 4% faster
|
|
143
|
+
first_half_pace = avg_pace * 1.04
|
|
144
|
+
second_half_pace = avg_pace * 0.96
|
|
145
|
+
|
|
146
|
+
splits = []
|
|
147
|
+
distance_covered = 0.0
|
|
148
|
+
cumulative_time = 0.0
|
|
149
|
+
|
|
150
|
+
while distance_covered < total_distance - 0.001
|
|
151
|
+
distance_covered += split_km
|
|
152
|
+
distance_covered = total_distance if distance_covered > total_distance
|
|
153
|
+
|
|
154
|
+
if distance_covered <= half_distance
|
|
155
|
+
# First half
|
|
156
|
+
cumulative_time = distance_covered * first_half_pace
|
|
157
|
+
else
|
|
158
|
+
# Second half
|
|
159
|
+
time_at_halfway = half_distance * first_half_pace
|
|
160
|
+
distance_in_second_half = distance_covered - half_distance
|
|
161
|
+
cumulative_time = time_at_halfway + (distance_in_second_half * second_half_pace)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
splits << convert_to_clocktime(cumulative_time.round)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
splits
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Calculates positive splits (first half faster than second half)
|
|
171
|
+
# First half is ~4% faster, second half is ~4% slower
|
|
172
|
+
#
|
|
173
|
+
# @param total_distance [Float] total race distance in kilometers
|
|
174
|
+
# @param target_seconds [Float] target finish time in seconds
|
|
175
|
+
# @param split_km [Float] split distance in kilometers
|
|
176
|
+
# @return [Array<String>] array of cumulative split times
|
|
177
|
+
def calculate_positive_splits(total_distance, target_seconds, split_km)
|
|
178
|
+
half_distance = total_distance / 2.0
|
|
179
|
+
avg_pace = target_seconds / total_distance
|
|
180
|
+
|
|
181
|
+
# First half: 4% faster, second half: 4% slower
|
|
182
|
+
first_half_pace = avg_pace * 0.96
|
|
183
|
+
second_half_pace = avg_pace * 1.04
|
|
184
|
+
|
|
185
|
+
splits = []
|
|
186
|
+
distance_covered = 0.0
|
|
187
|
+
cumulative_time = 0.0
|
|
188
|
+
|
|
189
|
+
while distance_covered < total_distance - 0.001
|
|
190
|
+
distance_covered += split_km
|
|
191
|
+
distance_covered = total_distance if distance_covered > total_distance
|
|
192
|
+
|
|
193
|
+
if distance_covered <= half_distance
|
|
194
|
+
# First half
|
|
195
|
+
cumulative_time = distance_covered * first_half_pace
|
|
196
|
+
else
|
|
197
|
+
# Second half
|
|
198
|
+
time_at_halfway = half_distance * first_half_pace
|
|
199
|
+
distance_in_second_half = distance_covered - half_distance
|
|
200
|
+
cumulative_time = time_at_halfway + (distance_in_second_half * second_half_pace)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
splits << convert_to_clocktime(cumulative_time.round)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
splits
|
|
207
|
+
end
|
|
208
|
+
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.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- João Gilberto Saraiva
|
|
@@ -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:
|