diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-21 11:09:29 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-21 11:09:29 +0200 |
| commit | 0e27097e3d706e0e5bd761bf35fe301fd119d2a9 (patch) | |
| tree | c86c0c0cb33608ed9c11a8bd96b133155032fbcd /ride | |
| parent | fc1743af87bb0c91ec1e3bea2f59c3201abcbd22 (diff) | |
refactor(ride): express category thresholds as inclusive min scores
Diffstat (limited to 'ride')
| -rw-r--r-- | ride/climb.go | 38 | ||||
| -rw-r--r-- | ride/difficulty_test.go | 27 |
2 files changed, 49 insertions, 16 deletions
diff --git a/ride/climb.go b/ride/climb.go index 8852cbf..29a4dcf 100644 --- a/ride/climb.go +++ b/ride/climb.go @@ -28,20 +28,19 @@ const ( type categoryInfo struct { label string - maxScore float64 + minScore float64 } -// categories describes each Category: its display label and the exclusive -// Cotacol score ceiling it applies to. A score at or above CategoryOne's -// ceiling is Hors Catégorie. +// categories describes each Category: its display label and the inclusive +// Cotacol score floor at which it starts applying. A score at or above +// CategoryOne's floor is Hors Catégorie; CategoryNo has no floor. var categories = map[Category]categoryInfo{ - CategoryNo: {label: "NO", maxScore: 35}, - CategoryFour: {label: "Cat 4", maxScore: 80}, - CategoryThree: {label: "Cat 3", maxScore: 180}, - CategoryTwo: {label: "Cat 2", maxScore: 250}, - CategoryOne: {label: "Cat 1", maxScore: 600}, - // Hors Catégorie has no ceiling. - CategoryHC: {label: "HC", maxScore: math.Inf(+1)}, + CategoryNo: {label: "NO", minScore: math.Inf(-1)}, + CategoryFour: {label: "Cat 4", minScore: 35}, + CategoryThree: {label: "Cat 3", minScore: 80}, + CategoryTwo: {label: "Cat 2", minScore: 180}, + CategoryOne: {label: "Cat 1", minScore: 250}, + CategoryHC: {label: "HC", minScore: 600}, } // climbDetectionMinimumScore is the climb-detection score below which a @@ -144,16 +143,23 @@ func (c Category) String() string { return categories[c].label } -// CategoryOf returns the difficulty class of a Cotacol score. +// CategoryOf returns the difficulty class of a Cotacol score: the category +// with the highest floor the score reaches. func CategoryOf(cotacol float64) Category { - best := CategoryHC - bestMax := math.Inf(+1) + best := CategoryNo + bestMin := math.Inf(-1) + found := false for category, info := range categories { - if cotacol < info.maxScore && info.maxScore < bestMax { + if cotacol >= info.minScore && (!found || info.minScore > bestMin) { best = category - bestMax = info.maxScore + bestMin = info.minScore + found = true } } + if !found { + // NaN reaches no floor; preserve the original switch default (HC). + return CategoryHC + } return best } diff --git a/ride/difficulty_test.go b/ride/difficulty_test.go index 142ac0a..5773376 100644 --- a/ride/difficulty_test.go +++ b/ride/difficulty_test.go @@ -1,6 +1,7 @@ package ride_test import ( + "math" "testing" "github.com/martinlehoux/biking_home/ride" @@ -48,6 +49,32 @@ func TestCotacolClimbNotAtStart(t *testing.T) { assert.InDelta(t, 98.0, climbs[0].DifficultyScore(), 1e-9) } +func TestCategoryOfBoundaries(t *testing.T) { + cases := []struct { + score float64 + want ride.Category + }{ + {-5, ride.CategoryNo}, + {30, ride.CategoryNo}, + {35, ride.CategoryFour}, // 35 is the first floor, not below it + {79, ride.CategoryFour}, + {80, ride.CategoryThree}, + {179, ride.CategoryThree}, + {180, ride.CategoryTwo}, + {249, ride.CategoryTwo}, + {250, ride.CategoryOne}, + {599, ride.CategoryOne}, + {600, ride.CategoryHC}, + {700, ride.CategoryHC}, + {math.NaN(), ride.CategoryHC}, // reaches no floor, like the original switch default + } + for _, c := range cases { + assert.Equal(t, c.want, ride.CategoryOf(c.score), "score %v", c.score) + } + assert.Equal(t, "Cat 1", ride.CategoryOne.String()) + assert.Equal(t, "NO", ride.CategoryNo.String()) +} + func TestCotacolHautacamClimbfinderReference(t *testing.T) { r, err := ride.ParseFile(parser, "../examples/2022-07-21.Pogacar.gpx") assert.NoError(t, err) |
