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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d9eb6ddf00dde81f5405ab23cf97f609eaf5b11e2f49f69724cc1579e976dc3
4
- data.tar.gz: 97400c042ef3109c0479655e07bb55e72f136b4dcc5ba044cdd9ae6311ad8dd4
3
+ metadata.gz: c753575818b486be4f9c955d69e062263d61fd604e415c82409203c7d8d03156
4
+ data.tar.gz: 95459f1aa861dec764b2cfae6b9cd69cab5d866e31547d843672e460c309999d
5
5
  SHA512:
6
- metadata.gz: 6b74a386783c041329e47dbc6308f2844afe7be54fff27049d54f9d91dc00cc696522cdbd1a035cbd401299276a7c75ab9438220248b14b9b815bca39cd52609
7
- data.tar.gz: 8328528b5680a4eaca14ad9ee9dddc233069386888d1e59cd676614a5d6a7292cc777360cfeabf0cc6d8fa7d8e9f4266feb3dbed7bc0de0b2ebb8d03025bc457
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.7'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Calcpace
4
- VERSION = '1.9.7'
4
+ VERSION = '1.9.8'
5
5
  end
@@ -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
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.9.7
4
+ version: 1.9.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - João Gilberto Saraiva