calcpace 1.9.10 → 1.10.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: 0e8d5ca7f00eaacd53af3a268a695a80523a497eedf9caebbd8c5459ea261808
4
- data.tar.gz: 974e29530df057bcc3317c66d78343af96c30d246c076c51115a0bcfe93c0736
3
+ metadata.gz: 6d89d16ea156af81fa6cd59976eebf680d435b38b41a65fcd46ee79ec2ea5fe6
4
+ data.tar.gz: b79d25c3f1da091543d1f1a930350bdd846ff15369f7e8476303e99aeccbcc79
5
5
  SHA512:
6
- metadata.gz: 30f1a094b09bb131ad43923f6cabda560c4bb64b811c6b621d6b87e92f9f53101f9ff94f5a5c62926c003b743b53b3a2967e387c06effef1fd463de2481a0f8d
7
- data.tar.gz: 38eb834c5acb4d18dc9729159733dc0ff72ec56d5e7391867c1182f1c2abe62dd690348afdee0686a5aa25f7570c3fe1e176d1cdd99671ad03e9c44eaa57cf7e
6
+ metadata.gz: 1582ebc32a95e1e7702881c0efc16da68768a6616382bb1fb8102e83c9be665ac3f8eb3524f1581f0ec82db1d05e1db2eedf2217b822f959a2dc62711e6b8aa5
7
+ data.tar.gz: 9b95fd1feacc28b87a6477788a76bf141651cfa0ee254d4923ccc44cb9f6af63feb666046835c01d3ff5ddd65fb1632ef5d7825b540933ea3295f1ae406f55d3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.10.0] - 2026-07-11
11
+
12
+ ### Added
13
+ - Training Zones module (`TrainingZones`)
14
+ - `training_paces(vo2max)`: personalized Easy/Marathon/Threshold/Interval/Repetition
15
+ pace bands per km, inverting the Daniels & Gilbert velocity equation
16
+ - `training_paces_from_race(distance_km, time)`: pace bands straight from a race result
17
+ - `hr_zones(hr_max:, hr_rest:)`: five Karvonen (Heart Rate Reserve) heart-rate zones
18
+ - Structured results (`PaceBand`, `HrZone`) with seconds and clock formats
19
+
10
20
  ## [1.9.10] - 2026-07-09
11
21
 
12
22
  ### Changed
@@ -205,7 +215,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
205
215
 
206
216
  See git history for changes in earlier versions.
207
217
 
208
- [Unreleased]: https://github.com/0jonjo/calcpace/compare/v1.9.6...HEAD
218
+ [Unreleased]: https://github.com/0jonjo/calcpace/compare/v1.10.0...HEAD
219
+ [1.10.0]: https://github.com/0jonjo/calcpace/compare/v1.9.10...v1.10.0
209
220
  [1.9.6]: https://github.com/0jonjo/calcpace/compare/v1.9.5...v1.9.6
210
221
  [1.9.5]: https://github.com/0jonjo/calcpace/compare/v1.9.4...v1.9.5
211
222
  [1.6.0]: https://github.com/0jonjo/calcpace/releases/tag/v1.6.0
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # Calcpace [![Gem Version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=rb&r=r&ts=1683906897&type=6e&v=1.9.8&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.10.0&x2=0)](https://badge.fury.io/rb/calcpace)
2
2
 
