Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* [#103](https://github.com/dblock/strava-ruby-client/pull/103): Migrate Danger to use the `danger-pr-comment` reusable workflow - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).
* [#104](https://github.com/dblock/strava-ruby-client/pull/104): Fixes `Danger Comment` workflow failing with a `contents: none` permissions error - [@dblock](https://github.com/dblock).
* [#102](https://github.com/dblock/strava-ruby-client/pull/102): Changes default API base URL to `https://www.api-v3.strava.com` and adds `revoke`, deprecating `deauthorize`, per Strava's June 2026/2027 developer program changes - [@dblock](https://github.com/dblock).
* [#105](https://github.com/dblock/strava-ruby-client/pull/105): Adds `total_elevation_gain`, `total_elevation_loss` and formatted helpers to `Strava::Models::Stream`, computed from altitude stream data - [@dblock](https://github.com/dblock).
* Your contribution here.

### 3.0.0 (2025/10/24)
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,17 @@ streams.latlng # => Strava::Models::Stream
streams.altitude # => Strava::Models::Stream
```

The Strava API doesn't return a total elevation loss (descent) property for streams, but it can be computed from an altitude stream's data.

```ruby
streams = client.activity_streams(1946417534, keys: %w[altitude]) # => Strava::Models::StreamSet

streams.altitude.total_elevation_gain # => 725.3
streams.altitude.total_elevation_loss # => 680.1
streams.altitude.total_elevation_gain_s # => "725.3m"
streams.altitude.total_elevation_loss_s # => "680.1m"
```

#### Get Activity Streams

Returns the given activity's streams.
Expand Down
65 changes: 65 additions & 0 deletions lib/strava/models/stream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ module Models
# puts "#{lat}, #{lng}"
# end
#
# @example Computing elevation gain and loss from an altitude stream
# streams = client.activity_streams(1234567890, keys: 'altitude')
# puts streams.altitude.total_elevation_gain_s # => "725.3m"
# puts streams.altitude.total_elevation_loss_s # => "680.1m"
#
class Stream < Strava::Models::Response
# @return [Integer] Total number of data points in the original stream before any resampling
property 'original_size'
Expand All @@ -49,6 +54,66 @@ class Stream < Strava::Models::Response
# - moving: Array of booleans (true if moving, false if stopped)
# - grade_smooth: Array of floats (grade as percentage)
property 'data'

#
# Returns the total elevation gain, calculated as the sum of all positive
# elevation increases between consecutive data points.
#
# This is only meaningful when the stream represents altitude data.
#
# @return [Float, nil] Total elevation gain in meters, or nil if data is not present
#
# @example
# streams = client.activity_streams(1234567890, keys: 'altitude')
# puts streams.altitude.total_elevation_gain # => 725.3
#
def total_elevation_gain
return if data.nil?

data.each_cons(2).sum { |a, b| b > a ? b - a : 0 }
end

#
# Returns the total elevation loss, calculated as the sum of all negative
# elevation decreases between consecutive data points.
#
# This is only meaningful when the stream represents altitude data.
#
# @return [Float, nil] Total elevation loss in meters, or nil if data is not present
#
# @example
# streams = client.activity_streams(1234567890, keys: 'altitude')
# puts streams.altitude.total_elevation_loss # => 680.1
#
def total_elevation_loss
return if data.nil?

data.each_cons(2).sum { |a, b| a > b ? a - b : 0 }
end

#
# Returns formatted total elevation gain in meters.
#
# @return [String, nil] Formatted elevation (e.g., "725.3m")
#
def total_elevation_gain_s
gain = total_elevation_gain
return if gain.nil?

format('%gm', format('%.1f', gain))
end

#
# Returns formatted total elevation loss in meters.
#
# @return [String, nil] Formatted elevation (e.g., "680.1m")
#
def total_elevation_loss_s
loss = total_elevation_loss
return if loss.nil?

format('%gm', format('%.1f', loss))
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
expect(streams.distance).to be_a Strava::Models::Stream
expect(streams.latlng).to be_a Strava::Models::Stream
expect(streams.altitude).to be_a Strava::Models::Stream
expect(streams.altitude.total_elevation_gain).to eq 0
expect(streams.altitude.total_elevation_loss).to eq 3.3
expect(streams.altitude.total_elevation_gain_s).to eq '0m'
expect(streams.altitude.total_elevation_loss_s).to eq '3.3m'
end

it 'returns multiple keys by id', vcr: { cassette_name: 'client/segment_streams_keys' } do
Expand All @@ -46,5 +50,7 @@
expect(streams.distance).to be_a Strava::Models::Stream
expect(streams.latlng).to be_a Strava::Models::Stream
expect(streams.altitude).to be_a Strava::Models::Stream
expect(streams.altitude.total_elevation_gain).to eq 0
expect(streams.altitude.total_elevation_loss).to eq 3.3
end
end
63 changes: 63 additions & 0 deletions spec/strava/models/stream_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Strava::Models::Stream do
describe '#total_elevation_gain' do
it 'sums positive elevation changes' do
stream = described_class.new('data' => [100, 105, 102, 110, 108])
expect(stream.total_elevation_gain).to eq 13
end

it 'returns 0 when there is no elevation gain' do
stream = described_class.new('data' => [100, 90, 80])
expect(stream.total_elevation_gain).to eq 0
end

it 'returns nil when data is nil' do
stream = described_class.new(nil)
expect(stream.total_elevation_gain).to be_nil
end
end

describe '#total_elevation_loss' do
it 'sums negative elevation changes' do
stream = described_class.new('data' => [100, 105, 102, 110, 108])
expect(stream.total_elevation_loss).to eq 5
end

it 'returns 0 when there is no elevation loss' do
stream = described_class.new('data' => [80, 90, 100])
expect(stream.total_elevation_loss).to eq 0
end

it 'returns nil when data is nil' do
stream = described_class.new(nil)
expect(stream.total_elevation_loss).to be_nil
end
end

describe '#total_elevation_gain_s' do
it 'returns formatted elevation gain' do
stream = described_class.new('data' => [100.0, 105.3, 102.1, 110.7, 108.2])
expect(stream.total_elevation_gain_s).to eq '13.9m'
end

it 'returns nil when data is nil' do
stream = described_class.new(nil)
expect(stream.total_elevation_gain_s).to be_nil
end
end

describe '#total_elevation_loss_s' do
it 'returns formatted elevation loss' do
stream = described_class.new('data' => [100.0, 105.3, 102.1, 110.7, 108.2])
expect(stream.total_elevation_loss_s).to eq '5.7m'
end

it 'returns nil when data is nil' do
stream = described_class.new(nil)
expect(stream.total_elevation_loss_s).to be_nil
end
end
end
Loading