Skip to content

feat: training zones in miles, %HRmax fallback, and miles input (v1.11.0) - #81

Merged
0jonjo merged 10 commits into
mainfrom
feat/training-zones-imperial-v1.11.0
Jul 25, 2026
Merged

feat: training zones in miles, %HRmax fallback, and miles input (v1.11.0)#81
0jonjo merged 10 commits into
mainfrom
feat/training-zones-imperial-v1.11.0

Conversation

@0jonjo

@0jonjo 0jonjo commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

Training Zones II — completes the TrainingZones module and adds imperial input support across the gem, unblocking the pending "zones in the UI" work on calcpace_web.

  • Mile pace bands: training_paces and training_paces_from_race accept unit: :mi for output per mile (default stays :km). Sanity-checked against Daniels VDOT 50: T-pace 04:15/km ↔ 06:51/mi.
  • hr_zones_from_max(hr_max:): five heart-rate zones from max HR only (classic %HRmax model, 50–100%) — fallback when resting HR is unknown. Prefer Karvonen hr_zones when hr_rest is available.
  • Race names in training_paces_from_race: '10k', 'marathon', '5mile', ... resolved via RACE_DISTANCES, matching predict_time/race_pace. Also fixes a silent trap: '10k' previously "worked" via String#to_f, while '5mile' became 5.0 km.
  • distance_unit: :mi keyword on the five km-bound methods (estimate_vo2max, estimate_detailed_vo2max, age_grade, age_grade_percent, training_paces_from_race) via a shared normalize_distance_km helper reusing Distance::MI_TO_KM. Calculator methods (pace, time, velocity) stay unit-agnostic by design.

Fully backward compatible (new kwargs default to current behavior). Version bumped to 1.11.0 with CHANGELOG and README updates.

Test plan

  • TDD throughout; 294 runs, 676 assertions, 0 failures (bundle exec rake)
  • RuboCop clean (config: Metrics/ParameterLists.CountKeywordArgs: false)
  • New coverage: mile band scaling, VDOT table values per mile, %HRmax zone boundaries/contiguity, race-name resolution and unknown-name errors, miles input equivalence for vo2max/age grading/training paces

Note: merging to main triggers automatic publish to RubyGems via publish_gem.yml (version file changed).

Copilot AI review requested due to automatic review settings July 25, 2026 11:44
@0jonjo 0jonjo self-assigned this Jul 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

0jonjo added 5 commits July 25, 2026 10:13
Accepting race names made every non-Numeric input fall through to the
RACE_DISTANCES lookup, so numeric strings that worked in v1.10.0 ('10',
'21.0975') started raising "Unknown race". Parse the input with
Float(..., exception: false) first and only treat unparseable input as a
race name.
The 0.001 km match window is narrower than the rounding runners use for
mile distances, so 3.1, 6.2, 13.1 and 26.2 mi were all rejected — only
6-decimal conversions matched. Match with a relative tolerance (0.5% of
the standard distance, 1 m floor) and report the rejected input in the
unit it was given instead of always saying "km".
The unit: and distance_unit: keywords raised a bare ArgumentError, which
escapes `rescue Calcpace::Error` and contradicts the documented error
hierarchy. Both keyword lookups now raise Calcpace::UnsupportedUnitError,
which gained an optional list of supported units for a clear message.

Unit keywords are also normalized with to_s.downcase.to_sym, so 'MI'
works and nil raises the same UnsupportedUnitError instead of a
NoMethodError.
estimate_detailed_vo2max accepted a negative distance whenever
elevation_gain_m was large enough to make the equivalent flat distance
positive; check it right after normalization.

Docs: HR_ZONE_BOUNDARIES is shared by Karvonen and %HRmax zones, so its
comment no longer claims Karvonen only; document that distance_unit: is
ignored for race names in training_paces_from_race and age_grade; list
UnsupportedUnitError in the README and record the behaviour changes of
this release in the CHANGELOG.
Two truths for the mile length coexisted: pace bands used 1609.344 while the
converter table used the truncated 1.60934, so a single call could disagree with
itself about how long a mile is, and the drift ate 10% of the age-grading match
tolerance. Distance, speed, and race-distance factors now derive from one
constant.

Also folds in the rest of the review: distance_unit alongside a race name raises
instead of being silently ignored, race-name lookup normalizes in one place
(whitespace and case now tolerated everywhere, with one message wording), and
ParameterLists keeps a ceiling of 6 instead of ignoring keyword arguments.
@0jonjo
0jonjo merged commit 081971d into main Jul 25, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants