calcpace 1.9.6 → 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: dc7e42385a1c6bf01908929f49004b332943dd0971bcb2d93ac69c838bc94ad2
4
- data.tar.gz: 955bff3b89fabb29e6a4223efd5e8654bc0a76c6b59b507a31a1db8673cebce8
3
+ metadata.gz: c753575818b486be4f9c955d69e062263d61fd604e415c82409203c7d8d03156
4
+ data.tar.gz: 95459f1aa861dec764b2cfae6b9cd69cab5d866e31547d843672e460c309999d
5
5
  SHA512:
6
- metadata.gz: 750ec40b02ae3607bb01efd6333ffd0e6ccfa21689914a58e3bb6d8f0ce17a60c6c66f01465766786847979a83fa581130550877edcb57056a549b613e72925f
7
- data.tar.gz: b1bec9ff070a7511a604e5a6d34c2912735b27b7695f16b4afaaf9f6f0fbb9e56135068a881ab8b4de732bcba501bfe6a2dce8d07eff5b13b0f45b3805b3863c
6
+ metadata.gz: eddb87f77382d6973059b517c9c2ce696d9cc31246f38db46651b1c62d155e52fafb022fff373220f0aced664b4213eddf9a4b57aa71564be22321799cc0f254
7
+ data.tar.gz: 1c5cfc3771471b3d89b9ba8eee8e742bf9508815e13489692d4ceae3d0f2a8a223a29f7b9205c57ceed8f3b7df28df27893c4e512f356489916ffe3644b05010
data/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ 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
+
19
+ ## [1.9.7] - 2026-05-16
20
+
21
+ ### Added
22
+ - Environmental Performance Adjustments module (`EnvironmentalAdjuster`)
23
+ - Adjust race results and predictions based on temperature and altitude
24
+ - Scientific basis: Matthew Ely et al. (2007) for heat and NCAA standards for altitude
25
+ - Data-driven penalty tables stored in `lib/calcpace/data/environmental_factors.yml`
26
+ - Support for interpolation between data points in penalty tables
27
+ - Transparent return values including penalty percentage and factor breakdown
28
+ - New prediction methods with environmental support:
29
+ - `predict_time_adjusted` (Riegel-based)
30
+ - `predict_time_cameron_adjusted` (Cameron-based)
31
+ - `adjust_time` and `calculate_penalty` methods for direct environmental impact analysis
32
+
10
33
  ## [1.9.6] - 2026-05-15
11
34
 
12
35
  ### Changed
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.6'
8
+ gem 'calcpace', '~> 1.9.8'
9
9
  ```
10
10
 
11
11
  ## Usage
@@ -33,6 +33,44 @@ calc.checked_distance('01:21:32', '00:06:27') # => 12.64
33
33
 
34
34
  ---
35
35
 
36
+ ### Environmental Performance Adjustments
37
+
38
+ Adjust race performance based on heat and altitude. Calculations are based on scientific models
39
+ (Matthew Ely 2007 for heat, NCAA standards for altitude).
40
+
41
+ ```ruby
42
+ # Calculate penalty for 25°C and 2000m altitude (Defaults to 60-min effort)
43
+ penalty = calc.calculate_penalty(temperature: 25, altitude: 2000)
44
+ # => {
45
+ # total_penalty_percent: 8.62,
46
+ # factors: { heat: 4.3, altitude: 4.32 }
47
+ # }
48
+
49
+ # Fahrenheit support
50
+ calc.calculate_penalty(temperature: 80, temperature_unit: :f)
51
+ # => { total_penalty_percent: 5.03, ... }
52
+
53
+ # Adjust a 3:30 marathon time (12600s) for these conditions (High exposure penalty)
54
+ result = calc.adjust_time(12600, temperature: 25, altitude: 2000)
55
+ # => {
56
+ # original_time: 12600,
57
+ # adjusted_time: 15176.7,
58
+ # adjusted_time_clock: "04:12:56",
59
+ # penalty_percent: 20.45,
60
+ # factors: { heat: 16.13, altitude: 4.32 }
61
+ # }
62
+
63
+ # Predicted adjusted times (Riegel formula)
64
+ calc.predict_time_adjusted('5k', '00:20:00', '10k', temperature: 28)
65
+ # => { adjusted_time: 2599.74, adjusted_time_clock: "00:43:19", penalty_percent: 3.91, ... }
66
+
67
+ # Predicted adjusted times (Cameron formula)
68
+ calc.predict_time_cameron_adjusted('10k', '00:40:00', 'marathon', temperature: 80, temperature_unit: :f)
69
+ # => { adjusted_time: 11585.88, adjusted_time_clock: "03:13:05", penalty_percent: 14.18, ... }
70
+ ```
71
+
72
+ ---
73
+
36
74
  ### Unit Conversions
37
75
 
38
76
  30+ units supported. String or symbol format:
@@ -183,6 +221,8 @@ calc.vo2max_label(51.9) # => "Very Good"
183
221
  | 30–39 | Fair |
184
222
  | < 30 | Beginner |
185
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
+
186
226
  **Formula:**
187
227
  ```
