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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba8948e4e291f5381e58be3bc289ad8f54aa480a1cef744fc121dbc43449b523
4
- data.tar.gz: a9e4b3da8526c245adfaed895f460b820d737ca4a3eb805a4e2c9b1510f1a6ca
3
+ metadata.gz: fb37c0c8cf8bb5944459915e43549229d6db504b77538729cea721fa041f7266
4
+ data.tar.gz: d6900ff2d3cd7e2a821d4637d96beafa69cd8e656a31f2251b68d9e7a818cefb
5
5
  SHA512:
6
- metadata.gz: 61cea9354aa1343b9db15e92a1a11c06f053158ed72a48c5d1062e4b7d733888a7a8eaf3b88c7696cb879b7a92d196891c01e85369947dd4dead497b322b69b9
7
- data.tar.gz: fcf1db5f88479865c65ce64d7c788a3762ea01f39c28f10fa3604e268bce1737589cec4eb8b66f94a10f70a1acb12b7ab73b082990e0bc9a1a0c11d0461159d6
6
+ metadata.gz: 683f9af0342d287411cc79fd6b2acbf3ebae0d2bf408a282f2309b36a6f62e7478d51983d9ab86bb9c5b524ffe55ab0b445ac9c6b212bd628466adba84e0e249
7
+ data.tar.gz: 95286680a44cceede252bb0b9429ba321be66a895246986339dd261af30c0547f8f95726d36bfc85fc2da1e8333fad9141d3836f2659b0237ef39a56ea8f1f6a
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  calcpace-*.gem
2
2
  !calcpace.gemspec
3
+ improvements_plan.md
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.7.0] - Unreleased
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 [![Gem Version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=rb&r=r&ts=1683906897&type=6e&v=1.7.0&x2=0)](https://badge.fury.io/rb/calcpace)
1
+ # Calcpace [![Gem Version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=rb&r=r&ts=1683906897&type=6e&v=1.8.0&x2=0)](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.7.0'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Calcpace
4
- VERSION = '1.7.0'
4
+ VERSION = '1.8.0'
5
5
  end
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.7.0
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: