aboutsummaryrefslogtreecommitdiff
path: root/ride
diff options
context:
space:
mode:
Diffstat (limited to 'ride')
-rw-r--r--ride/climb_test.go68
-rw-r--r--ride/difficulty_test.go17
-rw-r--r--ride/ride.go7
3 files changed, 23 insertions, 69 deletions
diff --git a/ride/climb_test.go b/ride/climb_test.go
index 6b5c097..3632da7 100644
--- a/ride/climb_test.go
+++ b/ride/climb_test.go
@@ -1,72 +1,18 @@
package ride_test
import (
- "strconv"
- "strings"
"testing"
- "time"
- "github.com/jftuga/geodist"
+ "github.com/martinlehoux/biking_home/internal/ridetest"
"github.com/martinlehoux/biking_home/ride"
- "github.com/martinlehoux/kagamigo/kcore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-type RideBuilderSection struct {
- slope float64
- distance float64
-}
-
-type RideBuilder struct {
- precision float64
- sections []RideBuilderSection
-}
-
-// Example: 13.4km at 7.5%
-func (b RideBuilder) WithSection(input string) RideBuilder {
- inputs := strings.Split(input, " ")
- kcore.Assert(strings.HasSuffix(inputs[0], "km"), "wrong unit")
- distanceKm, err := strconv.ParseFloat(strings.TrimSuffix(inputs[0], "km"), 64)
- kcore.Expect(err, "faile to parse kms")
- kcore.Assert(strings.HasSuffix(inputs[2], "%"), "wrong unit")
- slopePercent, err := strconv.ParseFloat(strings.TrimSuffix(inputs[2], "%"), 64)
- kcore.Expect(err, "failed to parse %")
-
- b.sections = append(b.sections, RideBuilderSection{
- slope: slopePercent / 100,
- distance: distanceKm * 1000,
- })
-
- return b
-}
-
-func (b RideBuilder) Build() ride.Ride {
- distances := []float64{0}
- elevations := []float64{0}
- coords := []geodist.Coord{{}}
- timestamps := []time.Time{{}}
- distance := 0.0
- elevation := 0.0
- for _, section := range b.sections {
- curSecDist := 0.0
- for curSecDist < section.distance {
- curSecDist += b.precision
- elevation += b.precision * section.slope
- distances = append(distances, distance+curSecDist)
- elevations = append(elevations, elevation)
- coords = append(coords, geodist.Coord{})
- timestamps = append(timestamps, timestamps[len(timestamps)-1].Add(time.Second))
- }
- distance += curSecDist
- }
- return ride.FromColumns(distances, elevations, coords, timestamps)
-}
-
var parser = ride.GPXRideParser{}
func TestClimbThenFalseFlat(t *testing.T) {
- r := RideBuilder{precision: 100}.WithSection("2km at 7%").WithSection("10km at 1%").Build()
+ r := ridetest.MustRide("2km at 7% | 10km at 1%")
climbs := r.DetectClimbs(nil, ride.ClimbSlopeMinimum)
assert.Len(t, climbs, 1)
@@ -74,7 +20,7 @@ func TestClimbThenFalseFlat(t *testing.T) {
}
func TestSmallClimbWithDescentInsideFalseFlat(t *testing.T) {
- r := RideBuilder{precision: 100}.WithSection("10km at 1%").WithSection("2km at 7%").WithSection("1km at -7%").WithSection("10km at 1%").Build()
+ r := ridetest.MustRide("10km at 1% | 2km at 7% | 1km at -7% | 10km at 1%")
climbs := r.DetectClimbs(nil, ride.ClimbSlopeMinimum)
assert.Len(t, climbs, 1)
@@ -147,7 +93,7 @@ func TestAlpesVerdonTour20230617(t *testing.T) {
func TestDetectClimbsSkipsMiddleClaim(t *testing.T) {
// A climb, a false flat giving a saddle, then a climb again. Claiming the
// saddle's descent base should leave both climbs detected separately.
- r := RideBuilder{precision: 100}.WithSection("2km at 7%").WithSection("1km at -7%").WithSection("2km at 7%").Build()
+ r := ridetest.MustRide("2km at 7% | 1km at -7% | 2km at 7%")
climbs := r.DetectClimbs([]ride.Range{{Start: 20, End: 20}}, ride.ClimbSlopeMinimum)
// No free region falls under the min distance except the two climbs.
@@ -158,13 +104,13 @@ func TestDetectClimbsSkipsMiddleClaim(t *testing.T) {
func TestDetectClimbsOverlappingClaimDropsClimb(t *testing.T) {
// Claiming the ascent region entirely swallows the climb.
- r := RideBuilder{precision: 100}.WithSection("2km at 7%").WithSection("20km at 0%").Build()
+ r := ridetest.MustRide("2km at 7% | 20km at 0%")
climbs := r.DetectClimbs([]ride.Range{{Start: 0, End: 40}}, ride.ClimbSlopeMinimum)
assert.Empty(t, climbs)
}
func TestDetectClimbsIgnoresOutOfRangeClaim(t *testing.T) {
- r := RideBuilder{precision: 100}.WithSection("2km at 7%").WithSection("20km at 0%").Build()
+ r := ridetest.MustRide("2km at 7% | 20km at 0%")
climbs := r.DetectClimbs([]ride.Range{{Start: -5, End: -1}}, ride.ClimbSlopeMinimum)
require.Len(t, climbs, 1)
assert.Equal(t, "0.0km-2.0km: 2.0km at 7.0% (98 pts - Cat 3)", climbs[0].String())
@@ -176,7 +122,7 @@ func TestDetectClimbsWithUnsortedClaimsDropsOverlappingClimb(t *testing.T) {
// 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()
+ r := ridetest.MustRide("2km at 7% | 1km at -7% | 2km at 7%")
// 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}}
diff --git a/ride/difficulty_test.go b/ride/difficulty_test.go
index 2e7cf56..bd316b4 100644
--- a/ride/difficulty_test.go
+++ b/ride/difficulty_test.go
@@ -4,35 +4,36 @@ import (
"math"
"testing"
+ "github.com/martinlehoux/biking_home/internal/ridetest"
"github.com/martinlehoux/biking_home/ride"
"github.com/stretchr/testify/assert"
)
func TestCotacolConstantClimb(t *testing.T) {
- r := RideBuilder{precision: 100}.WithSection("2km at 7%").Build()
+ r := ridetest.MustRide("2km at 7%")
assert.InDelta(t, 98.0, ride.Cotacol(r), 1e-9)
}
func TestCotacolIgnoresDescent(t *testing.T) {
- r := RideBuilder{precision: 100}.WithSection("2km at 7%").WithSection("2km at -7%").Build()
+ r := ridetest.MustRide("2km at 7% | 2km at -7%")
assert.InDelta(t, 98.0, ride.Cotacol(r), 1e-9)
}
func TestCotacolFlatIsZero(t *testing.T) {
- r := RideBuilder{precision: 100}.WithSection("5km at 0%").Build()
+ r := ridetest.MustRide("5km at 0%")
assert.Zero(t, ride.Cotacol(r))
}
func TestCotacolSteepShortCountsMore(t *testing.T) {
- steep := RideBuilder{precision: 100}.WithSection("0.5km at 14%").Build()
- long := RideBuilder{precision: 100}.WithSection("1km at 7%").Build()
+ steep := ridetest.MustRide("0.5km at 14%")
+ long := ridetest.MustRide("1km at 7%")
assert.InDelta(t, 98.0, ride.Cotacol(steep), 1e-9)
assert.InDelta(t, 49.0, ride.Cotacol(long), 1e-9)
}
func TestCotacolPrecisionInvariant(t *testing.T) {
- coarse := RideBuilder{precision: 100}.WithSection("3km at 5%").Build()
- fine := RideBuilder{precision: 50}.WithSection("3km at 5%").Build()
+ coarse := ridetest.MustRide("3km at 5%")
+ fine := ridetest.MustRide("3km at 5%", ridetest.Sampling(50))
assert.InDelta(t, ride.Cotacol(coarse), ride.Cotacol(fine), 1e-9)
}
@@ -43,7 +44,7 @@ func TestCotacolExampleRide(t *testing.T) {
}
func TestCotacolClimbNotAtStart(t *testing.T) {
- r := RideBuilder{precision: 100}.WithSection("1km at 0%").WithSection("2km at 7%").Build()
+ r := ridetest.MustRide("1km at 0% | 2km at 7%")
climbs := r.DetectClimbs(nil, ride.ClimbSlopeMinimum)
assert.Len(t, climbs, 1)
assert.InDelta(t, 98.0, climbs[0].DifficultyScore(), 1e-9)
diff --git a/ride/ride.go b/ride/ride.go
index 1de8b07..7863878 100644
--- a/ride/ride.go
+++ b/ride/ride.go
@@ -161,6 +161,13 @@ func FromColumns(distances []float64, elevations []float64, coords []geodist.Coo
return fromColumns(distances, elevations, coords, timestamps, nil, nil, nil)
}
+// FromColumnsWithMetrics builds a Ride from its columns, including optional
+// heart-rate, cadence and power components of the same length as the other
+// columns.
+func FromColumnsWithMetrics(distances []float64, elevations []float64, coords []geodist.Coord, timestamps []time.Time, heartRates, cadences, powers []float64) Ride {
+ return fromColumns(distances, elevations, coords, timestamps, heartRates, cadences, powers)
+}
+
func fromColumns(distances []float64, elevations []float64, coords []geodist.Coord, timestamps []time.Time, heartRates, cadences, powers []float64) Ride {
ride := Ride{
distances: distances,