calcpace 1.9.9 → 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 +4 -4
- data/CHANGELOG.md +17 -1
- data/README.md +31 -3
- data/calcpace.gemspec +5 -4
- data/lib/calcpace/track_calculator.rb +8 -9
- data/lib/calcpace/training_zones.rb +123 -0
- data/lib/calcpace/version.rb +1 -1
- data/lib/calcpace.rb +2 -0
- metadata +9 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d89d16ea156af81fa6cd59976eebf680d435b38b41a65fcd46ee79ec2ea5fe6
|
|
4
|
+
data.tar.gz: b79d25c3f1da091543d1f1a930350bdd846ff15369f7e8476303e99aeccbcc79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1582ebc32a95e1e7702881c0efc16da68768a6616382bb1fb8102e83c9be665ac3f8eb3524f1581f0ec82db1d05e1db2eedf2217b822f959a2dc62711e6b8aa5
|
|
7
|
+
data.tar.gz: 9b95fd1feacc28b87a6477788a76bf141651cfa0ee254d4923ccc44cb9f6af63feb666046835c01d3ff5ddd65fb1632ef5d7825b540933ea3295f1ae406f55d3
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ 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
|
+
|
|
20
|
+
## [1.9.10] - 2026-07-09
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Refactor `TrackCalculator`: extract a single `dig_key` helper for symbol/string key access, removing duplicated fallback logic across coordinate, elevation, and time readers (no behavior change)
|
|
24
|
+
|
|
10
25
|
## [1.9.9] - 2026-06-17
|
|
11
26
|
|
|
12
27
|
### Changed
|
|
@@ -200,7 +215,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
200
215
|
|
|
201
216
|
See git history for changes in earlier versions.
|
|
202
217
|
|
|
203
|
-
[Unreleased]: https://github.com/0jonjo/calcpace/compare/v1.
|
|
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
|
|
204
220
|
[1.9.6]: https://github.com/0jonjo/calcpace/compare/v1.9.5...v1.9.6
|
|
205
221
|
[1.9.5]: https://github.com/0jonjo/calcpace/compare/v1.9.4...v1.9.5
|
|
206
222
|
[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
|
|
@@ -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 = '
|
|
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'
|
|
@@ -74,8 +74,8 @@ module TrackCalculator
|
|
|
74
74
|
return 0.0 if points.nil? || points.size < 2
|
|
75
75
|
|
|
76
76
|
total = points.each_cons(2).sum do |a, b|
|
|
77
|
-
haversine_distance(
|
|
78
|
-
|
|
77
|
+
haversine_distance(dig_key(a, :lat), dig_key(a, :lon),
|
|
78
|
+
dig_key(b, :lat), dig_key(b, :lon))
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
total.round(2)
|
|
@@ -180,25 +180,24 @@ module TrackCalculator
|
|
|
180
180
|
end
|
|
181
181
|
end
|
|
182
182
|
|
|
183
|
-
def
|
|
183
|
+
def dig_key(point, key)
|
|
184
184
|
point[key] || point[key.to_s]
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
def fetch_ele(point)
|
|
188
|
-
|
|
189
|
-
val&.to_f
|
|
188
|
+
dig_key(point, :ele)&.to_f
|
|
190
189
|
end
|
|
191
190
|
|
|
192
191
|
def validate_points_have_time(points)
|
|
193
192
|
points.each_with_index do |pt, i|
|
|
194
|
-
next if pt
|
|
193
|
+
next if dig_key(pt, :time)
|
|
195
194
|
|
|
196
195
|
raise ArgumentError, "Point at index #{i} is missing :time key required for splits"
|
|
197
196
|
end
|
|
198
197
|
end
|
|
199
198
|
|
|
200
199
|
def point_time(point)
|
|
201
|
-
t = point
|
|
200
|
+
t = dig_key(point, :time)
|
|
202
201
|
t.respond_to?(:to_f) ? t.to_f : t
|
|
203
202
|
end
|
|
204
203
|
|
|
@@ -228,8 +227,8 @@ module TrackCalculator
|
|
|
228
227
|
end
|
|
229
228
|
|
|
230
229
|
def process_segment(point_a, point_b, split_km, state)
|
|
231
|
-
segment_km = haversine_distance(
|
|
232
|
-
|
|
230
|
+
segment_km = haversine_distance(dig_key(point_a, :lat), dig_key(point_a, :lon),
|
|
231
|
+
dig_key(point_b, :lat), dig_key(point_b, :lon))
|
|
233
232
|
state[:accumulated_km] += segment_km
|
|
234
233
|
|
|
235
234
|
while state[:accumulated_km] >= split_km * state[:split_number]
|
|
@@ -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
|
data/lib/calcpace/version.rb
CHANGED
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.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
|
|
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
|
|
@@ -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:
|
|
76
|
-
|
|
78
|
+
summary: 'Running calculations: pace, race predictions, GPS track analysis, VO2max,
|
|
79
|
+
and training zones.'
|
|
77
80
|
test_files: []
|