calcpace 1.17.0 → 1.18.1

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: 1b4581502040b195baf42c3dbf64bdb8cc6902d2fd1f4011d7982c18961bbf5f
4
- data.tar.gz: 0dff0cbdd1604680521987db6a08b96ec136546074d76fe7f45b7d4028f9f8bf
3
+ metadata.gz: 70f0253d8a81513d2f48082bc39c96a1c9c324fc29ffb7f64ffde0590e5134c7
4
+ data.tar.gz: 2add58bc0d0f904e1e4cf6e08379e495bdeef2ee74bdcb068b973e4e4f0c995a
5
5
  SHA512:
6
- metadata.gz: 0d4106b957c4da8334d470d67aa5a350f243bac65d0ca5767a356f93e62063a197f7fe338fa2474dcf4079bc5e366f3cfb3a98125820020f3b4bacdb7c4fd856
7
- data.tar.gz: d8f85dd081aa9e1941bcea8931d70d994bb195f9dd5e46fbc47cb2a3ace57df99c545c8283ba76f0a3b4e53b35ff880e8f663d6a464cc85276a3a03e3e7ab1b1
6
+ metadata.gz: 9abfbeae4e5cccd4c9634f0abcfa7bc51a29d317f47db86aceffd4afe04b17a629f0075440cc3729fd0893d614b007d16d3f5318ee3e4c671226832fb2373327
7
+ data.tar.gz: 8d6f1e3e2cd0e10a0436bbdd9ea7ebfc1f0a6973d2e69a83c0419c17ce0ae12c554790d1f80b5fedf2ad047ec6c5092c03bb58710da2bdc76434b71ac9ccc071
data/CHANGELOG.md CHANGED
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.18.1] - 2026-09-06
11
+
12
+ ### Fixed
13
+ - `age_grade_label(Float::NAN)` raised an opaque `NoMethodError` (`undefined
14
+ method '[]' for nil`) instead of a clear error; it now raises
15
+ `ArgumentError`. `Float::INFINITY` is unchanged and still maps to the top
16
+ band, "Approximate World Record Level".
17
+
18
+ ### Changed
19
+ - `AGE_GRADE_LABELS` is now sorted by `min` descending at load time and
20
+ validated to be strictly descending with a `min` of exactly `0.0` on the
21
+ last entry, raising `Calcpace::InvalidDataError` at require time otherwise.
22
+ `age_grade_label` walks the list in order and returns the first match, so
23
+ an out-of-order entry in the data file would have silently misclassified
24
+ percentages, and a missing zero floor raised an opaque `NoMethodError`
25
+ instead of failing loudly.
26
+ - Corrected the provenance note for the age-grade categories in the README
27
+ and in `wma_2023_open_standards.yml`. The WMA / Alan Jones (Howard Grubb)
28
+ tables are numeric age factors and open standards only — they define no
29
+ categories at all. The bands from Local Class (60%) upward follow the
30
+ USATF Masters / National Masters News convention; the three bands below
31
+ 60% remain calcpace's own extension for recreational runners. No band
32
+ boundaries or labels changed, only the attribution text.
33
+ - Documented in the README that the bands apply to the percentage as
34
+ reported by `age_grade` (rounded to one decimal), so calling
35
+ `age_grade_label` directly on an unrounded value can land one band lower
36
+ at an edge.
37
+
38
+ ## [1.18.0] - 2026-09-06
39
+
40
+ ### Breaking
41
+ - **`age_grade[:category]` and `age_grade_label` return different strings for
42
+ anything below 60%.** The old single `"Developing"` band below 60% is now
43
+ three bands: `"Intermediate"` (50–59.9%), `"Recreational"` (40–49.9%) and
44
+ `"Active Beginner"` (below 40%). Most recreational runners grade under 60%,
45
+ so the old catch-all told nearly every one of them the same thing. A caller
46
+ matching on the string `"Developing"` must update — that string is no
47
+ longer returned by either method.
48
+
49
+ ```ruby
50
+ calc.age_grade_label(55.0) # => "Intermediate" (was "Developing")
51
+ calc.age_grade_label(45.0) # => "Recreational" (was "Developing")
52
+ calc.age_grade_label(30.0) # => "Active Beginner" (was "Developing")
53
+ ```
54
+
55
+ The bands at 60% and above are untouched, and the numeric fields —
56
+ `age_grade_percent`, `age_graded_time_seconds` and the rest — are
57
+ unaffected; only the two label-returning methods changed.
58
+
10
59
  ## [1.17.0] - 2026-09-05