188
228
  velocity (m/min) = distance_m / time_min
@@ -193,6 +233,47 @@ VO2max = VO2 / %VO2max
193
233
 
194
234
  Accuracy: ±3–5 ml/kg/min vs. laboratory testing. Best with efforts between **5 and 60 minutes** at near-maximal pace.
195
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
+
196
277
  ---
197
278
 
198
279
  ### Other Utilities
@@ -96,6 +96,18 @@ module CameronPredictor
96
96
  convert_to_clocktime(predict_pace_cameron(from_race, from_time, to_race))
97
97
  end
98
98
 
99
+ # Predicts race time adjusted for environmental conditions using Cameron formula
100
+ #
101
+ # @param from_race [String, Symbol] known race distance
102
+ # @param from_time [String, Numeric] time achieved at known distance
103
+ # @param to_race [String, Symbol] target race distance to predict
104
+ # @param options [Hash] environmental options (temperature, altitude, etc.)
105
+ # @return [Hash] hash with adjusted prediction and penalty details
106
+ def predict_time_cameron_adjusted(from_race, from_time, to_race, **)
107
+ predicted_seconds = predict_time_cameron(from_race, from_time, to_race)
108
+ adjust_time(predicted_seconds, **)
109
+ end
110
+
99
111
  private
100
112
 
101
113
  # Computes the Cameron exponential correction factor for a given distance
