aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-23 12:28:24 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-23 12:28:24 +0200
commit2a6189bda8dd9f39d9177ffca2330101e914d8f0 (patch)
tree9e42602902173fa5537d8b44604b25461dc2fe25
parente5e7870eafbfc80f4668d2c0d0995149ddc2f60a (diff)
Suppress overlapping detected climbs when official claims are unsorted
-rw-r--r--ride/climb.go9
-rw-r--r--ride/climb_test.go18
2 files changed, 25 insertions, 2 deletions
diff --git a/ride/climb.go b/ride/climb.go
index f42a9a2..647cf9a 100644
--- a/ride/climb.go
+++ b/ride/climb.go
@@ -270,12 +270,17 @@ func isValidClaim(claim Range, routeLen int) bool {
// freeRegions returns the maximal sub-ranges of [rStart, rEnd] that are not
// covered by any claimed range, after discarding claims that fall outside.
func freeRegions(rStart, rEnd int, claimed []Range) []Range {
- regions := make([]Range, 0, len(claimed)+1)
- current := rStart
+ valid := make([]Range, 0, len(claimed))
for _, claim := range claimed {
if !isValidClaim(claim, rEnd+1) {
continue
}
+ valid = append(valid, claim)
+ }
+ slices.SortFunc(valid, func(a, b Range) int { return a.Start - b.Start })
+ regions := make([]Range, 0, len(valid)+1)
+ current := rStart
+ for _, claim := range valid {
if claim.End < current {
continue
}
diff --git a/ride/climb_test.go b/ride/climb_test.go
index c0d4bd6..6b5c097 100644
--- a/ride/climb_test.go
+++ b/ride/climb_test.go
@@ -169,3 +169,21 @@ func TestDetectClimbsIgnoresOutOfRangeClaim(t *testing.T) {
require.Len(t, climbs, 1)
assert.Equal(t, "0.0km-2.0km: 2.0km at 7.0% (98 pts - Cat 3)", climbs[0].String())
}
+
+func TestDetectClimbsWithUnsortedClaimsDropsOverlappingClimb(t *testing.T) {
+ // Reproduces ride 6 Chemin des Lauriers bug: DetectClimbs must not depend on
+ // caller-provided order. Two official claims out of ID order (Bastides
+ // 2140-2627 before Lauriers 1215-1588) caused freeRegions to skip the
+ // earlier claim and leave the overlapping Cat 4 1352-1601 visible.
+ // Minimal reproduction: two climbs 0-2km and 3-5km separated by a descent.
+ r := RideBuilder{precision: 100}.WithSection("2km at 7%").WithSection("1km at -7%").WithSection("2km at 7%").Build()
+ // Both climbs claimed, but in reverse order (mimics ORDER BY id returning 30-50 before 0-20).
+ // Sorted input correctly yields 0 detected climbs.
+ sorted := []ride.Range{{Start: 0, End: 20}, {Start: 30, End: 50}}
+ assert.Empty(t, r.DetectClimbs(sorted, ride.ClimbSlopeMinimum))
+
+ unsorted := []ride.Range{{Start: 30, End: 50}, {Start: 0, End: 20}}
+ climbs := r.DetectClimbs(unsorted, ride.ClimbSlopeMinimum)
+ // Before fix this fails: freeRegions skips {0,20} because 20 < 30 and returns 1 climb.
+ assert.Empty(t, climbs, "unsorted claimed ranges must still suppress overlapping climbs — expected 0, got %v", climbs)
+}