calcpace 1.9.7 → 1.9.8
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/CHANGELOG.md +9 -0
- data/README.md +44 -1
- data/lib/calcpace/version.rb +1 -1
- data/lib/calcpace/vo2max_estimator.rb +72 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c753575818b486be4f9c955d69e062263d61fd604e415c82409203c7d8d03156
|
|
4
|
+
data.tar.gz: 95459f1aa861dec764b2cfae6b9cd69cab5d866e31547d843672e460c309999d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eddb87f77382d6973059b517c9c2ce696d9cc31246f38db46651b1c62d155e52fafb022fff373220f0aced664b4213eddf9a4b57aa71564be22321799cc0f254
|
|
7
|
+
data.tar.gz: 1c5cfc3771471b3d89b9ba8eee8e742bf9508815e13489692d4ceae3d0f2a8a223a29f7b9205c57ceed8f3b7df28df27893c4e512f356489916ffe3644b05010
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.9.8] - 2026-05-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Contextualized VO2max estimation (`Vo2maxEstimator#estimate_detailed_vo2max`)
|
|
14
|
+
- Confidence Score based on effort duration (Daniels & Gilbert optimal window)
|
|
15
|
+
- Elevation Adjustment (Equivalent Flat Distance) using Naismith-based heuristic
|
|
16
|
+
- Sub-maximal effort detection via Heart Rate intensity validation (%HRmax)
|
|
17
|
+
- Structured result object (`Vo2maxResult`) with value, confidence, and metadata
|
|
18
|
+
|
|
10
19
|
## [1.9.7] - 2026-05-16
|
|
11
20
|
|
|
12
21
|
### Added
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A Ruby gem for running and cycling calculations: pace, time, distance, unit conv
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
|
-
gem 'calcpace', '~> 1.9.
|
|
8
|
+
gem 'calcpace', '~> 1.9.8'
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
@@ -221,6 +221,8 @@ calc.vo2max_label(51.9) # => "Very Good"
|
|
|
221
221
|
| 30–39 | Fair |
|
|
222
222
|
| < 30 | Beginner |
|
|
223
223
|
|
|
224
|
+
*Thresholds based on Daniels, J. (2014). Daniels' Running Formula (3rd ed.), consistent with ACSM guidelines and McArdle, Katch & Katch (2015) Exercise Physiology.*
|
|
225
|
+
|
|
224
226
|
**Formula:**
|
|
225
227
|
```
|
|
226
228
|
velocity (m/min) = distance_m / time_min
|
|
@@ -231,6 +233,47 @@ VO2max = VO2 / %VO2max
|
|
|
231
233
|
|
|
232
234
|
Accuracy: ±3–5 ml/kg/min vs. laboratory testing. Best with efforts between **5 and 60 minutes** at near-maximal pace.
|
|
233
235
|
|
|
236
|
+
#### Contextualized estimation
|
|
237
|
+
|
|
238
|
+
`estimate_detailed_vo2max` returns a richer result that accounts for elevation, heart rate, and formula reliability:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# Mountain 10K: 200 m elevation gain, avg HR 172, max HR 190
|
|
242
|
+
result = calc.estimate_detailed_vo2max(
|
|
243
|
+
10.0, '00:48:30',
|
|
244
|
+
elevation_gain_m: 200,
|
|
245
|
+
hr_avg: 172,
|
|
246
|
+
hr_max: 190
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
result.value # => 47.7 (corrected for 1.2 km of equivalent flat distance)
|
|
250
|
+
result.adjusted_distance_km # => 11.2 (10 km + 200 m × 6 flat-equivalent)
|
|
251
|
+
result.confidence # => :high (48 min is inside the 5–60 min optimal window)
|
|
252
|
+
result.sub_maximal # => false (172/190 = 90.5 % HRmax → maximal effort)
|
|
253
|
+
|
|
254
|
+
calc.vo2max_label(result.value) # => "Good"
|
|
255
|
+
|
|
256
|
+
# Compare: same effort ignoring elevation → underestimates VO2max
|
|
257
|
+
flat = calc.estimate_detailed_vo2max(10.0, '00:48:30')
|
|
258
|
+
flat.value # => 41.5
|
|
259
|
+
|
|
260
|
+
# Easy recovery run: sub-maximal effort flag + confidence downgrade
|
|
261
|
+
easy = calc.estimate_detailed_vo2max(10.0, '01:05:00', hr_avg: 135, hr_max: 190)
|
|
262
|
+
easy.sub_maximal # => true (135/190 = 71 % HRmax < 85 %)
|
|
263
|
+
easy.confidence # => :low (formula assumes race-pace effort)
|
|
264
|
+
easy.value # => 29.3 (underestimates real aerobic capacity)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
| `confidence` | Effort duration | Notes |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `:high` | 5–60 min | Daniels & Gilbert optimal window |
|
|
270
|
+
| `:medium` | > 60–120 min | Muscular fatigue starts distorting the estimate |
|
|
271
|
+
| `:low` | < 5 min or > 120 min | Anaerobic / glycogen-depletion effects dominate |
|
|
272
|
+
|
|
273
|
+
> If `hr_avg > hr_max`, a `Calcpace::Error` is raised (physiologically impossible input).
|
|
274
|
+
> If you provide heart rate data, both `hr_avg` and `hr_max` must be present.
|
|
275
|
+
> `elevation_gain_m` must be zero or positive.
|
|
276
|
+
|
|
234
277
|
---
|
|
235
278
|
|
|
236
279
|
### Other Utilities
|
data/lib/calcpace/version.rb
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
# Accuracy: ±3–5 ml/kg/min vs laboratory testing. Best results with efforts
|
|
15
15
|
# between 5 and 60 minutes at race pace (i.e. near-maximal effort).
|
|
16
16
|
module Vo2maxEstimator
|
|
17
|
+
# Classification thresholds based on:
|
|
18
|
+
# Daniels, J. (2014). Daniels' Running Formula (3rd ed.). Human Kinetics.
|
|
19
|
+
# General ranges are consistent with ACSM guidelines and widely cited in
|
|
20
|
+
# exercise physiology literature (McArdle, Katch & Katch, 2015).
|
|
17
21
|
VO2MAX_LABELS = [
|
|
18
22
|
{ min: 70, label: 'Elite' },
|
|
19
23
|
{ min: 60, label: 'Excellent' },
|
|
@@ -23,6 +27,9 @@ module Vo2maxEstimator
|
|
|
23
27
|
{ min: 0, label: 'Beginner' }
|
|
24
28
|
].freeze
|
|
25
29
|
|
|
30
|
+
# Represents a contextualized VO2max estimation result
|
|
31
|
+
Vo2maxResult = Struct.new(:value, :confidence, :sub_maximal, :adjusted_distance_km)
|
|
32
|
+
|
|
26
33
|
# Estimates VO2max from a race performance using Daniels & Gilbert formula
|
|
27
34
|
#
|
|
28
35
|
# @param distance_km [Numeric] race distance in kilometres (must be > 0)
|
|
@@ -48,6 +55,30 @@ module Vo2maxEstimator
|
|
|
48
55
|
(vo2 / pct_vo2max).round(1)
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
# Estimates a detailed and contextualized VO2max
|
|
59
|
+
#
|
|
60
|
+
# @param distance_km [Numeric] race distance in kilometres
|
|
61
|
+
# @param time [String, Integer] finish time
|
|
62
|
+
# @param elevation_gain_m [Numeric] total elevation gain in metres
|
|
63
|
+
# @param hr_avg [Numeric] average heart rate during the effort
|
|
64
|
+
# @param hr_max [Numeric] athlete's maximum heart rate
|
|
65
|
+
# @return [Vo2maxResult] structured result with value and metadata
|
|
66
|
+
def estimate_detailed_vo2max(distance_km, time, elevation_gain_m: 0, hr_avg: nil, hr_max: nil)
|
|
67
|
+
adj_dist_km = adjusted_distance_for_vo2(distance_km, elevation_gain_m)
|
|
68
|
+
vo2max_val = estimate_vo2max(adj_dist_km, time)
|
|
69
|
+
confidence = calculate_time_confidence(parse_time_minutes(time))
|
|
70
|
+
|
|
71
|
+
hr_data = validate_and_analyze_hr(hr_avg, hr_max)
|
|
72
|
+
confidence = :low if hr_data[:sub_maximal]
|
|
73
|
+
|
|
74
|
+
Vo2maxResult.new(
|
|
75
|
+
value: vo2max_val,
|
|
76
|
+
confidence: confidence,
|
|
77
|
+
sub_maximal: hr_data[:sub_maximal],
|
|
78
|
+
adjusted_distance_km: adj_dist_km.round(2)
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
51
82
|
# Returns a descriptive label for a given VO2max value
|
|
52
83
|
#
|
|
53
84
|
# @param value [Numeric] VO2max in ml/kg/min
|
|
@@ -64,6 +95,47 @@ module Vo2maxEstimator
|
|
|
64
95
|
|
|
65
96
|
private
|
|
66
97
|
|
|
98
|
+
def adjusted_distance_for_vo2(distance_km, elevation_gain_m)
|
|
99
|
+
check_non_negative(elevation_gain_m, 'Elevation gain')
|
|
100
|
+
|
|
101
|
+
# Naismith-based heuristic: 100m gain = +600m flat
|
|
102
|
+
((distance_km.to_f * 1000) + (elevation_gain_m.to_f * 6.0)) / 1000.0
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def validate_and_analyze_hr(hr_avg, hr_max)
|
|
106
|
+
if hr_avg.nil? ^ hr_max.nil?
|
|
107
|
+
raise Calcpace::Error, 'Average heart rate and maximum heart rate must be provided together'
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return { sub_maximal: false } unless hr_avg && hr_max
|
|
111
|
+
|
|
112
|
+
check_positive(hr_avg, 'Average heart rate')
|
|
113
|
+
check_positive(hr_max, 'Maximum heart rate')
|
|
114
|
+
|
|
115
|
+
avg = hr_avg.to_f
|
|
116
|
+
max = hr_max.to_f
|
|
117
|
+
|
|
118
|
+
raise Calcpace::Error, "Average heart rate (#{avg}) cannot exceed maximum heart rate (#{max})" if avg > max
|
|
119
|
+
|
|
120
|
+
{ sub_maximal: (avg / max) < 0.85 }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def calculate_time_confidence(time_min)
|
|
124
|
+
if time_min.between?(5, 60)
|
|
125
|
+
:high
|
|
126
|
+
elsif time_min > 60 && time_min <= 120
|
|
127
|
+
:medium
|
|
128
|
+
else
|
|
129
|
+
:low
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def check_non_negative(number, name = 'Input')
|
|
134
|
+
return if number.is_a?(Numeric) && number >= 0
|
|
135
|
+
|
|
136
|
+
raise Calcpace::Error, "#{name} must be zero or a positive number"
|
|
137
|
+
end
|
|
138
|
+
|
|
67
139
|
def vo2_at_velocity(velocity)
|
|
68
140
|
-4.60 + (0.182258 * velocity) + (0.000104 * (velocity**2))
|
|
69
141
|
end
|