From 2a6189bda8dd9f39d9177ffca2330101e914d8f0 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Sun, 23 Aug 2026 12:28:24 +0200 Subject: Suppress overlapping detected climbs when official claims are unsorted --- ride/climb.go | 9 +++++++-- ride/climb_test.go | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) (limited to 'ride') 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) +} -- cgit v1.3