calcpace 1.12.1 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -1
- data/README.md +54 -4
- data/lib/calcpace/converter.rb +51 -4
- data/lib/calcpace/pace_converter.rb +30 -17
- data/lib/calcpace/track_calculator.rb +51 -13
- data/lib/calcpace/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3fb2c004f6564fef1686e1c92db831e7c54a47360dd9c540a90bb334582ee95
|
|
4
|
+
data.tar.gz: 31c20dd413fe2df72d57f7516bb613c7661724bba196ff81da3af3b2c17c0ee0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 784831b687993538654becf398906fc798ff089e8344ed1cd8768f3b42735111b490f99f185b0535f09d84ad15b0d207703e49d494c6e61ca1c3311630016b9a
|
|
7
|
+
data.tar.gz: 3f4170b7d5a3c4f71ed40ab732fb02be5ea2f1daf494c179aedab9b63e1837b90631ebf8db8f1484e83fbd9b55b569b7c9db31f66d915ca3228dae745eb7d619
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,87 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.14.0] - 2026-08-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `compact:` keyword on every pace-producing method, so a caller can ask for the
|
|
14
|
+
display format `convert_to_clocktime(compact: true)` introduced in 1.13.0
|
|
15
|
+
without reformatting the string itself
|
|
16
|
+
- `convert_pace(pace, conversion, compact: false)`
|
|
17
|
+
- `pace_km_to_mi(pace_per_km, compact: false)`
|
|
18
|
+
- `pace_mi_to_km(pace_per_mi, compact: false)`
|
|
19
|
+
- `track_splits(points, split_km = 1.0, compact: false)` — only the `:pace`
|
|
20
|
+
value changes; `:km` and `:elapsed` are numbers and stay as they are
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
calc.pace_km_to_mi('05:00') # => "00:08:02"
|
|
24
|
+
calc.pace_km_to_mi('05:00', compact: true) # => "8:02"
|
|
25
|
+
calc.track_splits(points, 1.0, compact: true)
|
|
26
|
+
# => [{ km: 1.0, elapsed: 312, pace: "5:12" }, ...]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The default stays `compact: false` everywhere, byte-for-byte the previous
|
|
30
|
+
output — the one exception is the negative-split fix below, which corrects a
|
|
31
|
+
value that was arithmetically wrong. Input validation is untouched:
|
|
32
|
+
a zero or negative pace still raises `Calcpace::NonPositiveInputError` and an
|
|
33
|
+
unknown conversion still raises `ArgumentError` in both modes.
|
|
34
|
+
|
|
35
|
+
Three places the two formats disagree about more than padding, all of them
|
|
36
|
+
now reachable through the pace APIs:
|
|
37
|
+
|
|
38
|
+
- A split pace slower than an hour per unit: the padded format keeps counting
|
|
39
|
+
minutes (`"66:33"`), as `track_splits` always has, while the compact one
|
|
40
|
+
rolls them into an hour field (`"1:06:33"`), consistent with every other
|
|
41
|
+
compact duration in the gem. Past 24 hours per unit the gap widens —
|
|
42
|
+
`"2248:18"` padded against `"37:28:18"` compact.
|
|
43
|
+
- Durations past 24 hours, the day-prefix rule 1.13.0 documented for
|
|
44
|
+
`convert_to_clocktime` alone, now visible through `convert_pace` too:
|
|
45
|
+
`convert_pace(100_000, :km_to_mi)` #=> `"1 20:42:14"`, against
|
|
46
|
+
`"44:42:14"` compact.
|
|
47
|
+
- A negative split (see Fixed below), signed in both formats but padded to a
|
|
48
|
+
different width: `"-00:40"` against `"-0:40"`.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- `track_splits` no longer misreports a negative split pace. A GPS track can
|
|
52
|
+
step backwards in time — a watch resyncing its clock, a device paused and
|
|
53
|
+
restarted, two segments merged out of order — which makes a split's elapsed
|
|
54
|
+
time negative. The padded format rendered that through Ruby's floor division,
|
|
55
|
+
so a −40 s split printed as `"-1:20"`; it now prints `"-00:40"`, and the
|
|
56
|
+
compact format prints `"-0:40"`. Neither mode raises: bad GPS data has always
|
|
57
|
+
been reported rather than blown up, and `compact: true` does not change that.
|
|
58
|
+
- `convert_pace`, `pace_km_to_mi` and `pace_mi_to_km` documented their return
|
|
59
|
+
value as `'08:02'` when they have always returned the padded `'00:08:02'`.
|
|
60
|
+
The docs now match the code; the code is unchanged.
|
|
61
|
+
- README and YARD examples for `track_distance`, `haversine_distance` and
|
|
62
|
+
`track_splits` printed numbers their own input never produced (`0.87` km for
|
|
63
|
+
a 1.51 km track, a `"05:12"` split for a `"06:55"` one). Every example is now
|
|
64
|
+
the real output of the code above it.
|
|
65
|
+
|
|
66
|
+
## [1.13.0] - 2026-08-28
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
- `convert_to_clocktime(seconds, compact: true)` — the display format a runner
|
|
70
|
+
reads, next to the padded format the gem already returned
|
|
71
|
+
- drops the hour when it is zero and the leading zero of the most significant
|
|
72
|
+
component, keeping two digits on everything after it:
|
|
73
|
+
`convert_to_clocktime(292, compact: true)` #=> `'4:52'`,
|
|
74
|
+
`convert_to_clocktime(45, compact: true)` #=> `'0:45'`,
|
|
75
|
+
`convert_to_clocktime(5025, compact: true)` #=> `'1:23:45'`
|
|
76
|
+
- past 24 hours it keeps counting hours (`100_000` #=> `'27:46:40'`) instead
|
|
77
|
+
of the padded format's day prefix (`'1 03:46:40'`) — a day count brings back
|
|
78
|
+
the padding and the extra unit the compact format exists to strip
|
|
79
|
+
- fractional seconds truncate, as they already did in the padded format:
|
|
80
|
+
`292.9` #=> `'4:52'`
|
|
81
|
+
|
|
82
|
+
The default stays `compact: false`, byte-for-byte the previous output, so every
|
|
83
|
+
existing caller is unaffected.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- `convert_to_clocktime` with a negative number now raises
|
|
87
|
+
`Calcpace::NonPositiveInputError` instead of silently wrapping around
|
|
88
|
+
(`-5` used to return `'23:59:55'`, a `Time.at` artifact). Zero remains a valid
|
|
89
|
+
duration in both formats. Non-numeric input keeps raising as before.
|
|
90
|
+
|
|
10
91
|
## [1.12.1] - 2026-08-15
|
|
11
92
|
|
|
12
93
|
### Changed
|
|
@@ -300,7 +381,9 @@ predictors are untouched.
|
|
|
300
381
|
|
|
301
382
|
See git history for changes in earlier versions.
|
|
302
383
|
|
|
303
|
-
[Unreleased]: https://github.com/0jonjo/calcpace/compare/v1.
|
|
384
|
+
[Unreleased]: https://github.com/0jonjo/calcpace/compare/v1.14.0...HEAD
|
|
385
|
+
[1.14.0]: https://github.com/0jonjo/calcpace/compare/v1.13.0...v1.14.0
|
|
386
|
+
[1.13.0]: https://github.com/0jonjo/calcpace/compare/v1.12.1...v1.13.0
|
|
304
387
|
[1.12.0]: https://github.com/0jonjo/calcpace/compare/v1.11.0...v1.12.0
|
|
305
388
|
[1.11.0]: https://github.com/0jonjo/calcpace/compare/v1.10.0...v1.11.0
|
|
306
389
|
[1.10.0]: https://github.com/0jonjo/calcpace/compare/v1.9.10...v1.10.0
|
data/README.md
CHANGED
|
@@ -93,6 +93,16 @@ See all units: `calc.list_all`, `calc.list_distance`, `calc.list_speed`.
|
|
|
93
93
|
```ruby
|
|
94
94
|
calc.pace_km_to_mi('05:00') # => "00:08:02"
|
|
95
95
|
calc.pace_mi_to_km('08:00') # => "00:04:58"
|
|
96
|
+
calc.convert_pace(300, :km_to_mi) # => "00:08:02"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
All three take a `compact:` keyword for the display format a runner reads on a
|
|
100
|
+
screen, the same one `convert_to_clocktime` offers:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
calc.pace_km_to_mi('05:00', compact: true) # => "8:02"
|
|
104
|
+
calc.pace_mi_to_km(480, compact: true) # => "4:58"
|
|
105
|
+
calc.convert_pace('05:00', :km_to_mi, compact: true) # => "8:02"
|
|
96
106
|
```
|
|
97
107
|
|
|
98
108
|
---
|
|
@@ -152,12 +162,33 @@ points = [
|
|
|
152
162
|
{ lat: -23.5520, lon: -46.6480, ele: 758.0, time: Time.parse('2024-01-01 07:10:00') },
|
|
153
163
|
]
|
|
154
164
|
|
|
155
|
-
calc.haversine_distance(-23.5505, -46.6333, -23.5510, -46.6340)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
calc.
|
|
165
|
+
calc.haversine_distance(-23.5505, -46.6333, -23.5510, -46.6340)
|
|
166
|
+
# => 0.09045636644035066 (km)
|
|
167
|
+
|
|
168
|
+
calc.track_distance(points) # => 1.51 (km)
|
|
169
|
+
calc.elevation_gain(points) # => { gain: 5.0, loss: 7.0 }
|
|
170
|
+
|
|
171
|
+
calc.track_splits(points, 1.0)
|
|
172
|
+
# => [{ km: 1.0, elapsed: 415, pace: "06:55" },
|
|
173
|
+
# { km: 1.51, elapsed: 600, pace: "06:04" }]
|
|
174
|
+
|
|
175
|
+
# Compact pace for display; :km and :elapsed are unchanged
|
|
176
|
+
calc.track_splits(points, 1.0, compact: true)
|
|
177
|
+
# => [{ km: 1.0, elapsed: 415, pace: "6:55" },
|
|
178
|
+
# { km: 1.51, elapsed: 600, pace: "6:04" }]
|
|
159
179
|
```
|
|
160
180
|
|
|
181
|
+
The last entry is the partial split — the leftover distance after the last full
|
|
182
|
+
one, so its `:km` is the track total rather than a multiple of `split_km`.
|
|
183
|
+
|
|
184
|
+
Two things to know about `compact:` here. A split slower than an hour per unit
|
|
185
|
+
is where the formats stop differing by padding alone: the padded one keeps
|
|
186
|
+
counting minutes (`"66:33"`), as `track_splits` always has, while the compact
|
|
187
|
+
one rolls them into an hour field (`"1:06:33"`). And a track that steps
|
|
188
|
+
backwards in time — a watch resyncing its clock, a paused device, two segments
|
|
189
|
+
merged out of order — produces a negative split, reported with a leading minus
|
|
190
|
+
in both formats (`"-00:40"` / `"-0:40"`) rather than raising.
|
|
191
|
+
|
|
161
192
|
**Haversine formula** — great-circle distance on a sphere (R = 6,371 km). Accuracy: ~0.3% of GPS/WGS84. Best for running and cycling distances; not for geodetic surveying.
|
|
162
193
|
|
|
163
194
|
---
|
|
@@ -367,6 +398,25 @@ calc.convert_to_clocktime(3600) # => "01:00:00"
|
|
|
367
398
|
calc.check_time('01:00:00') # => nil (valid)
|
|
368
399
|
```
|
|
369
400
|
|
|
401
|
+
`convert_to_clocktime` takes a `compact:` keyword for the format a runner reads
|
|
402
|
+
on a screen — no zero hour, no leading zero on the most significant component:
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
calc.convert_to_clocktime(292, compact: true) # => "4:52"
|
|
406
|
+
calc.convert_to_clocktime(45, compact: true) # => "0:45"
|
|
407
|
+
calc.convert_to_clocktime(5025, compact: true) # => "1:23:45"
|
|
408
|
+
calc.convert_to_clocktime(100_000, compact: true) # => "27:46:40"
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Past 24 hours the compact format keeps counting hours, where the padded one
|
|
412
|
+
prefixes a day count (`"1 03:46:40"`). Fractional seconds truncate in both.
|
|
413
|
+
A negative number of seconds raises `Calcpace::NonPositiveInputError`; zero is a
|
|
414
|
+
valid duration (`"00:00:00"` / `"0:00"`).
|
|
415
|
+
|
|
416
|
+
The same `compact:` keyword is accepted by `convert_pace`, `pace_km_to_mi`,
|
|
417
|
+
`pace_mi_to_km`, and `track_splits`. It always defaults to `false`, so every
|
|
418
|
+
call without it returns exactly what it returned before.
|
|
419
|
+
|
|
370
420
|
---
|
|
371
421
|
|
|
372
422
|
### Errors
|
data/lib/calcpace/converter.rb
CHANGED
|
@@ -99,13 +99,38 @@ module Converter
|
|
|
99
99
|
|
|
100
100
|
# Converts seconds to a clocktime string
|
|
101
101
|
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
102
|
+
# The default (padded) format is the machine-readable one: always HH:MM:SS,
|
|
103
|
+
# with a day prefix past 24 hours. The compact format is the one a runner
|
|
104
|
+
# reads on a screen — it drops a zero hour and the leading zero of the most
|
|
105
|
+
# significant component, keeping two digits on everything after it.
|
|
104
106
|
#
|
|
105
|
-
#
|
|
107
|
+
# Fractional seconds are truncated, not rounded, in both formats, so a
|
|
108
|
+
# predictor returning 292.9 s prints the same 4:52 either way. Past 24 hours
|
|
109
|
+
# the compact format keeps counting hours ('27:46:40') instead of adding the
|
|
110
|
+
# padded format's day prefix: a day count reintroduces the very padding and
|
|
111
|
+
# the extra unit the compact format exists to strip, and ultra finish times
|
|
112
|
+
# are read as a running hour count.
|
|
113
|
+
#
|
|
114
|
+
# @param seconds [Numeric] total seconds, zero or more
|
|
115
|
+
# @param compact [Boolean] when true, return the compact display format
|
|
116
|
+
# @return [String] time in HH:MM:SS format, or "D HH:MM:SS" for durations over
|
|
117
|
+
# 24 hours; with compact: true, "M:SS" or "H:MM:SS"
|
|
118
|
+
# @raise [Calcpace::NonPositiveInputError] if seconds is negative
|
|
119
|
+
#
|
|
120
|
+
# @example padded (default)
|
|
106
121
|
# convert_to_clocktime(3600) #=> '01:00:00' (1 hour)
|
|
122
|
+
# convert_to_clocktime(292) #=> '00:04:52'
|
|
107
123
|
# convert_to_clocktime(100000) #=> '1 03:46:40' (1 day, 3 hours, 46 minutes, 40 seconds)
|
|
108
|
-
|
|
124
|
+
#
|
|
125
|
+
# @example compact
|
|
126
|
+
# convert_to_clocktime(45, compact: true) #=> '0:45'
|
|
127
|
+
# convert_to_clocktime(292, compact: true) #=> '4:52'
|
|
128
|
+
# convert_to_clocktime(5025, compact: true) #=> '1:23:45'
|
|
129
|
+
# convert_to_clocktime(100000, compact: true) #=> '27:46:40'
|
|
130
|
+
def convert_to_clocktime(seconds, compact: false)
|
|
131
|
+
check_not_negative(seconds)
|
|
132
|
+
return compact_clocktime(seconds) if compact
|
|
133
|
+
|
|
109
134
|
days = (seconds / 86_400).to_i
|
|
110
135
|
format = days.positive? ? "#{days} %H:%M:%S" : '%H:%M:%S'
|
|
111
136
|
Time.at(seconds).utc.strftime(format)
|
|
@@ -149,6 +174,28 @@ module Converter
|
|
|
149
174
|
|
|
150
175
|
private
|
|
151
176
|
|
|
177
|
+
# Formats a duration without padding the most significant component, dropping
|
|
178
|
+
# the hour when there is none. Hours accumulate past 24 rather than rolling
|
|
179
|
+
# over into a day count.
|
|
180
|
+
def compact_clocktime(seconds)
|
|
181
|
+
total = seconds.to_i
|
|
182
|
+
parts = { hours: total / 3600, minutes: (total % 3600) / 60, seconds: total % 60 }
|
|
183
|
+
return format('%<hours>d:%<minutes>02d:%<seconds>02d', parts) if parts[:hours].positive?
|
|
184
|
+
|
|
185
|
+
format('%<minutes>d:%<seconds>02d', parts)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Guards against negative durations. Unlike Checker#check_positive, zero is a
|
|
189
|
+
# legitimate duration here — a zero split prints as 00:00:00 — so only a
|
|
190
|
+
# negative value is rejected. Non-numeric input is left to raise on its own,
|
|
191
|
+
# as it always has.
|
|
192
|
+
def check_not_negative(seconds)
|
|
193
|
+
return unless seconds.is_a?(Numeric) && seconds.negative?
|
|
194
|
+
|
|
195
|
+
raise Calcpace::NonPositiveInputError,
|
|
196
|
+
'Seconds must not be a negative number'
|
|
197
|
+
end
|
|
198
|
+
|
|
152
199
|
# Guards the "race name + distance_unit" combination. A standard race already
|
|
153
200
|
# carries its own distance, so the keyword can only be a caller mistake —
|
|
154
201
|
# better to say so than to ignore it silently.
|
|
@@ -7,48 +7,61 @@
|
|
|
7
7
|
module PaceConverter
|
|
8
8
|
# Converts pace from one unit to another
|
|
9
9
|
#
|
|
10
|
+
# The pace comes back in the same two formats #convert_to_clocktime offers:
|
|
11
|
+
# the padded HH:MM:SS by default, and the compact display format a runner
|
|
12
|
+
# reads on a screen with <tt>compact: true</tt>.
|
|
13
|
+
#
|
|
10
14
|
# @param pace [Numeric, String] pace in seconds per unit or time string (MM:SS)
|
|
11
15
|
# @param conversion [Symbol, String] conversion type (:km_to_mi, :mi_to_km, 'km to mi', 'mi to km')
|
|
12
|
-
# @
|
|
16
|
+
# @param compact [Boolean] when true, return the compact display format
|
|
17
|
+
# @return [String] converted pace in HH:MM:SS format, or 'M:SS' / 'H:MM:SS' with compact: true
|
|
13
18
|
# @raise [ArgumentError] if conversion type is not supported
|
|
14
19
|
# @raise [Calcpace::NonPositiveInputError] if pace is not positive
|
|
15
20
|
#
|
|
16
|
-
# @example
|
|
17
|
-
# convert_pace('05:00', :km_to_mi) #=> '08:02' (5:00/km = 8:02/mi)
|
|
18
|
-
# convert_pace('08:00', :mi_to_km) #=> '04:58' (8:00/mi ≈ 4:58/km)
|
|
19
|
-
# convert_pace(300, 'km to mi') #=> '08:02' (300s/km = 482s/mi)
|
|
20
|
-
|
|
21
|
+
# @example padded (default)
|
|
22
|
+
# convert_pace('05:00', :km_to_mi) #=> '00:08:02' (5:00/km = 8:02/mi)
|
|
23
|
+
# convert_pace('08:00', :mi_to_km) #=> '00:04:58' (8:00/mi ≈ 4:58/km)
|
|
24
|
+
# convert_pace(300, 'km to mi') #=> '00:08:02' (300s/km = 482s/mi)
|
|
25
|
+
#
|
|
26
|
+
# @example compact
|
|
27
|
+
# convert_pace('05:00', :km_to_mi, compact: true) #=> '8:02'
|
|
28
|
+
# convert_pace(300, 'km to mi', compact: true) #=> '8:02'
|
|
29
|
+
def convert_pace(pace, conversion, compact: false)
|
|
21
30
|
pace_seconds = pace.is_a?(String) ? convert_to_seconds(pace) : pace
|
|
22
31
|
check_positive(pace_seconds, 'Pace')
|
|
23
32
|
|
|
24
33
|
conversion_type = normalize_conversion(conversion)
|
|
25
34
|
converted_seconds = apply_pace_conversion(pace_seconds, conversion_type)
|
|
26
35
|
|
|
27
|
-
convert_to_clocktime(converted_seconds)
|
|
36
|
+
convert_to_clocktime(converted_seconds, compact: compact)
|
|
28
37
|
end
|
|
29
38
|
|
|
30
39
|
# Converts pace from kilometers to miles
|
|
31
40
|
#
|
|
32
41
|
# @param pace_per_km [Numeric, String] pace in seconds per km or time string (MM:SS)
|
|
33
|
-
# @
|
|
42
|
+
# @param compact [Boolean] when true, return the compact display format
|
|
43
|
+
# @return [String] pace per mile in HH:MM:SS format, or 'M:SS' / 'H:MM:SS' with compact: true
|
|
34
44
|
#
|
|
35
45
|
# @example
|
|
36
|
-
# pace_km_to_mi('05:00')
|
|
37
|
-
# pace_km_to_mi(300)
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
# pace_km_to_mi('05:00') #=> '00:08:02' (5:00/km = 8:02/mi)
|
|
47
|
+
# pace_km_to_mi(300) #=> '00:08:02' (300s/km = 482s/mi)
|
|
48
|
+
# pace_km_to_mi('05:00', compact: true) #=> '8:02'
|
|
49
|
+
def pace_km_to_mi(pace_per_km, compact: false)
|
|
50
|
+
convert_pace(pace_per_km, :km_to_mi, compact: compact)
|
|
40
51
|
end
|
|
41
52
|
|
|
42
53
|
# Converts pace from miles to kilometers
|
|
43
54
|
#
|
|
44
55
|
# @param pace_per_mi [Numeric, String] pace in seconds per mile or time string (MM:SS)
|
|
45
|
-
# @
|
|
56
|
+
# @param compact [Boolean] when true, return the compact display format
|
|
57
|
+
# @return [String] pace per kilometer in HH:MM:SS format, or 'M:SS' / 'H:MM:SS' with compact: true
|
|
46
58
|
#
|
|
47
59
|
# @example
|
|
48
|
-
# pace_mi_to_km('08:00')
|
|
49
|
-
# pace_mi_to_km(480)
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
# pace_mi_to_km('08:00') #=> '00:04:58' (8:00/mi ≈ 4:58/km)
|
|
61
|
+
# pace_mi_to_km(480) #=> '00:04:58' (480s/mi = 298s/km)
|
|
62
|
+
# pace_mi_to_km('08:00', compact: true) #=> '4:58'
|
|
63
|
+
def pace_mi_to_km(pace_per_mi, compact: false)
|
|
64
|
+
convert_pace(pace_per_mi, :mi_to_km, compact: compact)
|
|
52
65
|
end
|
|
53
66
|
|
|
54
67
|
private
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# { lat: -23.5510, lon: -46.6340 },
|
|
15
15
|
# { lat: -23.5520, lon: -46.6350 }
|
|
16
16
|
# ]
|
|
17
|
-
# calc.track_distance(points) #=> 0.
|
|
17
|
+
# calc.track_distance(points) #=> 0.24 (km)
|
|
18
18
|
#
|
|
19
19
|
# @example Calculate elevation gain and loss
|
|
20
20
|
# points = [
|
|
@@ -49,7 +49,7 @@ module TrackCalculator
|
|
|
49
49
|
#
|
|
50
50
|
# @example Distance between two points in São Paulo
|
|
51
51
|
# haversine_distance(-23.5505, -46.6333, -23.5510, -46.6340)
|
|
52
|
-
# #=> 0.
|
|
52
|
+
# #=> 0.09045636644035066 (km)
|
|
53
53
|
def haversine_distance(lat1, lon1, lat2, lon2)
|
|
54
54
|
validate_coordinates(lat1, lon1)
|
|
55
55
|
validate_coordinates(lat2, lon2)
|
|
@@ -69,7 +69,7 @@ module TrackCalculator
|
|
|
69
69
|
# { lat: -23.5510, lon: -46.6340 },
|
|
70
70
|
# { lat: -23.5520, lon: -46.6350 }
|
|
71
71
|
# ]
|
|
72
|
-
# track_distance(points) #=> 0.
|
|
72
|
+
# track_distance(points) #=> 0.24
|
|
73
73
|
def track_distance(points)
|
|
74
74
|
return 0.0 if points.nil? || points.size < 2
|
|
75
75
|
|
|
@@ -114,13 +114,26 @@ module TrackCalculator
|
|
|
114
114
|
# split distance is reached, then records elapsed time and pace for that split.
|
|
115
115
|
# Any remaining distance at the end is included as a partial split.
|
|
116
116
|
#
|
|
117
|
+
# Only :pace changes with compact: — :km and :elapsed are numbers, not
|
|
118
|
+
# formatted strings, and are the same in both modes.
|
|
119
|
+
#
|
|
120
|
+
# The two pace formats differ by more than padding once a split is slower than
|
|
121
|
+
# an hour per unit: the padded format keeps counting minutes ('66:33'), as it
|
|
122
|
+
# always has, while the compact one rolls them into an hour field ('1:06:33'),
|
|
123
|
+
# like every other compact duration in the gem. A track that steps backwards
|
|
124
|
+
# in time (a watch clock resync, a paused device, merged segments) yields a
|
|
125
|
+
# negative split, reported with a leading minus in both formats ('-00:40' /
|
|
126
|
+
# '-0:40') rather than raising.
|
|
127
|
+
#
|
|
117
128
|
# @param points [Array<Hash>] array of points with :lat, :lon, and :time keys.
|
|
118
129
|
# :time must respond to #to_f (Unix timestamp) or be a Time object.
|
|
119
130
|
# @param split_km [Numeric] split interval in kilometers (default: 1.0)
|
|
131
|
+
# @param compact [Boolean] when true, :pace uses the compact display format
|
|
120
132
|
# @return [Array<Hash>] array of split hashes, each with:
|
|
121
133
|
# - :km [Float] cumulative distance at split end
|
|
122
134
|
# - :elapsed [Integer] elapsed seconds from start of track to end of split
|
|
123
|
-
# - :pace [String] pace for this split in MM:SS format
|
|
135
|
+
# - :pace [String] pace for this split in MM:SS format, or 'M:SS' / 'H:MM:SS'
|
|
136
|
+
# with compact: true
|
|
124
137
|
# @raise [ArgumentError] if split_km is not positive
|
|
125
138
|
# @raise [ArgumentError] if any point is missing a :time key
|
|
126
139
|
#
|
|
@@ -131,12 +144,16 @@ module TrackCalculator
|
|
|
131
144
|
# { km: 2.0, elapsed: 624, pace: "05:12" },
|
|
132
145
|
# ...
|
|
133
146
|
# ]
|
|
134
|
-
|
|
147
|
+
#
|
|
148
|
+
# @example compact pace
|
|
149
|
+
# calc.track_splits(points, 1.0, compact: true)
|
|
150
|
+
# #=> [{ km: 1.0, elapsed: 312, pace: "5:12" }, ...]
|
|
151
|
+
def track_splits(points, split_km = 1.0, compact: false)
|
|
135
152
|
raise ArgumentError, 'split_km must be positive' unless split_km.is_a?(Numeric) && split_km.positive?
|
|
136
153
|
return [] if points.nil? || points.size < 2
|
|
137
154
|
|
|
138
155
|
validate_points_have_time(points)
|
|
139
|
-
collect_splits(points, split_km)
|
|
156
|
+
collect_splits(points, split_km, compact: compact)
|
|
140
157
|
end
|
|
141
158
|
|
|
142
159
|
private
|
|
@@ -209,17 +226,37 @@ module TrackCalculator
|
|
|
209
226
|
t_a + ((t_b - t_a) * (distance_into_segment / segment_km))
|
|
210
227
|
end
|
|
211
228
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
229
|
+
# Formats a split pace, in either format, without ever raising.
|
|
230
|
+
#
|
|
231
|
+
# A GPS track can step backwards in time — a watch resyncing its clock, a
|
|
232
|
+
# device paused and restarted, two segments merged out of order — which makes
|
|
233
|
+
# a split elapsed time negative. That is bad data, not a caller error, and
|
|
234
|
+
# track_splits has always reported it rather than blowing up; #sign_of keeps
|
|
235
|
+
# it that way now that the compact format goes through #convert_to_clocktime,
|
|
236
|
+
# which rejects negative durations.
|
|
237
|
+
def seconds_to_pace(seconds, km, compact: false)
|
|
215
238
|
pace_seconds = (seconds.to_f / km).round
|
|
239
|
+
"#{sign_of(pace_seconds)}#{format_pace(pace_seconds.abs, compact: compact)}"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def sign_of(pace_seconds)
|
|
243
|
+
pace_seconds.negative? ? '-' : ''
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# The padded format keeps the historical MM:SS, where the minutes keep
|
|
247
|
+
# counting past 60 (a 66-minute hiking split reads '66:33'); the compact
|
|
248
|
+
# format defers to #convert_to_clocktime, which rolls those minutes into an
|
|
249
|
+
# hour field ('1:06:33') exactly as it does everywhere else.
|
|
250
|
+
def format_pace(pace_seconds, compact:)
|
|
251
|
+
return convert_to_clocktime(pace_seconds, compact: true) if compact
|
|
252
|
+
|
|
216
253
|
format('%<min>02d:%<sec>02d', min: pace_seconds / 60, sec: pace_seconds % 60)
|
|
217
254
|
end
|
|
218
255
|
|
|
219
|
-
def collect_splits(points, split_km)
|
|
256
|
+
def collect_splits(points, split_km, compact:)
|
|
220
257
|
state = { splits: [], start_time: point_time(points.first),
|
|
221
258
|
split_start_time: point_time(points.first),
|
|
222
|
-
accumulated_km: 0.0, split_number: 1 }
|
|
259
|
+
accumulated_km: 0.0, split_number: 1, compact: compact }
|
|
223
260
|
|
|
224
261
|
points.each_cons(2) { |a, b| process_segment(a, b, split_km, state) }
|
|
225
262
|
append_partial_split(points.last, split_km, state)
|
|
@@ -249,7 +286,7 @@ module TrackCalculator
|
|
|
249
286
|
{
|
|
250
287
|
km: (split_km * state[:split_number]).round(2),
|
|
251
288
|
elapsed: (boundary_time - state[:start_time]).round,
|
|
252
|
-
pace: seconds_to_pace(split_elapsed, split_km)
|
|
289
|
+
pace: seconds_to_pace(split_elapsed, split_km, compact: state[:compact])
|
|
253
290
|
}
|
|
254
291
|
end
|
|
255
292
|
|
|
@@ -261,7 +298,8 @@ module TrackCalculator
|
|
|
261
298
|
state[:splits] << {
|
|
262
299
|
km: state[:accumulated_km].round(2),
|
|
263
300
|
elapsed: (last_time - state[:start_time]).round,
|
|
264
|
-
pace: seconds_to_pace((last_time - state[:split_start_time]).round, remaining_km
|
|
301
|
+
pace: seconds_to_pace((last_time - state[:split_start_time]).round, remaining_km,
|
|
302
|
+
compact: state[:compact])
|
|
265
303
|
}
|
|
266
304
|
end
|
|
267
305
|
end
|
data/lib/calcpace/version.rb
CHANGED