Skip to content
Go back

A loop that does five things is five problems

The problem: A loop that calculates distance, filters GPS errors, smooths elevation, and classifies terrain is doing four jobs. Adding a fifth means reading all four first.

public ActivitySummary ProcessGpxData(List<GpxPoint> gpxPoints)
{
    double totalDistance = 0;
    double totalElevationGain = 0;
    List<GpxPoint> processedPoints = new List<GpxPoint>();

    for (int i = 0; i < gpxPoints.Count; i++)
    {
        var point = gpxPoints[i];

        if (i > 0)
        {
            var prevPoint = gpxPoints[i - 1];

            double segmentDistance = CalculateHaversineDistance(prevPoint, point);
            double speed = CalculateSpeed(segmentDistance, point.Timestamp - prevPoint.Timestamp);

            if (speed > 30) continue;

            double elevationDelta = point.Elevation - prevPoint.Elevation;
            if (elevationDelta > 0.5)
                totalElevationGain += elevationDelta;

            totalDistance += segmentDistance;

            point.SegmentDistance = segmentDistance;
            point.Speed = speed;
            point.SmoothedElevation = CalculateSmoothedElevation(i, gpxPoints);
            point.TerrainType = DetermineTerrainType(elevationDelta, speed);
        }
        else
        {
            point.SegmentDistance = 0;
            point.Speed = 0;
            point.SmoothedElevation = point.Elevation;
            point.TerrainType = "flat";
        }

        processedPoints.Add(point);
    }

    return new ActivitySummary
    {
        ProcessedPoints = processedPoints,
        TotalDistance = totalDistance,
        TotalElevationGain = totalElevationGain
    };
}

Every new requirement becomes a merge conflict with the existing logic.


The fix: Break the loop into a pipeline. One function per step, each receiving and returning the same intermediate structure.

public ActivitySummary ProcessGpxData(List<GpxPoint> gpxPoints)
{
    var data = MapToIntermediateData(gpxPoints);

    data = CalculateSegmentMetrics(data);
    data = FilterGpsGlitches(data);
    data = ProcessElevationData(data);
    data = ApplyDataSmoothing(data);
    data = ClassifyTerrainTypes(data);
    data = CalculateAccumulatedTotals(data);

    return GenerateActivitySummary(data);
}

Each step has one job. The sequence is explicit. The intermediate structure carries state between steps:

private class GpxIntermediateData
{
    // Original data
    public double Latitude { get; set; }
    public double Longitude { get; set; }
    public double Elevation { get; set; }
    public DateTime Timestamp { get; set; }

    // Derived/accumulated
    public double SegmentDistance { get; set; }
    public double Speed { get; set; }
    public double ElevationDelta { get; set; }
    public bool IsGpsGlitch { get; set; }
    public double SmoothedElevation { get; set; }
    public string TerrainType { get; set; }
    public double AccumulatedDistance { get; set; }
    public double AccumulatedElevationGain { get; set; }
    public double AccumulatedElevationLoss { get; set; }
}

Implementation: Each step transforms the list and passes it forward.

private List<GpxIntermediateData> MapToIntermediateData(List<GpxPoint> gpxPoints)
{
    return gpxPoints.Select(p => new GpxIntermediateData
    {
        Latitude = p.Latitude,
        Longitude = p.Longitude,
        Elevation = p.Elevation,
        Timestamp = p.Timestamp
    }).ToList();
}

private List<GpxIntermediateData> CalculateSegmentMetrics(List<GpxIntermediateData> points)
{
    for (int i = 1; i < points.Count; i++)
    {
        var current = points[i];
        var previous = points[i - 1];

        current.SegmentDistance = CalculateHaversineDistance(previous, current);
        current.Speed = CalculateSpeed(current.SegmentDistance,
                                       current.Timestamp - previous.Timestamp);
        current.ElevationDelta = current.Elevation - previous.Elevation;
    }

    return points;
}

private List<GpxIntermediateData> FilterGpsGlitches(List<GpxIntermediateData> points)
{
    foreach (var point in points)
        point.IsGpsGlitch = point.Speed > 30;

    return points.Where(p => !p.IsGpsGlitch).ToList();
}

When to reach for this: Any loop that mixes filtering, calculating, and accumulating. Booking workflows, sensor ingestion, reporting pipelines — the pattern fits wherever a single pass is doing the work of several.


TLDR: Complex loops grow because extending them feels cheaper than splitting them. A transformation pipeline forces the split upfront. Each step gets a name, a clear input, and a testable output — and adding a new requirement means adding a new step, not reading the whole thing again.


Share this post on:

Previous Post
Build the throwaway prototype first
Next Post
Serilog Correlation IDs for background tasks in .NET Core