calcpace 1.18.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: def270498a0b653086ac234f0eb1ad4337f961cc2b93729a4bf32c7e48df78ba
4
- data.tar.gz: '075049703348c3cea1194b90a9f64f6cc37552fe360ce51e0d446a9edca5e729'
3
+ metadata.gz: 70f0253d8a81513d2f48082bc39c96a1c9c324fc29ffb7f64ffde0590e5134c7
4
+ data.tar.gz: 2add58bc0d0f904e1e4cf6e08379e495bdeef2ee74bdcb068b973e4e4f0c995a
5
5
  SHA512:
6
- metadata.gz: '083ff71a2666e55994ef03bd8184c435b1006c2029c4af97ab3546683f2d64e1422fb140c6828c001f00ad630bfc05f1caf98af53fbb9ab4d29ec603cad6ef04'
7
- data.tar.gz: '00833e0d9c636730852506c360e08cfec7ecc1de2b1993a4a2560dd0f0941328c48c79e6047ca143babe3545f4b57bfd8e1e47b7c1f864c10d42ab85133390e0'
6
+ metadata.gz: 9abfbeae4e5cccd4c9634f0abcfa7bc51a29d317f47db86aceffd4afe04b17a629f0075440cc3729fd0893d614b007d16d3f5318ee3e4c671226832fb2373327
7
+ data.tar.gz: 8d6f1e3e2cd0e10a0436bbdd9ea7ebfc1f0a6973d2e69a83c0419c17ce0ae12c554790d1f80b5fedf2ad047ec6c5092c03bb58710da2bdc76434b71ac9ccc071
data/CHANGELOG.md CHANGED
@@ -7,14 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [1.18.0] - 2026-09-06
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".
11
17
 
12
18
  ### Changed
13
- - `age_grade_label(percent)` and therefore `age_grade`'s `:category` splits
14
- the old single `"Developing"` band below 60% into three: `"Intermediate"`
15
- (50–59.9%), `"Recreational"` (40–49.9%) and `"Active Beginner"` (below 40%).
16
- Most recreational runners grade under 60%, so the old catch-all told nearly
17
- every one of them the same thing.
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.
18
48
 
19
49
  ```ruby
20
50
  calc.age_grade_label(55.0) # => "Intermediate" (was "Developing")
@@ -22,10 +52,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22
52
  calc.age_grade_label(30.0) # => "Active Beginner" (was "Developing")
23
53
  ```
24
54
 
25
- The bands at 60% and above are untouched, and no numeric output changes
26
- `age_grade_percent`, `age_graded_time_seconds` and the rest are identical.
27
- This is a label-only change, so callers that match on the string
28
- `"Developing"` need updating.
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.
29
58
 
30
59
  ## [1.17.0] - 2026-09-05
31
60
 
@@ -620,7 +649,9 @@ predictors are untouched.
620
649
 
621
650
  See git history for changes in earlier versions.
622
651
 
623
- [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
624
655
  [1.17.0]: https://github.com/0jonjo/calcpace/compare/v1.16.0...v1.17.0
625
656
  [1.16.0]: https://github.com/0jonjo/calcpace/compare/v1.15.0...v1.16.0
626
657
  [1.15.0]: https://github.com/0jonjo/calcpace/compare/v1.14.0...v1.15.0
data/README.md CHANGED
@@ -7,7 +7,7 @@ A Ruby gem for runners: pace, time, and distance calculations, unit conversions,
7
7
  ## Installation
8
8
 
9
9
  ```ruby
10
- gem 'calcpace', '~> 1.18.0'
10
+ gem 'calcpace', '~> 1.18.1'
11
11
  ```
12
12
 
13
13
  ## Usage
@@ -276,10 +276,16 @@ calc.age_grade_label(65.2) # => "Local Clas
276
276
  | 40–49.9% | Recreational |
277
277
  | below 40% | Active Beginner |
278
278
 
279
- The bands from Local Class up are the WMA / Alan Jones convention. Those tables
280
- define nothing below Local Class, so the three bands under 60% are calcpace's
281
- own extension most recreational runners land there, and one catch-all label
282
- for everybody under 60% tells them nothing.
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.
283
289
 
284
290
  Supported distances: 5K, 10K, half marathon, marathon.
285
291
 
@@ -685,6 +691,8 @@ All errors inherit from `Calcpace::Error`:
685
691
  - `Calcpace::InvalidTimeFormatError` — time string not in `HH:MM:SS` or `MM:SS` format
686
692
  - `Calcpace::UnsupportedUnitError` — unknown conversion (`convert`) or unknown
687
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)
688
696
 
689
697
  Argument validation that is not about units or numbers raises a plain `ArgumentError`:
690
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,10 +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 bands from Local Class up follow the WMA / Alan Jones convention. The
7
- # WMA tables define nothing below Local Class, so the three bands under 60%
8
- # are Calcpace's own extension, added to give recreational runners a
9
- # meaningful label instead of a single catch-all.
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.
10
12
  age_grade_classifications:
11
13
  - min: 100.0
12
14
  label: "Approximate World Record Level"
@@ -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.18.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.18.0
4
+ version: 1.18.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - João Gilberto Saraiva