3
- A Ruby gem for running and cycling calculations: pace, time, distance, unit conversions, race predictions, GPS track analysis, and VO2max estimation.
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.9.8'
8
+ gem 'calcpace', '~> 1.10.0'
9
9
  ```
10
10
 
11
11
  ## Usage
@@ -276,6 +276,34 @@ easy.value # => 29.3 (underestimates real aerobic capacity)
276
276
 
277
277
  ---
278
278
 
279
+ ### Training Zones
280
+
281
+ Personalized training paces (Daniels' Running Formula) and Karvonen heart-rate zones:
282
+
283
+ ```ruby
284
+ zones = calc.training_paces(50.0)
285
+ zones[:threshold].fast_clock # => "00:04:15" per km
286
+ zones[:easy].slow_clock # => "00:05:52" per km
287
+
288
+ calc.training_paces_from_race(10.0, '00:40:00') # from a recent race result
289
+
290
+ calc.hr_zones(hr_max: 190, hr_rest: 55)
291
+ # => [#<struct zone=1, min_bpm=123, max_bpm=136>, ... zone=5, max_bpm=190]
292
+ ```
293
+
294
+ | Zone | %VO2max | Purpose |
295
+ |------|---------|---------|
296
+ | Easy | 59–74% | Base building, recovery |
297
+ | Marathon | 75–84% | Marathon race pace |
298
+ | Threshold | 83–88% | Lactate threshold, tempo runs |
299
+ | Interval | 95–100% | VO2max development |
300
+ | Repetition | 105–110% | Speed and running economy |
301
+
302
+ Pace accuracy vs published VDOT tables: within a few seconds per km
303
+ (threshold matches exactly; easy band is a range heuristic).
304
+
305
+ ---
306
+
279
307
  ### Other Utilities
280
308
 
281
309
  ```ruby
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 = 'Pace, distance, GPS track analysis, and VO2max calculations for runners and cyclists.'
12
- spec.description = 'Ruby gem for running and cycling calculations: pace, time, distance, ' \
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
- 'and VO2max estimation (Daniels & Gilbert).'
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'
@@ -0,0 +1,123 @@
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).
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
+ # Heart-rate zone boundaries as fractions of Heart Rate Reserve (Karvonen)
27
+ HR_ZONE_BOUNDARIES = [0.50, 0.60, 0.70, 0.80, 0.90, 1.00].freeze
28
+
29
+ # One heart-rate training zone (1 = recovery … 5 = maximal)
30
+ HrZone = Struct.new(:zone, :min_bpm, :max_bpm)
31
+
32
+ # Derives training pace bands from a VO2max value
33
+ #
34
+ # @param vo2max [Numeric] VO2max in ml/kg/min (must be > 0)
35
+ # @return [Hash{Symbol => PaceBand}] keys: :easy, :marathon, :threshold,
36
+ # :interval, :repetition — paces per km
37
+ # @raise [Calcpace::NonPositiveInputError] if vo2max is not positive
38
+ #
39
+ # @example
40
+ # calc.training_paces(50.0)[:threshold].fast_clock #=> "00:04:15"
41
+ def training_paces(vo2max)
42
+ check_positive(vo2max.to_f, 'VO2max')
43
+
44
+ TRAINING_INTENSITIES.transform_values do |band|
45
+ slow = pace_seconds_at_pct(vo2max.to_f, band[:low])
46
+ fast = pace_seconds_at_pct(vo2max.to_f, band[:high])
47
+
48
+ PaceBand.new(
49
+ slow_seconds: slow,
50
+ fast_seconds: fast,
51
+ slow_clock: convert_to_clocktime(slow),
52
+ fast_clock: convert_to_clocktime(fast)
53
+ )
54
+ end
55
+ end
56
+
57
+ # Derives training pace bands from a recent race result
58
+ #
59
+ # Convenience wrapper: estimates VO2max via Daniels & Gilbert
60
+ # (see Vo2maxEstimator#estimate_vo2max) and derives the bands from it.
61
+ #
62
+ # @param distance_km [Numeric] race distance in kilometres (must be > 0)
63
+ # @param time [String, Integer] finish time as "HH:MM:SS" / "MM:SS" or total seconds
64
+ # @return [Hash{Symbol => PaceBand}] same shape as #training_paces
65
+ # @raise [Calcpace::NonPositiveInputError] if distance or time are not positive
66
+ # @raise [Calcpace::InvalidTimeFormatError] if time string is malformed
67
+ #
68
+ # @example
69
+ # calc.training_paces_from_race(10.0, '00:40:00')[:easy].slow_clock #=> "00:05:42"
70
+ def training_paces_from_race(distance_km, time)
71
+ training_paces(estimate_vo2max(distance_km, time))
72
+ end
73
+
74
+ # Computes the five Karvonen heart-rate training zones
75
+ #
76
+ # target_bpm = hr_rest + pct * (hr_max - hr_rest)
77
+ #
78
+ # @param hr_max [Numeric] maximum heart rate in bpm (must be > 0)
79
+ # @param hr_rest [Numeric] resting heart rate in bpm (must be > 0 and < hr_max)
80
+ # @return [Array<HrZone>] five contiguous zones from Z1 (50–60% HRR) to Z5 (90–100% HRR)
81
+ # @raise [Calcpace::NonPositiveInputError] if any rate is not positive
82
+ # @raise [Calcpace::Error] if hr_rest >= hr_max
83
+ #
84
+ # @example
85
+ # calc.hr_zones(hr_max: 190, hr_rest: 55).last.max_bpm #=> 190
86
+ def hr_zones(hr_max:, hr_rest:)
87
+ max = hr_max.to_f
88
+ rest = hr_rest.to_f
89
+ check_heart_rates(max, rest)
90
+
91
+ reserve = max - rest
92
+ points = HR_ZONE_BOUNDARIES.map { |pct| (rest + (pct * reserve)).round }
93
+
94
+ points.each_cons(2).with_index(1).map do |(min_bpm, max_bpm), zone|
95
+ HrZone.new(zone: zone, min_bpm: min_bpm, max_bpm: max_bpm)
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def check_heart_rates(hr_max, hr_rest)
102
+ check_positive(hr_max, 'Maximum heart rate')
103
+ check_positive(hr_rest, 'Resting heart rate')
104
+ return if hr_rest < hr_max
105
+
106
+ raise Calcpace::Error,
107
+ "Resting heart rate (#{hr_rest}) must be lower than maximum heart rate (#{hr_max})"
108
+ end
109
+
110
+ # Inverts Daniels & Gilbert: velocity (m/min) that demands a given VO2
111
+ def velocity_at_vo2(vo2)
112
+ a = 0.000104
113
+ b = 0.182258
114
+ c = -(4.60 + vo2)
115
+
116
+ (-b + Math.sqrt((b**2) - (4 * a * c))) / (2 * a)
117
+ end
118
+
119
+ def pace_seconds_at_pct(vo2max, pct)
120
+ velocity = velocity_at_vo2(vo2max * pct)
121
+ (60_000.0 / velocity).round
122
+ end
123
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Calcpace
4
- VERSION = '1.9.10'
4
+ VERSION = '1.10.0'
5
5
  end
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.9.10
4
+ version: 1.10.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 running and cycling calculations: pace, time, distance,
13
- unit conversions (30+ units), race predictions (Riegel & Cameron), GPS track analysis
14
- (Haversine, elevation gain, per-km splits), and VO2max estimation (Daniels & Gilbert).'
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
@@ -72,6 +75,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
75
  requirements: []
73
76
  rubygems_version: 4.0.10
74
77
  specification_version: 4
75
- summary: Pace, distance, GPS track analysis, and VO2max calculations for runners and
76
- cyclists.
78
+ summary: 'Running calculations: pace, race predictions, GPS track analysis, VO2max,
79
+ and training zones.'
77
80
  test_files: []