calcpace 1.9.10 → 1.11.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/.rubocop.yml +6 -0
- data/CHANGELOG.md +66 -1
- data/README.md +57 -5
- data/calcpace.gemspec +5 -4
- data/lib/calcpace/age_grading.rb +39 -17
- data/lib/calcpace/converter.rb +44 -13
- data/lib/calcpace/errors.rb +20 -4
- data/lib/calcpace/pace_calculator.rb +13 -5
- data/lib/calcpace/training_zones.rb +184 -0
- data/lib/calcpace/version.rb +1 -1
- data/lib/calcpace/vo2max_estimator.rb +20 -6
- data/lib/calcpace.rb +2 -0
- metadata +10 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8bc8b750605e9b531f01c27f0839668a04f9acf1d0c45316cd8860761afffbae
|
|
4
|
+
data.tar.gz: ceb6a60b435735552b2fd7eb0e78ec131c73a9faa3ef22a84ce59dda20d6d375
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b76dea0d30d9a98b6fa46364a1750f1d6051b342ae723205cf3e1b4560e3fc5ef568703df87c70cb74826434eedc78bcaa2d2cc57354b259521a371ecf9fd51
|
|
7
|
+
data.tar.gz: def67fc24bbab2e8d7275f2db2d9a2b8f724e9690b1a3bf40d2d329de919db1c6bf35dd37e2878b775ff943026a372b0eda3a5f27302ca980645ccf7e3c3f6f7
|
data/.rubocop.yml
CHANGED
|
@@ -50,6 +50,12 @@ Metrics/AbcSize:
|
|
|
50
50
|
Exclude:
|
|
51
51
|
- 'test/**/*'
|
|
52
52
|
|
|
53
|
+
# Keyword arguments are self-documenting, so the ceiling is a little higher than
|
|
54
|
+
# the default 5 — but it stays a ceiling: kwargs still count, so a method growing
|
|
55
|
+
# to eight or ten of them is flagged.
|
|
56
|
+
Metrics/ParameterLists:
|
|
57
|
+
Max: 6
|
|
58
|
+
|
|
53
59
|
Metrics/BlockLength:
|
|
54
60
|
Exclude:
|
|
55
61
|
- 'test/**/*'
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.11.0] - 2026-07-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Training zones improvements
|
|
14
|
+
- `training_paces` and `training_paces_from_race` accept `unit: :mi` for
|
|
15
|
+
pace bands per mile (default remains `:km`)
|
|
16
|
+
- `hr_zones_from_max(hr_max:)`: five heart-rate zones from maximum heart
|
|
17
|
+
rate only (%HRmax method) — fallback when resting heart rate is unknown
|
|
18
|
+
- `training_paces_from_race` accepts standard race names ('10k', 'marathon',
|
|
19
|
+
'5mile', ...) in addition to numeric kilometres, matching `predict_time`
|
|
20
|
+
and `race_pace`
|
|
21
|
+
- `distance_unit: :mi` keyword on `estimate_vo2max`, `estimate_detailed_vo2max`,
|
|
22
|
+
`age_grade`, `age_grade_percent`, and `training_paces_from_race` — numeric
|
|
23
|
+
distance inputs can now be given in miles (default remains kilometres)
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- `training_paces_from_race` resolves non-numeric distances as race names. Strings
|
|
27
|
+
that v1.10.0 silently parsed with `to_f` change meaning: `'5mile'` was 5.0 km and
|
|
28
|
+
is now the 5-mile standard distance (8.04672 km). Numeric strings (`'10'`,
|
|
29
|
+
`'21.0975'`) keep working as before, in kilometres.
|
|
30
|
+
- `training_paces_from_race` with an unparseable distance (`nil`, `'banana'`) now
|
|
31
|
+
raises `ArgumentError` ("Unknown race: ...") instead of
|
|
32
|
+
`Calcpace::NonPositiveInputError`.
|
|
33
|
+
- Unknown `unit:` / `distance_unit:` values raise `Calcpace::UnsupportedUnitError`
|
|
34
|
+
(inherits from `Calcpace::Error`) instead of `ArgumentError`. Unit keywords are
|
|
35
|
+
now case-insensitive (`'MI'` works) and `nil` raises the same error instead of a
|
|
36
|
+
`NoMethodError`.
|
|
37
|
+
- `unit: :mi` pace bands are computed natively per mile instead of being converted
|
|
38
|
+
from the km bands, so they can differ by ±1 s from `pace_km_to_mi(km_band)` —
|
|
39
|
+
the native value is the one without double rounding.
|
|
40
|
+
- Every mile-based factor now derives from the exact international mile
|
|
41
|
+
(1 mi = 1609.344 m), which was previously truncated to 1.60934 in some places
|
|
42
|
+
and exact in others. Affected values move by ~2.5e-6 relative:
|
|
43
|
+
`convert(1, :mi_to_km)` 1.60934 → 1.609344, `convert(1, :km_to_mi)` 0.621371 →
|
|
44
|
+
0.6213711922…, `convert(1, :mi_to_meters)` 1609.34 → 1609.344, the `mi_h`/`m_s`
|
|
45
|
+
speed pairs, and `list_races` entries `'1mile'` (1.609344) and `'10mile'`
|
|
46
|
+
(16.09344). Age-grading tolerance and pace bands now agree on mile length.
|
|
47
|
+
- Passing `distance_unit:` together with a race name (`training_paces_from_race('10k',
|
|
48
|
+
t, distance_unit: :mi)`, `age_grade('10k', …, distance_unit: :mi)`) raises
|
|
49
|
+
`ArgumentError` instead of silently ignoring the keyword — a standard race already
|
|
50
|
+
carries its own distance.
|
|
51
|
+
- Race-name lookup is normalized in one place: `' 10K '` and `:MARATHON` now resolve
|
|
52
|
+
everywhere (previously `PaceCalculator` did not strip whitespace), and `AgeGrading`
|
|
53
|
+
uses the same "Unknown race: …" message wording as the rest of the gem.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
- Age grading accepts mile distances as runners write them (`3.1`, `6.2`, `13.1`,
|
|
57
|
+
`26.2` with `distance_unit: :mi`); the previous 0.001 km match window only
|
|
58
|
+
accepted 6-decimal conversions.
|
|
59
|
+
- Unsupported age-grading distances report the input in the unit it was given
|
|
60
|
+
instead of always labelling it "km".
|
|
61
|
+
- `estimate_detailed_vo2max` rejects a non-positive distance even when
|
|
62
|
+
`elevation_gain_m` is positive (the elevation adjustment used to mask it).
|
|
63
|
+
|
|
64
|
+
## [1.10.0] - 2026-07-11
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
- Training Zones module (`TrainingZones`)
|
|
68
|
+
- `training_paces(vo2max)`: personalized Easy/Marathon/Threshold/Interval/Repetition
|
|
69
|
+
pace bands per km, inverting the Daniels & Gilbert velocity equation
|
|
70
|
+
- `training_paces_from_race(distance_km, time)`: pace bands straight from a race result
|
|
71
|
+
- `hr_zones(hr_max:, hr_rest:)`: five Karvonen (Heart Rate Reserve) heart-rate zones
|
|
72
|
+
- Structured results (`PaceBand`, `HrZone`) with seconds and clock formats
|
|
73
|
+
|
|
10
74
|
## [1.9.10] - 2026-07-09
|
|
11
75
|
|
|
12
76
|
### Changed
|
|
@@ -205,7 +269,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
205
269
|
|
|
206
270
|
See git history for changes in earlier versions.
|
|
207
271
|
|
|
208
|
-
[Unreleased]: https://github.com/0jonjo/calcpace/compare/v1.
|
|
272
|
+
[Unreleased]: https://github.com/0jonjo/calcpace/compare/v1.10.0...HEAD
|
|
273
|
+
[1.10.0]: https://github.com/0jonjo/calcpace/compare/v1.9.10...v1.10.0
|
|
209
274
|
[1.9.6]: https://github.com/0jonjo/calcpace/compare/v1.9.5...v1.9.6
|
|
210
275
|
[1.9.5]: https://github.com/0jonjo/calcpace/compare/v1.9.4...v1.9.5
|
|
211
276
|
[1.6.0]: https://github.com/0jonjo/calcpace/releases/tag/v1.6.0
|
data/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Calcpace [](https://badge.fury.io/rb/calcpace)
|
|
2
2
|
|
|
3
|
-
A Ruby gem for
|
|
3
|
+
A Ruby gem for runners: pace, time, and distance calculations, unit conversions, race predictions, GPS track analysis, age grading, VO2max estimation, and training zones.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
|
-
gem 'calcpace', '~> 1.
|
|
8
|
+
gem 'calcpace', '~> 1.10.0'
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
@@ -76,8 +76,8 @@ calc.predict_time_cameron_adjusted('10k', '00:40:00', 'marathon', temperature: 8
|
|
|
76
76
|
30+ units supported. String or symbol format:
|
|
77
77
|
|
|
78
78
|
```ruby
|
|
79
|
-
calc.convert(10, :km_to_mi) # => 6.
|
|
80
|
-
calc.convert(10, 'mi to km') # => 16.
|
|
79
|
+
calc.convert(10, :km_to_mi) # => 6.213711922...
|
|
80
|
+
calc.convert(10, 'mi to km') # => 16.09344
|
|
81
81
|
calc.convert(1, :m_s_to_km_h) # => 3.6
|
|
82
82
|
|
|
83
83
|
# Chain conversions
|
|
@@ -169,6 +169,7 @@ age factors and open standards.
|
|
|
169
169
|
|
|
170
170
|
```ruby
|
|
171
171
|
result = calc.age_grade(10.0, '00:45:00', age: 55, sex: :male)
|
|
172
|
+
# numeric distances also accepted in miles: calc.age_grade(6.21371, '00:45:00', age: 55, sex: :male, distance_unit: :mi)
|
|
172
173
|
# => {
|
|
173
174
|
# age_grade_percent: 64.6,
|
|
174
175
|
# category: "Local Class",
|
|
@@ -208,6 +209,7 @@ Estimate aerobic fitness from a race result using the **Daniels & Gilbert formul
|
|
|
208
209
|
calc.estimate_vo2max(10.0, '00:40:00') # => 51.9 ml/kg/min
|
|
209
210
|
calc.estimate_vo2max(42.195, '03:30:00') # => 44.8
|
|
210
211
|
calc.estimate_vo2max(5.0, 2400) # also accepts total seconds
|
|
212
|
+
calc.estimate_vo2max(6.21371, '00:40:00', distance_unit: :mi) # => 51.9 (miles input)
|
|
211
213
|
|
|
212
214
|
calc.vo2max_label(51.9) # => "Very Good"
|
|
213
215
|
```
|
|
@@ -276,6 +278,51 @@ easy.value # => 29.3 (underestimates real aerobic capacity)
|
|
|
276
278
|
|
|
277
279
|
---
|
|
278
280
|
|
|
281
|
+
### Training Zones
|
|
282
|
+
|
|
283
|
+
Personalized training paces (Daniels' Running Formula) and Karvonen heart-rate zones:
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
zones = calc.training_paces(50.0)
|
|
287
|
+
zones[:threshold].fast_clock # => "00:04:15" per km
|
|
288
|
+
zones[:easy].slow_clock # => "00:05:52" per km
|
|
289
|
+
|
|
290
|
+
calc.training_paces(50.0, unit: :mi)[:threshold].fast_clock # => "00:06:51" per mile
|
|
291
|
+
|
|
292
|
+
calc.training_paces_from_race(10.0, '00:40:00') # from a recent race result
|
|
293
|
+
calc.training_paces_from_race('5mile', '00:35:00', unit: :mi) # race names work too
|
|
294
|
+
calc.training_paces_from_race(6.2, '00:40:00', distance_unit: :mi, unit: :mi) # race distance in miles
|
|
295
|
+
|
|
296
|
+
calc.hr_zones(hr_max: 190, hr_rest: 55)
|
|
297
|
+
# => [#<struct zone=1, min_bpm=123, max_bpm=136>, ... zone=5, max_bpm=190]
|
|
298
|
+
|
|
299
|
+
calc.hr_zones_from_max(hr_max: 190)
|
|
300
|
+
# => [#<struct zone=1, min_bpm=95, max_bpm=114>, ... zone=5, max_bpm=190]
|
|
301
|
+
# %HRmax fallback — prefer hr_zones (Karvonen) when resting HR is known
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
| Zone | %VO2max | Purpose |
|
|
305
|
+
|------|---------|---------|
|
|
306
|
+
| Easy | 59–74% | Base building, recovery |
|
|
307
|
+
| Marathon | 75–84% | Marathon race pace |
|
|
308
|
+
| Threshold | 83–88% | Lactate threshold, tempo runs |
|
|
309
|
+
| Interval | 95–100% | VO2max development |
|
|
310
|
+
| Repetition | 105–110% | Speed and running economy |
|
|
311
|
+
|
|
312
|
+
Pace accuracy vs published VDOT tables: within a few seconds per km
|
|
313
|
+
(threshold matches exactly; easy band is a range heuristic).
|
|
314
|
+
|
|
315
|
+
`unit:` sets the unit of the returned pace bands; `distance_unit:` sets the unit of a
|
|
316
|
+
numeric race distance you pass in. Combining `distance_unit:` with a race name raises
|
|
317
|
+
`ArgumentError` — `'10k'` already carries its own distance. Mile bands are computed
|
|
318
|
+
natively (not converted from the km bands), so they can differ by ±1 s from
|
|
319
|
+
`pace_km_to_mi(km_band)`.
|
|
320
|
+
|
|
321
|
+
All mile factors derive from the exact international mile (1609.344 m), so distances,
|
|
322
|
+
pace bands, and age-grading tolerances agree to the metre.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
279
326
|
### Other Utilities
|
|
280
327
|
|
|
281
328
|
```ruby
|
|
@@ -292,6 +339,11 @@ All errors inherit from `Calcpace::Error`:
|
|
|
292
339
|
|
|
293
340
|
- `Calcpace::NonPositiveInputError` — numeric input is zero or negative
|
|
294
341
|
- `Calcpace::InvalidTimeFormatError` — time string not in `HH:MM:SS` or `MM:SS` format
|
|
342
|
+
- `Calcpace::UnsupportedUnitError` — unknown conversion (`convert`) or unknown
|
|
343
|
+
`unit:` / `distance_unit:` keyword
|
|
344
|
+
|
|
345
|
+
Argument validation that is not about units or numbers raises a plain `ArgumentError`:
|
|
346
|
+
unknown race names, unsupported age-grading distances, and invalid `age` / `sex` values.
|
|
295
347
|
|
|
296
348
|
---
|
|
297
349
|
|
data/calcpace.gemspec
CHANGED
|
@@ -8,11 +8,12 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ['João Gilberto Saraiva']
|
|
9
9
|
spec.email = ['joaogilberto@tuta.io']
|
|
10
10
|
|
|
11
|
-
spec.summary = '
|
|
12
|
-
spec.description = 'Ruby gem for
|
|
13
|
-
'unit conversions (30+ units), race predictions (Riegel & Cameron), ' \
|
|
11
|
+
spec.summary = 'Running calculations: pace, race predictions, GPS track analysis, VO2max, and training zones.'
|
|
12
|
+
spec.description = 'Ruby gem for runners: pace, time, and distance calculations, ' \
|
|
13
|
+
'unit conversions (30+ units), race time predictions (Riegel & Cameron), ' \
|
|
14
14
|
'GPS track analysis (Haversine, elevation gain, per-km splits), ' \
|
|
15
|
-
'
|
|
15
|
+
'age grading (WMA 2023), VO2max estimation (Daniels & Gilbert), and ' \
|
|
16
|
+
'personalized training zones (Daniels paces & Karvonen heart-rate zones).'
|
|
16
17
|
spec.homepage = 'https://github.com/0jonjo/calcpace'
|
|
17
18
|
spec.metadata['source_code_uri'] = spec.homepage
|
|
18
19
|
spec.license = 'MIT'
|
data/lib/calcpace/age_grading.rb
CHANGED
|
@@ -49,14 +49,22 @@ module AgeGrading
|
|
|
49
49
|
|
|
50
50
|
# Returns a full age-grading report for a race performance
|
|
51
51
|
#
|
|
52
|
-
# @param
|
|
53
|
-
# (5.0, 10.0, 21.0975, 42.195) or race key (:5k, :10k, :half_marathon, :marathon)
|
|
52
|
+
# @param distance [Numeric, String, Symbol] race distance in kilometres
|
|
53
|
+
# (5.0, 10.0, 21.0975, 42.195) or race key (:5k, :10k, :half_marathon, :marathon);
|
|
54
|
+
# numeric input can also be given in miles via distance_unit: :mi
|
|
54
55
|
# @param time [String, Numeric] performance time as HH:MM:SS / MM:SS, or total seconds
|
|
55
56
|
# @param age [Integer] athlete age (must be >= 18)
|
|
56
57
|
# @param sex [String, Symbol] male or female
|
|
58
|
+
# @param distance_unit [Symbol] unit of a numeric distance input — :km (default) or :mi.
|
|
59
|
+
# Rejected when distance is a race key: standard races already carry their own
|
|
60
|
+
# distance, so the combination is always a caller mistake
|
|
57
61
|
# @return [Hash] age-grading result details
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
# @raise [ArgumentError] if the distance, race key, age or sex is not supported,
|
|
63
|
+
# or if distance_unit is combined with a race key
|
|
64
|
+
# @raise [Calcpace::UnsupportedUnitError] if distance_unit is not :km or :mi
|
|
65
|
+
# @raise [Calcpace::InvalidTimeFormatError] if time string is malformed
|
|
66
|
+
def age_grade(distance, time, age:, sex:, distance_unit: nil)
|
|
67
|
+
distance_m = normalize_distance(distance, distance_unit)
|
|
60
68
|
seconds = parse_time_seconds(time)
|
|
61
69
|
age_value = normalize_age(age)
|
|
62
70
|
sex_value = normalize_sex(sex)
|
|
@@ -83,13 +91,15 @@ module AgeGrading
|
|
|
83
91
|
|
|
84
92
|
# Returns only the age-grade percentage
|
|
85
93
|
#
|
|
86
|
-
# @param
|
|
94
|
+
# @param distance [Numeric, String, Symbol] race distance in kilometres or race key
|
|
87
95
|
# @param time [String, Numeric] performance time
|
|
88
96
|
# @param age [Integer] athlete age
|
|
89
97
|
# @param sex [String, Symbol] male or female
|
|
98
|
+
# @param distance_unit [Symbol] unit of a numeric distance input — :km (default) or :mi
|
|
99
|
+
# (rejected alongside race keys, see #age_grade)
|
|
90
100
|
# @return [Float] age-grade percentage
|
|
91
|
-
def age_grade_percent(
|
|
92
|
-
age_grade(
|
|
101
|
+
def age_grade_percent(distance, time, age:, sex:, distance_unit: nil)
|
|
102
|
+
age_grade(distance, time, age: age, sex: sex, distance_unit: distance_unit)[:age_grade_percent]
|
|
93
103
|
end
|
|
94
104
|
|
|
95
105
|
# Returns a descriptive label for an age-grade percentage
|
|
@@ -110,23 +120,35 @@ module AgeGrading
|
|
|
110
120
|
|
|
111
121
|
private
|
|
112
122
|
|
|
113
|
-
def normalize_distance(
|
|
114
|
-
if
|
|
115
|
-
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
raise ArgumentError,
|
|
119
|
-
"Unsupported race '#{distance_km}'. Supported: #{RACE_TO_METERS.keys.join(', ')}"
|
|
123
|
+
def normalize_distance(distance_input, distance_unit = nil)
|
|
124
|
+
if distance_input.is_a?(String) || distance_input.is_a?(Symbol)
|
|
125
|
+
reject_distance_unit_with_race_name!(distance_unit, distance_input)
|
|
126
|
+
return race_key_to_meters(distance_input)
|
|
120
127
|
end
|
|
121
128
|
|
|
122
|
-
distance =
|
|
129
|
+
distance = normalize_distance_km(distance_input, distance_unit || :km)
|
|
123
130
|
check_positive(distance, 'Distance')
|
|
124
131
|
|
|
125
|
-
match = SUPPORTED_DISTANCES_KM.find { |value| (distance
|
|
132
|
+
match = SUPPORTED_DISTANCES_KM.find { |value| standard_distance?(distance, value) }
|
|
126
133
|
return DISTANCE_TO_METERS.fetch(match) if match
|
|
127
134
|
|
|
128
135
|
raise ArgumentError,
|
|
129
|
-
"Unsupported distance #{
|
|
136
|
+
"Unsupported distance #{distance_input}#{(distance_unit || :km).to_s.downcase}. " \
|
|
137
|
+
"Supported: #{SUPPORTED_DISTANCES_KM.join(', ')} km"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def race_key_to_meters(race_input)
|
|
141
|
+
# normalize_race_key is PaceCalculator's — one lookup convention gem-wide
|
|
142
|
+
RACE_TO_METERS.fetch(normalize_race_key(race_input)) do
|
|
143
|
+
raise ArgumentError,
|
|
144
|
+
"Unknown race: #{race_input}. Available races: #{RACE_TO_METERS.keys.join(', ')}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Runners write rounded distances (3.1 mi, 13.1 mi, 26.2 mi), so the match
|
|
149
|
+
# window is relative — 0.5% of the standard distance, never below 1 metre
|
|
150
|
+
def standard_distance?(distance, standard)
|
|
151
|
+
(distance - standard).abs <= [0.001, standard * 0.005].max
|
|
130
152
|
end
|
|
131
153
|
|
|
132
154
|
def parse_time_seconds(time)
|
data/lib/calcpace/converter.rb
CHANGED
|
@@ -7,14 +7,18 @@
|
|
|
7
7
|
# speed units (m/s, km/h, mi/h, knots, etc.).
|
|
8
8
|
module Converter
|
|
9
9
|
module Distance
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
# One international mile is exactly 1609.344 m. Every mile-based factor
|
|
11
|
+
# derives from this single value so that no two call sites — a pace band, a
|
|
12
|
+
# distance conversion, an age-grading tolerance — can disagree about how
|
|
13
|
+
# long a mile is.
|
|
14
|
+
MI_TO_KM = 1.609344
|
|
15
|
+
KM_TO_MI = 1 / MI_TO_KM
|
|
12
16
|
NAUTICAL_MI_TO_KM = 1.852
|
|
13
17
|
KM_TO_NAUTICAL_MI = 0.539957
|
|
14
18
|
METERS_TO_KM = 0.001
|
|
15
19
|
KM_TO_METERS = 1000
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
MI_TO_METERS = MI_TO_KM * 1000
|
|
21
|
+
METERS_TO_MI = 1 / MI_TO_METERS
|
|
18
22
|
METERS_TO_FEET = 3.28084
|
|
19
23
|
FEET_TO_METERS = 0.3048
|
|
20
24
|
METERS_TO_YARDS = 1.09361
|
|
@@ -28,30 +32,30 @@ module Converter
|
|
|
28
32
|
KM_TO_INCHES = 39_370.1
|
|
29
33
|
INCHES_TO_KM = 0.0000254
|
|
30
34
|
MI_TO_YARDS = 1760
|
|
31
|
-
YARDS_TO_MI = 0
|
|
35
|
+
YARDS_TO_MI = 1.0 / MI_TO_YARDS
|
|
32
36
|
MI_TO_FEET = 5280
|
|
33
|
-
FEET_TO_MI = 0
|
|
37
|
+
FEET_TO_MI = 1.0 / MI_TO_FEET
|
|
34
38
|
MI_TO_INCHES = 63_360
|
|
35
|
-
INCHES_TO_MI = 0
|
|
39
|
+
INCHES_TO_MI = 1.0 / MI_TO_INCHES
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
module Speed
|
|
39
43
|
M_S_TO_KM_H = 3.6
|
|
40
44
|
KM_H_TO_M_S = 0.277778
|
|
41
|
-
M_S_TO_MI_H =
|
|
42
|
-
MI_H_TO_M_S =
|
|
45
|
+
M_S_TO_MI_H = 3.6 / Distance::MI_TO_KM
|
|
46
|
+
MI_H_TO_M_S = Distance::MI_TO_METERS / 3600
|
|
43
47
|
M_S_TO_NAUTICAL_MI_H = 1.94384
|
|
44
48
|
NAUTICAL_MI_H_TO_M_S = 0.514444
|
|
45
49
|
M_S_TO_FEET_S = 3.28084
|
|
46
50
|
FEET_S_TO_M_S = 0.3048
|
|
47
51
|
M_S_TO_KNOTS = 1.94384
|
|
48
52
|
KNOTS_TO_M_S = 0.514444
|
|
49
|
-
KM_H_TO_MI_H =
|
|
50
|
-
MI_H_TO_KM_H =
|
|
53
|
+
KM_H_TO_MI_H = Distance::KM_TO_MI
|
|
54
|
+
MI_H_TO_KM_H = Distance::MI_TO_KM
|
|
51
55
|
KM_H_TO_NAUTICAL_MI_H = 0.539957
|
|
52
56
|
NAUTICAL_MI_H_TO_KM_H = 1.852
|
|
53
|
-
MI_H_TO_NAUTICAL_MI_H =
|
|
54
|
-
NAUTICAL_MI_H_TO_MI_H =
|
|
57
|
+
MI_H_TO_NAUTICAL_MI_H = Distance::MI_TO_KM / Distance::NAUTICAL_MI_TO_KM
|
|
58
|
+
NAUTICAL_MI_H_TO_MI_H = Distance::NAUTICAL_MI_TO_KM / Distance::MI_TO_KM
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
# Converts a value from one unit to another
|
|
@@ -139,8 +143,35 @@ module Converter
|
|
|
139
143
|
format_list(Distance.constants)
|
|
140
144
|
end
|
|
141
145
|
|
|
146
|
+
# Multipliers from a supported distance-input unit to kilometres
|
|
147
|
+
# (used by methods that accept a distance_unit: keyword)
|
|
148
|
+
DISTANCE_UNIT_TO_KM = { km: 1.0, mi: Distance::MI_TO_KM }.freeze
|
|
149
|
+
|
|
142
150
|
private
|
|
143
151
|
|
|
152
|
+
# Guards the "race name + distance_unit" combination. A standard race already
|
|
153
|
+
# carries its own distance, so the keyword can only be a caller mistake —
|
|
154
|
+
# better to say so than to ignore it silently.
|
|
155
|
+
#
|
|
156
|
+
# @raise [ArgumentError] if a distance_unit was given alongside a race name
|
|
157
|
+
def reject_distance_unit_with_race_name!(distance_unit, race)
|
|
158
|
+
return if distance_unit.nil?
|
|
159
|
+
|
|
160
|
+
raise ArgumentError,
|
|
161
|
+
"distance_unit: #{distance_unit.inspect} cannot be combined with the race name " \
|
|
162
|
+
"#{race.inspect} — a standard race already carries its own distance"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Normalizes a numeric distance input to kilometres
|
|
166
|
+
#
|
|
167
|
+
# @raise [Calcpace::UnsupportedUnitError] if distance_unit is not :km or :mi
|
|
168
|
+
def normalize_distance_km(value, distance_unit)
|
|
169
|
+
factor = DISTANCE_UNIT_TO_KM.fetch(distance_unit.to_s.downcase.to_sym) do
|
|
170
|
+
raise Calcpace::UnsupportedUnitError.new(distance_unit, supported: DISTANCE_UNIT_TO_KM.keys)
|
|
171
|
+
end
|
|
172
|
+
value.to_f * factor
|
|
173
|
+
end
|
|
174
|
+
|
|
144
175
|
def format_unit(unit)
|
|
145
176
|
unit.downcase.gsub(' ', '_').to_sym
|
|
146
177
|
end
|
data/lib/calcpace/errors.rb
CHANGED
|
@@ -20,11 +20,27 @@ class Calcpace
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Raised when an unsupported unit conversion is requested
|
|
23
|
+
# Raised when an unsupported unit or unit conversion is requested
|
|
24
|
+
#
|
|
25
|
+
# @example conversion pair
|
|
26
|
+
# raise UnsupportedUnitError, :km_to_furlong
|
|
27
|
+
# @example single unit, with the supported ones listed
|
|
28
|
+
# raise UnsupportedUnitError.new(:furlong, supported: %i[km mi])
|
|
24
29
|
class UnsupportedUnitError < Error
|
|
25
|
-
def initialize(unit = nil)
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
def initialize(unit = nil, supported: nil)
|
|
31
|
+
super(build_message(unit, supported))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def build_message(unit, supported)
|
|
37
|
+
return conversion_message(unit) unless supported
|
|
38
|
+
|
|
39
|
+
"Unsupported unit: #{unit.inspect}. Supported units: #{supported.map(&:inspect).join(', ')}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def conversion_message(unit)
|
|
43
|
+
unit ? "Unsupported unit conversion: #{unit}" : 'Unsupported unit conversion'
|
|
28
44
|
end
|
|
29
45
|
end
|
|
30
46
|
end
|
|
@@ -12,9 +12,9 @@ module PaceCalculator
|
|
|
12
12
|
'half_marathon' => 21.0975,
|
|
13
13
|
'marathon' => 42.195,
|
|
14
14
|
'100k' => 100.0,
|
|
15
|
-
'1mile' =>
|
|
16
|
-
'5mile' =>
|
|
17
|
-
'10mile' =>
|
|
15
|
+
'1mile' => Converter::Distance::MI_TO_KM,
|
|
16
|
+
'5mile' => 5 * Converter::Distance::MI_TO_KM,
|
|
17
|
+
'10mile' => 10 * Converter::Distance::MI_TO_KM
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
20
|
# Calculates the finish time for a race given a pace per kilometer
|
|
@@ -93,10 +93,18 @@ module PaceCalculator
|
|
|
93
93
|
# @return [Float] distance in kilometers
|
|
94
94
|
# @raise [ArgumentError] if race is not recognized
|
|
95
95
|
def race_distance(race)
|
|
96
|
-
|
|
97
|
-
RACE_DISTANCES.fetch(key) do
|
|
96
|
+
RACE_DISTANCES.fetch(normalize_race_key(race)) do
|
|
98
97
|
raise ArgumentError,
|
|
99
98
|
"Unknown race: #{race}. Available races: #{RACE_DISTANCES.keys.join(', ')}"
|
|
100
99
|
end
|
|
101
100
|
end
|
|
101
|
+
|
|
102
|
+
# Single normalization for every race-name lookup in the gem (here and in
|
|
103
|
+
# AgeGrading), so ' 10K ' and :marathon always resolve like '10k' and 'marathon'
|
|
104
|
+
#
|
|
105
|
+
# @param race [String, Symbol] race name in any case, with or without padding
|
|
106
|
+
# @return [String] normalized lookup key
|
|
107
|
+
def normalize_race_key(race)
|
|
108
|
+
race.to_s.strip.downcase
|
|
109
|
+
end
|
|
102
110
|
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Module for deriving personalized training zones from VO2max
|
|
4
|
+
#
|
|
5
|
+
# Training paces invert the Daniels & Gilbert (1979) velocity equation:
|
|
6
|
+
# VO2 = -4.60 + 0.182258 * v + 0.000104 * v² (v in m/min)
|
|
7
|
+
# Solving the quadratic for v at a target %VO2max gives the running
|
|
8
|
+
# velocity for each training intensity (Daniels' Running Formula).
|
|
9
|
+
#
|
|
10
|
+
# Heart rate zones use the Karvonen method (Heart Rate Reserve):
|
|
11
|
+
# target = hr_rest + pct * (hr_max - hr_rest)
|
|
12
|
+
module TrainingZones
|
|
13
|
+
# Training intensities as fraction of VO2max (Daniels' Running Formula)
|
|
14
|
+
TRAINING_INTENSITIES = {
|
|
15
|
+
easy: { low: 0.59, high: 0.74 },
|
|
16
|
+
marathon: { low: 0.75, high: 0.84 },
|
|
17
|
+
threshold: { low: 0.83, high: 0.88 },
|
|
18
|
+
interval: { low: 0.95, high: 1.00 },
|
|
19
|
+
repetition: { low: 1.05, high: 1.10 }
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# A pace band for one training zone (paces per kilometre or mile).
|
|
23
|
+
# slow = lower-intensity end of the band, fast = higher-intensity end.
|
|
24
|
+
PaceBand = Struct.new(:slow_seconds, :fast_seconds, :slow_clock, :fast_clock)
|
|
25
|
+
|
|
26
|
+
# Metres per pace unit — pace bands can be expressed per km or per mile
|
|
27
|
+
PACE_UNIT_METERS = { km: 1000.0, mi: Converter::Distance::MI_TO_METERS }.freeze
|
|
28
|
+
|
|
29
|
+
# Heart-rate zone boundaries as fractions of the range being split:
|
|
30
|
+
# Heart Rate Reserve in #hr_zones (Karvonen) and HRmax in #hr_zones_from_max
|
|
31
|
+
HR_ZONE_BOUNDARIES = [0.50, 0.60, 0.70, 0.80, 0.90, 1.00].freeze
|
|
32
|
+
|
|
33
|
+
# One heart-rate training zone (1 = recovery … 5 = maximal)
|
|
34
|
+
HrZone = Struct.new(:zone, :min_bpm, :max_bpm)
|
|
35
|
+
|
|
36
|
+
# Derives training pace bands from a VO2max value
|
|
37
|
+
#
|
|
38
|
+
# @param vo2max [Numeric] VO2max in ml/kg/min (must be > 0)
|
|
39
|
+
# @param unit [Symbol] pace unit — :km (default) or :mi
|
|
40
|
+
# @return [Hash{Symbol => PaceBand}] keys: :easy, :marathon, :threshold,
|
|
41
|
+
# :interval, :repetition — paces per chosen unit
|
|
42
|
+
# @raise [Calcpace::NonPositiveInputError] if vo2max is not positive
|
|
43
|
+
# @raise [Calcpace::UnsupportedUnitError] if unit is not :km or :mi
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# calc.training_paces(50.0)[:threshold].fast_clock #=> "00:04:15"
|
|
47
|
+
# calc.training_paces(50.0, unit: :mi)[:threshold].fast_clock #=> "00:06:51"
|
|
48
|
+
def training_paces(vo2max, unit: :km)
|
|
49
|
+
check_positive(vo2max.to_f, 'VO2max')
|
|
50
|
+
meters = pace_unit_meters(unit)
|
|
51
|
+
|
|
52
|
+
TRAINING_INTENSITIES.transform_values do |band|
|
|
53
|
+
slow = pace_seconds_at_pct(vo2max.to_f, band[:low], meters)
|
|
54
|
+
fast = pace_seconds_at_pct(vo2max.to_f, band[:high], meters)
|
|
55
|
+
|
|
56
|
+
PaceBand.new(
|
|
57
|
+
slow_seconds: slow,
|
|
58
|
+
fast_seconds: fast,
|
|
59
|
+
slow_clock: convert_to_clocktime(slow),
|
|
60
|
+
fast_clock: convert_to_clocktime(fast)
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Derives training pace bands from a recent race result
|
|
66
|
+
#
|
|
67
|
+
# Convenience wrapper: estimates VO2max via Daniels & Gilbert
|
|
68
|
+
# (see Vo2maxEstimator#estimate_vo2max) and derives the bands from it.
|
|
69
|
+
#
|
|
70
|
+
# @param race [Numeric, String, Symbol] race distance in kilometres (or in
|
|
71
|
+
# miles via distance_unit: :mi), either numeric or as a numeric string
|
|
72
|
+
# ('10', '21.0975'), or a standard race name ('5k', '10k', 'half_marathon',
|
|
73
|
+
# 'marathon', '1mile', '5mile', '10mile', '100k' — see
|
|
74
|
+
# PaceCalculator::RACE_DISTANCES)
|
|
75
|
+
# @param time [String, Integer] finish time as "HH:MM:SS" / "MM:SS" or total seconds
|
|
76
|
+
# @param unit [Symbol] pace unit of the output bands — :km (default) or :mi
|
|
77
|
+
# @param distance_unit [Symbol] unit of a numeric race distance input — :km (default) or :mi.
|
|
78
|
+
# Rejected when race is a race name: standard races already carry their own
|
|
79
|
+
# distance, so the combination is always a caller mistake
|
|
80
|
+
# @return [Hash{Symbol => PaceBand}] same shape as #training_paces
|
|
81
|
+
# @raise [ArgumentError] if a race name is not recognized, or if distance_unit
|
|
82
|
+
# is combined with a race name
|
|
83
|
+
# @raise [Calcpace::UnsupportedUnitError] if unit or distance_unit is not :km or :mi
|
|
84
|
+
# @raise [Calcpace::NonPositiveInputError] if distance or time are not positive
|
|
85
|
+
# @raise [Calcpace::InvalidTimeFormatError] if time string is malformed
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# calc.training_paces_from_race(10.0, '00:40:00')[:easy].slow_clock #=> "00:05:42"
|
|
89
|
+
# calc.training_paces_from_race('5mile', '00:35:00', unit: :mi)
|
|
90
|
+
# calc.training_paces_from_race(6.2, '00:40:00', distance_unit: :mi, unit: :mi)
|
|
91
|
+
def training_paces_from_race(race, time, unit: :km, distance_unit: nil)
|
|
92
|
+
# Numeric strings ('10') keep working as distances; only non-numeric input
|
|
93
|
+
# (race names) falls through to the RACE_DISTANCES lookup
|
|
94
|
+
numeric = race.is_a?(Numeric) ? race : Float(race, exception: false)
|
|
95
|
+
distance_km = if numeric
|
|
96
|
+
normalize_distance_km(numeric, distance_unit || :km)
|
|
97
|
+
else
|
|
98
|
+
reject_distance_unit_with_race_name!(distance_unit, race)
|
|
99
|
+
race_distance(race)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
training_paces(estimate_vo2max(distance_km, time), unit: unit)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Computes the five Karvonen heart-rate training zones
|
|
106
|
+
#
|
|
107
|
+
# target_bpm = hr_rest + pct * (hr_max - hr_rest)
|
|
108
|
+
#
|
|
109
|
+
# @param hr_max [Numeric] maximum heart rate in bpm (must be > 0)
|
|
110
|
+
# @param hr_rest [Numeric] resting heart rate in bpm (must be > 0 and < hr_max)
|
|
111
|
+
# @return [Array<HrZone>] five contiguous zones from Z1 (50–60% HRR) to Z5 (90–100% HRR)
|
|
112
|
+
# @raise [Calcpace::NonPositiveInputError] if any rate is not positive
|
|
113
|
+
# @raise [Calcpace::Error] if hr_rest >= hr_max
|
|
114
|
+
#
|
|
115
|
+
# @example
|
|
116
|
+
# calc.hr_zones(hr_max: 190, hr_rest: 55).last.max_bpm #=> 190
|
|
117
|
+
def hr_zones(hr_max:, hr_rest:)
|
|
118
|
+
max = hr_max.to_f
|
|
119
|
+
rest = hr_rest.to_f
|
|
120
|
+
check_heart_rates(max, rest)
|
|
121
|
+
|
|
122
|
+
reserve = max - rest
|
|
123
|
+
build_hr_zones(HR_ZONE_BOUNDARIES.map { |pct| (rest + (pct * reserve)).round })
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Computes five heart-rate zones from maximum heart rate only (%HRmax method)
|
|
127
|
+
#
|
|
128
|
+
# Fallback for athletes who don't know their resting heart rate — the
|
|
129
|
+
# classic percent-of-max model used as default by most sports watches:
|
|
130
|
+
# target_bpm = pct * hr_max
|
|
131
|
+
#
|
|
132
|
+
# Prefer #hr_zones (Karvonen) when resting heart rate is available.
|
|
133
|
+
#
|
|
134
|
+
# @param hr_max [Numeric] maximum heart rate in bpm (must be > 0)
|
|
135
|
+
# @return [Array<HrZone>] five contiguous zones from Z1 (50–60% HRmax) to Z5 (90–100% HRmax)
|
|
136
|
+
# @raise [Calcpace::NonPositiveInputError] if hr_max is not positive
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# calc.hr_zones_from_max(hr_max: 190).first.min_bpm #=> 95
|
|
140
|
+
def hr_zones_from_max(hr_max:)
|
|
141
|
+
max = hr_max.to_f
|
|
142
|
+
check_positive(max, 'Maximum heart rate')
|
|
143
|
+
|
|
144
|
+
build_hr_zones(HR_ZONE_BOUNDARIES.map { |pct| (pct * max).round })
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# Turns six ascending bpm boundary points into five contiguous HrZone structs
|
|
150
|
+
def build_hr_zones(points)
|
|
151
|
+
points.each_cons(2).with_index(1).map do |(min_bpm, max_bpm), zone|
|
|
152
|
+
HrZone.new(zone: zone, min_bpm: min_bpm, max_bpm: max_bpm)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def check_heart_rates(hr_max, hr_rest)
|
|
157
|
+
check_positive(hr_max, 'Maximum heart rate')
|
|
158
|
+
check_positive(hr_rest, 'Resting heart rate')
|
|
159
|
+
return if hr_rest < hr_max
|
|
160
|
+
|
|
161
|
+
raise Calcpace::Error,
|
|
162
|
+
"Resting heart rate (#{hr_rest}) must be lower than maximum heart rate (#{hr_max})"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Inverts Daniels & Gilbert: velocity (m/min) that demands a given VO2
|
|
166
|
+
def velocity_at_vo2(vo2)
|
|
167
|
+
a = 0.000104
|
|
168
|
+
b = 0.182258
|
|
169
|
+
c = -(4.60 + vo2)
|
|
170
|
+
|
|
171
|
+
(-b + Math.sqrt((b**2) - (4 * a * c))) / (2 * a)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def pace_unit_meters(unit)
|
|
175
|
+
PACE_UNIT_METERS.fetch(unit.to_s.downcase.to_sym) do
|
|
176
|
+
raise Calcpace::UnsupportedUnitError.new(unit, supported: PACE_UNIT_METERS.keys)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def pace_seconds_at_pct(vo2max, pct, meters)
|
|
181
|
+
velocity = velocity_at_vo2(vo2max * pct)
|
|
182
|
+
(meters * 60.0 / velocity).round
|
|
183
|
+
end
|
|
184
|
+
end
|
data/lib/calcpace/version.rb
CHANGED
|
@@ -32,17 +32,21 @@ module Vo2maxEstimator
|
|
|
32
32
|
|
|
33
33
|
# Estimates VO2max from a race performance using Daniels & Gilbert formula
|
|
34
34
|
#
|
|
35
|
-
# @param
|
|
35
|
+
# @param distance [Numeric] race distance (must be > 0), in kilometres by
|
|
36
|
+
# default or in the unit given by distance_unit
|
|
36
37
|
# @param time [String, Integer] finish time as "HH:MM:SS" / "MM:SS", or total seconds (must be > 0)
|
|
38
|
+
# @param distance_unit [Symbol] unit of the distance input — :km (default) or :mi
|
|
37
39
|
# @return [Float] estimated VO2max in ml/kg/min, rounded to one decimal place
|
|
38
40
|
# @raise [Calcpace::NonPositiveInputError] if distance or time are not positive
|
|
39
41
|
# @raise [Calcpace::InvalidTimeFormatError] if time string is not in HH:MM:SS or MM:SS format
|
|
42
|
+
# @raise [Calcpace::UnsupportedUnitError] if distance_unit is not :km or :mi
|
|
40
43
|
#
|
|
41
44
|
# @example 10 km in 40:00 → ~51.9 ml/kg/min
|
|
42
45
|
# calc = Calcpace.new
|
|
43
|
-
# calc.estimate_vo2max(10.0, '00:40:00')
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
# calc.estimate_vo2max(10.0, '00:40:00') #=> 51.9
|
|
47
|
+
# calc.estimate_vo2max(6.21371, '00:40:00', distance_unit: :mi) #=> 51.9
|
|
48
|
+
def estimate_vo2max(distance, time, distance_unit: :km)
|
|
49
|
+
distance_m = normalize_distance_km(distance, distance_unit) * 1000
|
|
46
50
|
time_min = parse_time_minutes(time)
|
|
47
51
|
|
|
48
52
|
check_positive(distance_m, 'Distance')
|
|
@@ -57,13 +61,23 @@ module Vo2maxEstimator
|
|
|
57
61
|
|
|
58
62
|
# Estimates a detailed and contextualized VO2max
|
|
59
63
|
#
|
|
60
|
-
# @param
|
|
64
|
+
# @param distance [Numeric] race distance, in kilometres by default or in
|
|
65
|
+
# the unit given by distance_unit
|
|
61
66
|
# @param time [String, Integer] finish time
|
|
62
67
|
# @param elevation_gain_m [Numeric] total elevation gain in metres
|
|
63
68
|
# @param hr_avg [Numeric] average heart rate during the effort
|
|
64
69
|
# @param hr_max [Numeric] athlete's maximum heart rate
|
|
70
|
+
# @param distance_unit [Symbol] unit of the distance input — :km (default) or :mi
|
|
65
71
|
# @return [Vo2maxResult] structured result with value and metadata
|
|
66
|
-
|
|
72
|
+
# (adjusted_distance_km is always in kilometres)
|
|
73
|
+
# @raise [Calcpace::NonPositiveInputError] if distance or time are not positive
|
|
74
|
+
# @raise [Calcpace::UnsupportedUnitError] if distance_unit is not :km or :mi
|
|
75
|
+
def estimate_detailed_vo2max(distance, time, elevation_gain_m: 0, hr_avg: nil, hr_max: nil, distance_unit: :km)
|
|
76
|
+
distance_km = normalize_distance_km(distance, distance_unit)
|
|
77
|
+
# Validated before the elevation adjustment, which would otherwise turn a
|
|
78
|
+
# negative distance into a positive equivalent-flat one
|
|
79
|
+
check_positive(distance_km, 'Distance')
|
|
80
|
+
|
|
67
81
|
adj_dist_km = adjusted_distance_for_vo2(distance_km, elevation_gain_m)
|
|
68
82
|
vo2max_val = estimate_vo2max(adj_dist_km, time)
|
|
69
83
|
confidence = calculate_time_confidence(parse_time_minutes(time))
|
data/lib/calcpace.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative 'calcpace/pace_converter'
|
|
|
13
13
|
require_relative 'calcpace/race_predictor'
|
|
14
14
|
require_relative 'calcpace/race_splits'
|
|
15
15
|
require_relative 'calcpace/track_calculator'
|
|
16
|
+
require_relative 'calcpace/training_zones'
|
|
16
17
|
require_relative 'calcpace/vo2max_estimator'
|
|
17
18
|
|
|
18
19
|
# Calcpace - A Ruby gem for pace, distance, and time calculations
|
|
@@ -49,6 +50,7 @@ class Calcpace
|
|
|
49
50
|
include RacePredictor
|
|
50
51
|
include RaceSplits
|
|
51
52
|
include TrackCalculator
|
|
53
|
+
include TrainingZones
|
|
52
54
|
include Vo2maxEstimator
|
|
53
55
|
|
|
54
56
|
# Creates a new Calcpace instance
|
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.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- João Gilberto Saraiva
|
|
@@ -9,9 +9,11 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description: 'Ruby gem for
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
description: 'Ruby gem for runners: pace, time, and distance calculations, unit conversions
|
|
13
|
+
(30+ units), race time predictions (Riegel & Cameron), GPS track analysis (Haversine,
|
|
14
|
+
elevation gain, per-km splits), age grading (WMA 2023), VO2max estimation (Daniels
|
|
15
|
+
& Gilbert), and personalized training zones (Daniels paces & Karvonen heart-rate
|
|
16
|
+
zones).'
|
|
15
17
|
email:
|
|
16
18
|
- joaogilberto@tuta.io
|
|
17
19
|
executables: []
|
|
@@ -47,6 +49,7 @@ files:
|
|
|
47
49
|
- lib/calcpace/race_predictor.rb
|
|
48
50
|
- lib/calcpace/race_splits.rb
|
|
49
51
|
- lib/calcpace/track_calculator.rb
|
|
52
|
+
- lib/calcpace/training_zones.rb
|
|
50
53
|
- lib/calcpace/version.rb
|
|
51
54
|
- lib/calcpace/vo2max_estimator.rb
|
|
52
55
|
homepage: https://github.com/0jonjo/calcpace
|
|
@@ -70,8 +73,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
70
73
|
- !ruby/object:Gem::Version
|
|
71
74
|
version: '0'
|
|
72
75
|
requirements: []
|
|
73
|
-
rubygems_version: 4.0.
|
|
76
|
+
rubygems_version: 4.0.16
|
|
74
77
|
specification_version: 4
|
|
75
|
-
summary:
|
|
76
|
-
|
|
78
|
+
summary: 'Running calculations: pace, race predictions, GPS track analysis, VO2max,
|
|
79
|
+
and training zones.'
|
|
77
80
|
test_files: []
|