@@ -0,0 +1,31 @@
1
+ # Environmental Performance Adjustment Factors
2
+ #
3
+ # Sources:
4
+ # - Altitude: NCAA Altitude Adjustment Factors (TFRRS)
5
+ # Ref: ~3.76% penalty for 1828.8m (6000ft).
6
+ # - Heat: Matthew Ely et al. (2007) "Impact of Weather on Marathon-Running Performance"
7
+ # NOTE: These heat factors are the BASELINE for a 60-minute effort.
8
+ # The final penalty is scaled by a DurationFactor (0.5x to 4.5x) based on total exposure time.
9
+
10
+ # Altitude adjustments (NCAA standards)
11
+ altitude:
12
+ threshold_meters: 914.4
13
+ data_points:
14
+ 0: 0.0
15
+ 914.4: 1.41
16
+ 1219.2: 2.15
17
+ 1524.0: 2.90
18
+ 1828.8: 3.76
19
+ 2133.6: 4.75
20
+ 2438.4: 5.90
21
+
22
+ # Heat adjustments (60-minute baseline)
23
+ # These values represent the penalty for a 1-hour run.
24
+ # For longer/shorter runs, the DurationFactor will scale these up/down.
25
+ heat:
26
+ ideal_range_celsius: [10.0, 15.0]
27
+ data_points:
28
+ 15: 0.0
29
+ 20: 2.8 # Base for 60m. For 3h (3.0x) = 8.4% (Ely: 9%)
30
+ 25: 4.3 # Base for 60m. For 3h (3.0x) = 12.9% (Ely: 12%)
31
+ 30: 6.5 # Base for 60m. For 3h (3.0x) = 19.5%
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ # Module for adjusting race performance based on environmental conditions
6
+ #
7
+ # Scientific basis:
8
+ # - Heat: Matthew Ely et al. (2007) "Impact of Weather on Marathon-Running Performance"
9
+ # - Altitude: NCAA Altitude Adjustment Factors (TFRRS)
10
+ module EnvironmentalAdjuster
11
+ DATA_PATH = File.expand_path('data/environmental_factors.yml', __dir__).freeze
12
+ FACTORS = YAML.safe_load_file(DATA_PATH, permitted_classes: [], aliases: false).freeze
13
+
14
+ # Calculates the performance penalty percentage for given environmental conditions
15
+ #
16
+ # @param temperature [Numeric, nil] ambient temperature
17
+ # @param temperature_unit [Symbol, String] :c (Celsius) or :f (Fahrenheit)
18
+ # @param altitude [Numeric, nil] altitude in meters
19
+ # @param time_seconds [Numeric, nil] duration of the effort in seconds
20
+ # @return [Hash] hash with :total_penalty_percent and breakdown in :factors
21
+ def calculate_penalty(temperature: nil, temperature_unit: :c, altitude: nil, time_seconds: nil)
22
+ heat_penalty = calculate_heat_penalty(temperature, temperature_unit, time_seconds)
23
+ altitude_penalty = calculate_altitude_penalty(altitude)
24
+
25
+ {
26
+ total_penalty_percent: (heat_penalty + altitude_penalty).round(2),
27
+ factors: {
28
+ heat: heat_penalty,
29
+ altitude: altitude_penalty
30
+ }
31
+ }
32
+ end
33
+
34
+ # Adjusts a given time based on environmental conditions
35
+ #
36
+ # @param time_seconds [Numeric] original time in seconds
37
+ # @param options [Hash] environmental options
38
+ # @return [Hash] hash with adjusted time and penalty details
39
+ def adjust_time(time_seconds, **)
40
+ penalty = calculate_penalty(time_seconds: time_seconds, **)
41
+ percent = penalty[:total_penalty_percent]
42
+ adjusted_seconds = (time_seconds * (1 + (percent / 100.0))).round(2)
43
+
44
+ {
45
+ original_time: time_seconds,
46
+ adjusted_time: adjusted_seconds,
47
+ adjusted_time_clock: convert_to_clocktime(adjusted_seconds),
48
+ penalty_percent: percent,
49
+ factors: penalty[:factors]
50
+ }
51
+ end
52
+
53
+ # Normalizes a time achieved in non-ideal conditions to its ideal equivalent
54
+ #
55
+ # @param time_seconds [Numeric] performance time in seconds
56
+ # @param options [Hash] environmental options
57
+ # @return [Hash] hash with normalized time and penalty details
58
+ def normalize_time(time_seconds, **)
59
+ penalty = calculate_penalty(time_seconds: time_seconds, **)
60
+ percent = penalty[:total_penalty_percent]
61
+ normalized_seconds = (time_seconds / (1 + (percent / 100.0))).round(2)
62
+
63
+ {
64
+ original_time: time_seconds,
65
+ normalized_time: normalized_seconds,
66
+ normalized_time_clock: convert_to_clocktime(normalized_seconds),
67
+ penalty_percent: percent,
68
+ factors: penalty[:factors]
69
+ }
70
+ end
71
+
72
+ private
73
+
74
+ def calculate_heat_penalty(temp, unit, time_seconds)
75
+ return 0.0 if temp.nil?
76
+
77
+ temp_c = normalize_temperature(temp, unit)
78
+
79
+ data = FACTORS.fetch('heat')
80
+ ideal_min, ideal_max = data.fetch('ideal_range_celsius')
81
+ return 0.0 if temp_c.between?(ideal_min, ideal_max)
82
+
83
+ points = data.fetch('data_points')
84
+ base_penalty = interpolate_environmental_factor(points, temp_c)
85
+
86
+ (base_penalty * duration_factor(time_seconds)).round(2)
87
+ end
88
+
89
+ def duration_factor(time_seconds)
90
+ return 1.0 if time_seconds.nil?
91
+
92
+ minutes = time_seconds / 60.0
93
+
94
+ # Rule based on Matthew Ely (2007) heat degradation curve.
95
+ # Scaled for piecewise linear interpolation to avoid jumps.
96
+ if minutes <= 30
97
+ 0.5
98
+ elsif minutes <= 60
99
+ # Scale from 0.5x (30m) up to 1.0x (60m)
100
+ 0.5 + (((minutes - 30.0) / 30.0) * 0.5)
101
+ elsif minutes <= 180
102
+ # Scale from 1.0x (60m) up to 3.0x (180m / 3h)
103
+ 1.0 + (((minutes - 60.0) / 120.0) * 2.0)
104
+ else
105
+ # Scale from 3.0x (3h) up to 4.5x (4h)
106
+ capped_minutes = [minutes, 240.0].min
107
+ 3.0 + (((capped_minutes - 180.0) / 60.0) * 1.5)
108
+ end
109
+ end
110
+
111
+ def normalize_temperature(temp, unit)
112
+ return temp.to_f if %i[c celsius].include?(unit.to_s.downcase.to_sym)
113
+ return ((temp.to_f - 32) * 5.0 / 9.0).round(2) if %i[f fahrenheit].include?(unit.to_s.downcase.to_sym)
114
+
115
+ raise ArgumentError, "Unsupported temperature unit '#{unit}'. Supported: :c, :f"
116
+ end
117
+
118
+ def calculate_altitude_penalty(alt)
119
+ return 0.0 if alt.nil?
120
+
121
+ data = FACTORS.fetch('altitude')
122
+ threshold = data.fetch('threshold_meters')
123
+ return 0.0 if alt <= threshold
124
+
125
+ points = data.fetch('data_points')
126
+ interpolate_environmental_factor(points, alt)
127
+ end
128
+
129
+ def interpolate_environmental_factor(points, value)
130
+ key_map, sorted_floats = environmental_key_mapping(points)
131
+
132
+ return points.fetch(key_map[sorted_floats.first]).to_f if value <= sorted_floats.first
133
+ return points.fetch(key_map[sorted_floats.last]).to_f if value >= sorted_floats.last
134
+
135
+ lower_val, upper_val = neighboring_environmental_points(sorted_floats, value)
136
+ return points.fetch(key_map[lower_val]).to_f if lower_val == upper_val
137
+
138
+ interpolate_values(points, key_map, lower_val, upper_val, value)
139
+ end
140
+
141
+ def environmental_key_mapping(points)
142
+ map = points.keys.to_h { |k| [k.to_f, k] }
143
+ [map, map.keys.sort]
144
+ end
145
+
146
+ def neighboring_environmental_points(sorted_floats, value)
147
+ lower = sorted_floats.select { |k| k <= value }.max
148
+ upper = sorted_floats.select { |k| k >= value }.min
149
+ [lower, upper]
150
+ end
151
+
152
+ def interpolate_values(points, key_map, lower_val, upper_val, value)
153
+ lower_factor = points.fetch(key_map[lower_val]).to_f
154
+ upper_factor = points.fetch(key_map[upper_val]).to_f
155
+
156
+ ratio = (value - lower_val) / (upper_val - lower_val)
157
+ (lower_factor + ((upper_factor - lower_factor) * ratio)).round(2)
158
+ end
159
+ end
@@ -123,4 +123,23 @@ module RacePredictor
123
123
  pace_clock: convert_to_clocktime(predicted_pace)
124
124
  }
125
125
  end
126
+
127
+ # Predicts race time adjusted for environmental conditions
128
+ #
129
+ # @param from_race [String, Symbol] known race distance
130
+ # @param from_time [String, Numeric] time achieved at known distance
131
+ # @param to_race [String, Symbol] target race distance to predict
132
+ # @param options [Hash] environmental options:
133
+ # - :temperature [Numeric]
134
+ # - :temperature_unit [Symbol, String] :c or :f
135
+ # - :altitude [Numeric]
136
+ # @return [Hash] hash with adjusted prediction and penalty details
137
+ #
138
+ # @example Predict marathon time from 5K adjusted for heat (25C)
139
+ # predict_time_adjusted('5k', '00:20:00', 'marathon', temperature: 25)
140
+ # #=> { adjusted_time: 12213.4, penalty_percent: 6.0, ... }
141
+ def predict_time_adjusted(from_race, from_time, to_race, **)
142
+ predicted_seconds = predict_time(from_race, from_time, to_race)
143
+ adjust_time(predicted_seconds, **)
144
+ end
126
145
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Calcpace
4
- VERSION = '1.9.6'
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
data/lib/calcpace.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative 'calcpace/calculator'
4
4
  require_relative 'calcpace/cameron_predictor'
5
5
  require_relative 'calcpace/age_grading'
6
+ require_relative 'calcpace/environmental_adjuster'
6
7
  require_relative 'calcpace/checker'
7
8
  require_relative 'calcpace/converter'
8
9
  require_relative 'calcpace/converter_chain'
@@ -37,6 +38,7 @@ require_relative 'calcpace/vo2max_estimator'
37
38
  # @see https://github.com/0jonjo/calcpace
38
39
  class Calcpace
39
40
  include AgeGrading
41
+ include EnvironmentalAdjuster
40
42
  include Calculator
41
43
  include CameronPredictor
42
44
  include Checker
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.6
4
+ version: 1.9.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - João Gilberto Saraiva
@@ -37,8 +37,10 @@ files:
37
37
  - lib/calcpace/checker.rb
38
38
  - lib/calcpace/converter.rb
39
39
  - lib/calcpace/converter_chain.rb
40
+ - lib/calcpace/data/environmental_factors.yml
40
41
  - lib/calcpace/data/wma_2023_open_standards.yml
41
42
  - lib/calcpace/data/wma_2023_road.yml
43
+ - lib/calcpace/environmental_adjuster.rb
42
44
  - lib/calcpace/errors.rb
43
45
  - lib/calcpace/pace_calculator.rb
44
46
  - lib/calcpace/pace_converter.rb