11
60
 
12
61
  ### Added
@@ -600,7 +649,9 @@ predictors are untouched.
600
649
 
601
650
  See git history for changes in earlier versions.
602
651
 
603
- [Unreleased]: https://github.com/0jonjo/calcpace/compare/v1.17.0...HEAD
652
+ [Unreleased]: https://github.com/0jonjo/calcpace/compare/v1.18.1...HEAD
653
+ [1.18.1]: https://github.com/0jonjo/calcpace/compare/v1.18.0...v1.18.1
654
+ [1.18.0]: https://github.com/0jonjo/calcpace/compare/v1.17.0...v1.18.0
604
655
  [1.17.0]: https://github.com/0jonjo/calcpace/compare/v1.16.0...v1.17.0
605
656
  [1.16.0]: https://github.com/0jonjo/calcpace/compare/v1.15.0...v1.16.0
606
657
  [1.15.0]: https://github.com/0jonjo/calcpace/compare/v1.14.0...v1.15.0
data/README.md CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
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
+ > **See it in action:** [calcpace.app](https://calcpace.app) — free running calculators, race predictors and a training log, all powered by this gem.
6
+
5
7
  ## Installation
6
8
 
7
9
  ```ruby
8
- gem 'calcpace', '~> 1.12.0'
10
+ gem 'calcpace', '~> 1.18.1'
9
11
  ```
10
12
 
11
13
  ## Usage
@@ -261,6 +263,30 @@ calc.age_grade_percent(5.0, '00:22:30', age: 40, sex: :female) # => 65.2
261
263
  calc.age_grade_label(65.2) # => "Local Class"
262
264
  ```
263
265
 
266
+ `category` (and `age_grade_label`) returns one of:
267
+
268
+ | Age grade | Category |
269
+ | --- | --- |
270
+ | 100%+ | Approximate World Record Level |
271
+ | 90–99.9% | World Class |
272
+ | 80–89.9% | National Class |
273
+ | 70–79.9% | Regional Class |
274
+ | 60–69.9% | Local Class |
275
+ | 50–59.9% | Intermediate |
276
+ | 40–49.9% | Recreational |
277
+ | below 40% | Active Beginner |
278
+
279
+ The WMA / Alan Jones (Howard Grubb) tables are numeric age factors and open
280
+ standards only — they define no categories at all. The bands from Local Class
281
+ (60%) upward follow the USATF Masters / National Masters News convention; the
282
+ three bands below 60% are calcpace's own extension — most recreational
283
+ runners land there, and one catch-all label for everybody under 60% tells
284
+ them nothing.
285
+
286
+ The bands apply to the percentage as reported by `age_grade`, rounded to one
287
+ decimal, so calling `age_grade_label` directly on an unrounded value can land
288
+ one band lower at an edge.
289
+
264
290
  Supported distances: 5K, 10K, half marathon, marathon.
265
291
 
266
292
  A numeric distance within **2%** of one of those is graded as that standard —
@@ -665,6 +691,8 @@ All errors inherit from `Calcpace::Error`:
665
691
  - `Calcpace::InvalidTimeFormatError` — time string not in `HH:MM:SS` or `MM:SS` format
666
692
  - `Calcpace::UnsupportedUnitError` — unknown conversion (`convert`) or unknown
667
693
  `unit:` / `distance_unit:` keyword
694
+ - `Calcpace::InvalidDataError` — the bundled data table failed its load-time
695
+ consistency check (raised on `require`, never for user input)
668
696
 
669
697
  Argument validation that is not about units or numbers raises a plain `ArgumentError`:
670
698
  unknown race names, unsupported age-grading distances, and invalid `age` / `sex` values.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
+ require_relative 'errors'
4
5
 
5
6
  # Module for age-grading race performances with a versioned table
6
7
  #
@@ -28,9 +29,18 @@ module AgeGrading
28
29
  aliases: false).freeze
29
30
  TABLE_VERSION = OPEN_STANDARDS_DATA.fetch('meta').fetch('table_version').freeze
30
31
 
31
- AGE_GRADE_LABELS = OPEN_STANDARDS_DATA.fetch('age_grade_classifications').map do |entry|
32
+ raw_age_grade_labels = OPEN_STANDARDS_DATA.fetch('age_grade_classifications').map do |entry|
32
33
  { min: entry.fetch('min').to_f, label: entry.fetch('label') }
33
- end.freeze
34
+ end
35
+ AGE_GRADE_LABELS = raw_age_grade_labels.sort_by { |entry| -entry[:min] }.freeze
36
+
37
+ age_grade_label_mins = AGE_GRADE_LABELS.map { |entry| entry[:min] }
38
+ unless age_grade_label_mins.all?(&:finite?) &&
39
+ age_grade_label_mins == age_grade_label_mins.uniq &&
40
+ age_grade_label_mins.last == 0.0
41
+ raise Calcpace::InvalidDataError,
42
+ "age_grade_classifications must have unique finite mins ending at 0.0, got #{age_grade_label_mins.inspect}"
43
+ end
34
44
 
35
45
  DISTANCE_TO_METERS = {
36
46
  5.0 => '5000',
@@ -115,6 +125,7 @@ module AgeGrading
115
125
  #
116
126
  # @param percent [Numeric] age-grade percentage
117
127
  # @return [String] category label
128
+ # @raise [ArgumentError] when percent is not numeric, is negative, or is NaN
118
129
  def age_grade_label(percent)
119
130
  percent_value = begin
120
131
  Float(percent)
@@ -123,6 +134,7 @@ module AgeGrading
123
134
  end
124
135
 
125
136
  raise ArgumentError, 'Age-grade percent must be greater than or equal to 0' if percent_value.negative?
137
+ raise ArgumentError, 'Age-grade percent must be a number' if percent_value.nan?
126
138
 
127
139
  AGE_GRADE_LABELS.find { |entry| percent_value >= entry[:min] }[:label]
128
140
  end
@@ -3,6 +3,12 @@ meta:
3
3
  url: "https://howardgrubb.co.uk/athletics/wmatnf23.html"
4
4
  table_version: "WMA_2023_ONE_YEAR_FACTORS_V1"
5
5
 
6
+ # The WMA / Alan Jones (Howard Grubb) tables are numeric age factors and open
7
+ # standards only — they define no categories at all. The bands from Local
8
+ # Class (60%) upward follow the USATF Masters / National Masters News
9
+ # convention; the three bands under 60% are Calcpace's own extension, added
10
+ # to give recreational runners a meaningful label instead of a single
11
+ # catch-all.
6
12
  age_grade_classifications:
7
13
  - min: 100.0
8
14
  label: "Approximate World Record Level"
@@ -14,8 +20,12 @@ age_grade_classifications:
14
20
  label: "Regional Class"
15
21
  - min: 60.0
16
22
  label: "Local Class"
23
+ - min: 50.0
24
+ label: "Intermediate"
25
+ - min: 40.0
26
+ label: "Recreational"
17
27
  - min: 0.0
18
- label: "Developing"
28
+ label: "Active Beginner"
19
29
 
20
30
  open_standards_seconds:
21
31
  M:
@@ -20,6 +20,9 @@ class Calcpace
20
20
  end
21
21
  end
22
22
 
23
+ # Raised when versioned/static gem data fails an internal consistency check
24
+ class InvalidDataError < Error; end
25
+
23
26
  # Raised when an unsupported unit or unit conversion is requested
24
27
  #
25
28
  # @example conversion pair
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Calcpace
4
- VERSION = '1.17.0'
4
+ VERSION = '1.18.1'
5
5
  end
data/lib/calcpace.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'calcpace/errors'
3
4
  require_relative 'calcpace/calculator'
4
5
  require_relative 'calcpace/cameron_predictor'
5
6
  require_relative 'calcpace/age_grading'
@@ -7,7 +8,6 @@ require_relative 'calcpace/environmental_adjuster'
7
8
  require_relative 'calcpace/checker'
8
9
  require_relative 'calcpace/converter'
9
10
  require_relative 'calcpace/converter_chain'
10
- require_relative 'calcpace/errors'
11
11
  require_relative 'calcpace/fitness_predictor'
12
12
  require_relative 'calcpace/lap_analyzer'
13
13
  require_relative 'calcpace/pace_calculator'
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.17.0
4
+ version: 1.18.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - João Gilberto Saraiva