runby_pace 0.2.50 → 0.2.50.111
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 +5 -5
- data/.rubocop.yml +10 -0
- data/.travis.yml +9 -2
- data/Gemfile +4 -0
- data/README.md +16 -5
- data/Rakefile +40 -6
- data/bin/_guard-core +17 -16
- data/bin/guard +17 -16
- data/bin/runbypace +15 -0
- data/lib/runby_pace/cli/cli.rb +127 -0
- data/lib/runby_pace/cli/config.rb +82 -0
- data/lib/runby_pace/distance.rb +135 -0
- data/lib/runby_pace/distance_unit.rb +89 -0
- data/lib/runby_pace/golden_pace_set.rb +50 -0
- data/lib/runby_pace/pace.rb +152 -0
- data/lib/runby_pace/{pace_data.rb → pace_calculator.rb} +29 -13
- data/lib/runby_pace/pace_range.rb +27 -9
- data/lib/runby_pace/run_math.rb +14 -0
- data/lib/runby_pace/run_type.rb +12 -4
- data/lib/runby_pace/run_types/all_run_types.g.rb +14 -12
- data/lib/runby_pace/run_types/all_run_types.template +6 -4
- data/lib/runby_pace/run_types/distance_run.rb +55 -0
- data/lib/runby_pace/run_types/easy_run.rb +31 -10
- data/lib/runby_pace/run_types/fast_tempo_run.rb +23 -0
- data/lib/runby_pace/run_types/find_divisor.rb +13 -17
- data/lib/runby_pace/run_types/five_kilometer_race_run.rb +22 -0
- data/lib/runby_pace/run_types/long_run.rb +32 -10
- data/lib/runby_pace/run_types/mile_race_run.rb +24 -0
- data/lib/runby_pace/run_types/slow_tempo_run.rb +22 -0
- data/lib/runby_pace/run_types/tempo_run.rb +54 -0
- data/lib/runby_pace/run_types/ten_kilometer_race_run.rb +23 -0
- data/lib/runby_pace/runby_range.rb +22 -0
- data/lib/runby_pace/runby_time.rb +138 -0
- data/lib/runby_pace/runby_time_parser.rb +80 -0
- data/lib/runby_pace/speed.rb +97 -0
- data/lib/runby_pace/speed_range.rb +30 -0
- data/lib/runby_pace/utility/parameter_sanitizer.rb +29 -0
- data/lib/runby_pace/version.rb +17 -2
- data/lib/runby_pace/version.seed +5 -0
- data/lib/runby_pace.rb +4 -1
- data/misc/runbypace_logo.png +0 -0
- data/runby_pace.gemspec +5 -6
- metadata +32 -9
- data/lib/runby_pace/pace_time.rb +0 -110
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Runby
|
4
|
+
# Represents the distance units (e.g. kilometers, miles) used in paces
|
5
|
+
# including the human-readable description of each unit
|
6
|
+
# and the factor used to convert it to kilometers.
|
7
|
+
class DistanceUnit
|
8
|
+
attr_reader :symbol, :description, :conversion_factor
|
9
|
+
|
10
|
+
def self.new(unit_of_measure)
|
11
|
+
return unit_of_measure if unit_of_measure.is_a? DistanceUnit
|
12
|
+
return DistanceUnit.parse(unit_of_measure) if unit_of_measure.is_a? String
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(unit_of_measure)
|
17
|
+
raise "':#{unit_of_measure}' is an unknown unit of measure" unless DistanceUnit.known_uom? unit_of_measure
|
18
|
+
@symbol = unit_of_measure
|
19
|
+
@conversion_factor = UOM_DEFINITIONS[@symbol][:conversion_factor]
|
20
|
+
@description = UOM_DEFINITIONS[@symbol][:description]
|
21
|
+
freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s(format: :long, pluralize: false)
|
25
|
+
case format
|
26
|
+
when :short then @symbol.to_s
|
27
|
+
when :long then pluralize ? description_plural : @description
|
28
|
+
else raise "Invalid string format #{format}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def description_plural
|
33
|
+
UOM_DEFINITIONS[@symbol][:description_plural]
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.parse(description)
|
37
|
+
return new description if description.is_a? Symbol
|
38
|
+
description = description.strip.chomp.downcase
|
39
|
+
found_uom = nil
|
40
|
+
UOM_DEFINITIONS.each do |uom, details|
|
41
|
+
if details[:synonyms].include? description
|
42
|
+
found_uom = uom
|
43
|
+
break
|
44
|
+
end
|
45
|
+
end
|
46
|
+
raise "Error parsing distance unit '#{description}'" unless found_uom
|
47
|
+
DistanceUnit.new found_uom
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.try_parse(str)
|
51
|
+
uom, error_message = nil
|
52
|
+
begin
|
53
|
+
uom = parse str
|
54
|
+
rescue StandardError => ex
|
55
|
+
error_message = ex.message
|
56
|
+
end
|
57
|
+
{ uom: uom, error: error_message }
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.known_uom?(symbol)
|
61
|
+
UOM_DEFINITIONS.key?(symbol)
|
62
|
+
end
|
63
|
+
|
64
|
+
def ==(other)
|
65
|
+
if other.is_a? DistanceUnit
|
66
|
+
@symbol == other.symbol
|
67
|
+
elsif other.is_a? String
|
68
|
+
self == DistanceUnit.parse(other)
|
69
|
+
else
|
70
|
+
raise "Unable to compare DistanceUnit to #{other.class}(#{other})"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
UOM_DEFINITIONS =
|
75
|
+
{ km: { description: 'kilometer', description_plural: 'kilometers', conversion_factor: 1.0,
|
76
|
+
synonyms: %w[k km kms kilometer kilometers] },
|
77
|
+
m: { description: 'meter', description_plural: 'meters', conversion_factor: 0.001,
|
78
|
+
synonyms: %w[m meter meters] },
|
79
|
+
mi: { description: 'mile', description_plural: 'miles', conversion_factor: 1.609344,
|
80
|
+
synonyms: %w[mi mile miles] },
|
81
|
+
ft: { description: 'foot', description_plural: 'feet', conversion_factor: 0.0003048,
|
82
|
+
synonyms: %w[ft foot feet] },
|
83
|
+
yd: { description: 'yard', description_plural: 'yards', conversion_factor: 1093.61,
|
84
|
+
synonyms: %w[y yd yds yard yards] },
|
85
|
+
# Fun distance unit of measures
|
86
|
+
marathon: { description: 'marathon', description_plural: 'marathons', conversion_factor: 42.1648128,
|
87
|
+
synonyms: %w[marathon] } }.freeze
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Runby
|
4
|
+
|
5
|
+
# Maps a set of 5K race times with their pre-calculated pace recommendations.
|
6
|
+
# This is useful in testing as well as defining the fastest and slowest supported 5K times.
|
7
|
+
# GoldenPaceSet could conceivably be used to pre-compute a large number of recommended paces,
|
8
|
+
# thus reducing runtime CPU overhead.
|
9
|
+
class GoldenPaceSet
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
attr_reader :paces
|
13
|
+
|
14
|
+
# The fastest 5K time supported by RunbyPace
|
15
|
+
FASTEST_5K = :'14:00'
|
16
|
+
|
17
|
+
# The slowest 5K time supported by RunbyPace
|
18
|
+
SLOWEST_5K = :'42:00'
|
19
|
+
|
20
|
+
# @param [Hash] paces_hash is a hash mapping 5K time symbols to times, represented as strings.
|
21
|
+
# An example paces_hash is {'14:00':'4:00', '15:00':'4:55'}
|
22
|
+
def initialize(paces_hash)
|
23
|
+
@paces = {}
|
24
|
+
paces_hash.each { |five_k_time, recommended_pace| @paces[five_k_time.to_sym] = Pace.new(recommended_pace) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def each
|
28
|
+
@paces.each do |h, v|
|
29
|
+
yield h, v
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns first/fastest recommended pace in the set
|
34
|
+
def first
|
35
|
+
@paces[FASTEST_5K]
|
36
|
+
end
|
37
|
+
alias fastest first
|
38
|
+
|
39
|
+
# Return the last/slowest recommended pace in the set
|
40
|
+
def last
|
41
|
+
@paces[SLOWEST_5K]
|
42
|
+
end
|
43
|
+
alias slowest last
|
44
|
+
|
45
|
+
# Creates and returns a new GoldenPaceSet with only two entries
|
46
|
+
def self.new_from_endpoints(fastest, slowest)
|
47
|
+
GoldenPaceSet.new(FASTEST_5K => fastest, SLOWEST_5K => slowest)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Runby
|
4
|
+
# Represents a pace consisting of a distance and a time in which that distance was covered
|
5
|
+
class Pace
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
attr_reader :time, :distance
|
9
|
+
|
10
|
+
def self.new(time_or_pace, distance = '1K')
|
11
|
+
return time_or_pace if time_or_pace.is_a? Pace
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(time_or_pace, distance = '1K')
|
16
|
+
case time_or_pace
|
17
|
+
when RunbyTime
|
18
|
+
init_from_time time_or_pace, distance
|
19
|
+
when String
|
20
|
+
init_from_string time_or_pace, distance
|
21
|
+
else
|
22
|
+
raise 'Invalid Time or Pace'
|
23
|
+
end
|
24
|
+
freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
def convert_to(target_distance)
|
28
|
+
target_distance = Distance.new(target_distance) unless target_distance.is_a?(Distance)
|
29
|
+
return self if @distance == target_distance
|
30
|
+
conversion_factor = target_distance / @distance
|
31
|
+
Pace.new @time * conversion_factor, target_distance
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s(format: :short)
|
35
|
+
leading_one_regex = /^1 ?/
|
36
|
+
distance_s = @distance.to_s(format: format).gsub(leading_one_regex, '')
|
37
|
+
case format
|
38
|
+
when :short then "#{time} p/#{distance_s}"
|
39
|
+
when :long then "#{time} per #{distance_s}"
|
40
|
+
else raise "Invalid string format #{format}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def as_speed
|
45
|
+
total_minutes = @time.total_minutes
|
46
|
+
multiplier = total_minutes.positive? ? (60 / total_minutes).round(2) : 0
|
47
|
+
distance = Runby::Distance.new(@distance.uom, multiplier)
|
48
|
+
Runby::Speed.new distance
|
49
|
+
end
|
50
|
+
|
51
|
+
def meters_per_minute
|
52
|
+
total_minutes = @time.total_minutes
|
53
|
+
return 0 unless total_minutes.positive?
|
54
|
+
@distance.meters / total_minutes
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param [String] str is either a long-form pace such as "10:00 per mile" or a short-form pace like "10:00 p/mi"
|
58
|
+
def self.parse(str)
|
59
|
+
str = str.to_s.strip.chomp
|
60
|
+
match = str.match %r{^(?<time>[:\d]*) ?(?: per |p\/)(?<distance>(?:[\d.]+ ?)?\w+)$}
|
61
|
+
raise "Invalid pace format (#{str})" unless match
|
62
|
+
time = Runby::RunbyTime.new(match[:time])
|
63
|
+
distance = Runby::Distance.new(match[:distance])
|
64
|
+
Pace.new time, distance
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.try_parse(str)
|
68
|
+
pace = nil
|
69
|
+
error_message = nil
|
70
|
+
warning_message = nil
|
71
|
+
begin
|
72
|
+
pace = Pace.parse str
|
73
|
+
rescue StandardError => ex
|
74
|
+
error_message = ex.message
|
75
|
+
end
|
76
|
+
{ pace: pace, error: error_message, warning: warning_message }
|
77
|
+
end
|
78
|
+
|
79
|
+
def <=>(other)
|
80
|
+
raise "Unable to compare Runby::Pace to #{other.class}(#{other})" unless [Pace, RunbyTime, String].include? other.class
|
81
|
+
if other.is_a? Pace
|
82
|
+
meters_per_minute.round(2) <=> other.meters_per_minute.round(2)
|
83
|
+
elsif other.is_a? RunbyTime
|
84
|
+
@time <=> other
|
85
|
+
elsif other.is_a? String
|
86
|
+
return 0 if to_s == other || to_s(format: :long) == other
|
87
|
+
return 0 if @time == other
|
88
|
+
self <=> try_parse(other)[:pace]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def almost_equals?(other_pace, tolerance_time = '00:01')
|
93
|
+
if other_pace.is_a?(RunbyTime)
|
94
|
+
return almost_equals?(Pace.new(other_pace, @distance), tolerance_time)
|
95
|
+
end
|
96
|
+
if other_pace.is_a?(String)
|
97
|
+
return almost_equals?(Pace.new(other_pace, @distance), tolerance_time) if other_pace.match?(/^\d?\d:\d\d$/)
|
98
|
+
other_pace = Pace.parse(other_pace)
|
99
|
+
end
|
100
|
+
tolerance = RunbyTime.new(tolerance_time)
|
101
|
+
fast_end = (self - tolerance)
|
102
|
+
slow_end = (self + tolerance)
|
103
|
+
slow_end <= other_pace && other_pace <= fast_end
|
104
|
+
end
|
105
|
+
|
106
|
+
# @param [Pace, RunbyTime] other
|
107
|
+
def -(other)
|
108
|
+
if other.is_a?(Pace)
|
109
|
+
Pace.new(@time - other.convert_to(@distance).time, @distance)
|
110
|
+
elsif other.is_a?(RunbyTime)
|
111
|
+
Pace.new(@time - other, @distance)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# @param [Pace, RunbyTime] other
|
116
|
+
def +(other)
|
117
|
+
if other.is_a?(Pace)
|
118
|
+
Pace.new(@time + other.convert_to(@distance).time, @distance)
|
119
|
+
elsif other.is_a?(RunbyTime)
|
120
|
+
Pace.new(@time + other, @distance)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def distance_covered_over_time(time)
|
125
|
+
time = Runby.sanitize(time).as(RunbyTime)
|
126
|
+
if time.total_minutes.zero? || @distance.multiplier.zero?
|
127
|
+
return Runby::Distance.new(@distance.uom, 0)
|
128
|
+
end
|
129
|
+
divisor = @time.total_minutes / time.total_minutes / @distance.multiplier
|
130
|
+
distance_covered = Runby::Distance.new(@distance.uom, 1 / divisor)
|
131
|
+
distance_covered
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def init_from_string(string, distance = '1K')
|
137
|
+
pace = Pace.try_parse(string)
|
138
|
+
if pace[:pace]
|
139
|
+
@time = pace[:pace].time
|
140
|
+
@distance = pace[:pace].distance
|
141
|
+
return
|
142
|
+
end
|
143
|
+
@time = Runby::RunbyTime.new string
|
144
|
+
@distance = Runby::Distance.new distance
|
145
|
+
end
|
146
|
+
|
147
|
+
def init_from_time(time, distance)
|
148
|
+
@time = time
|
149
|
+
@distance = Runby::Distance.new distance
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -1,7 +1,8 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
class PaceData
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
3
|
+
module Runby
|
4
|
+
# Encapsulates the algorithms used to calculate target paces.
|
5
|
+
class PaceCalculator
|
5
6
|
# The number of data points plotted on our line of 5K times.
|
6
7
|
# We take 5K times from 14:00 to 42:00 with a sample rate
|
7
8
|
# of 30 seconds, and out pops 57.
|
@@ -26,26 +27,41 @@ module RunbyPace
|
|
26
27
|
# until it matches that of the data. (See #curve_minutes)
|
27
28
|
attr_reader :midpoint_radius_divisor
|
28
29
|
|
29
|
-
def initialize(
|
30
|
-
@fastest_pace_km =
|
31
|
-
@slowest_pace_km =
|
30
|
+
def initialize(golden_pace_set, midpoint_radius_divisor)
|
31
|
+
@fastest_pace_km = golden_pace_set.fastest
|
32
|
+
@slowest_pace_km = golden_pace_set.slowest
|
32
33
|
@midpoint_radius_divisor = midpoint_radius_divisor
|
33
34
|
end
|
34
35
|
|
35
36
|
# Calculate the slope of the line between the fastest and slowest paces
|
36
37
|
def slope
|
37
|
-
(@slowest_pace_km.total_minutes - @fastest_pace_km.total_minutes) / (DATA_POINTS_COUNT - 1)
|
38
|
+
(@slowest_pace_km.time.total_minutes - @fastest_pace_km.time.total_minutes) / (DATA_POINTS_COUNT - 1)
|
38
39
|
end
|
39
40
|
|
40
41
|
# Calculate the prescribed pace for the given 5K time
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
42
|
+
# @return [Pace]
|
43
|
+
def calc(five_k_time, distance_units = :km)
|
44
|
+
five_k_time = Runby.sanitize(five_k_time).as(RunbyTime)
|
45
|
+
distance_units = Runby.sanitize(distance_units).as(DistanceUnit)
|
46
|
+
|
47
|
+
minutes_per_unit = calculate_minutes_per_unit(distance_units, five_k_time)
|
48
|
+
build_pace minutes_per_unit, distance_units
|
45
49
|
end
|
46
50
|
|
47
51
|
private
|
48
52
|
|
53
|
+
def build_pace(minutes_per_unit, distance_units)
|
54
|
+
time = RunbyTime.from_minutes(minutes_per_unit)
|
55
|
+
distance = Distance.new distance_units, 1
|
56
|
+
Pace.new time, distance
|
57
|
+
end
|
58
|
+
|
59
|
+
def calculate_minutes_per_unit(distance_units, five_k_time)
|
60
|
+
x2 = ((five_k_time.total_minutes * 2) - (MIDPOINT_X - 1)) - 1
|
61
|
+
minutes_per_km = slope * x2 + @fastest_pace_km.time.total_minutes + curve_minutes(x2)
|
62
|
+
minutes_per_km * distance_units.conversion_factor
|
63
|
+
end
|
64
|
+
|
49
65
|
# Since the paces for each 5K time do not progress in a straight line
|
50
66
|
# when plotted on a graph, but rather a curve with its highest point near
|
51
67
|
# the center, we must add some seconds to the calculated time depending on
|
@@ -55,12 +71,12 @@ module RunbyPace
|
|
55
71
|
# The default curve radius is the same as the midpoint of the X axis,
|
56
72
|
# forming a circle. Use #midpoint_radius_divisor to reduce it's size.
|
57
73
|
def curve_minutes(x_axis)
|
58
|
-
return 0 if @midpoint_radius_divisor
|
74
|
+
return 0 if @midpoint_radius_divisor.zero?
|
59
75
|
midpoint_reduction = x_axis
|
60
76
|
midpoint = MIDPOINT_X
|
61
77
|
if midpoint_reduction > midpoint
|
62
78
|
midpoint_reduction = midpoint - (midpoint_reduction - midpoint)
|
63
|
-
midpoint_reduction = 0 if midpoint_reduction
|
79
|
+
midpoint_reduction = 0 if midpoint_reduction.negative?
|
64
80
|
end
|
65
81
|
# TODO: Use an actual curve instead of a triangle to calculate the number of minutes to add.
|
66
82
|
midpoint_reduction / @midpoint_radius_divisor / 60
|
@@ -1,15 +1,33 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
attr_reader :fast, :slow
|
3
|
+
require_relative 'runby_range'
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
module Runby
|
6
|
+
# Represents a range of paces, from fast to slow.
|
7
|
+
class PaceRange < RunbyRange
|
8
|
+
def initialize(fast, slow, distance_units = :km)
|
9
|
+
if fast.is_a?(Pace) && slow.is_a?(Pace)
|
10
|
+
@fast = fast
|
11
|
+
@slow = slow
|
12
|
+
else
|
13
|
+
# Hopefully 'fast' and 'slow' are parseable as a RunbyTime
|
14
|
+
distance = Distance.new distance_units, 1
|
15
|
+
@fast = Pace.new(fast, distance)
|
16
|
+
@slow = Pace.new(slow, distance)
|
17
|
+
end
|
18
|
+
freeze
|
9
19
|
end
|
10
20
|
|
11
|
-
def
|
12
|
-
|
21
|
+
def as_speed_range
|
22
|
+
SpeedRange.new @fast.as_speed, @slow.as_speed
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s(format: :short)
|
26
|
+
if @fast == @slow
|
27
|
+
@fast.to_s(format: format)
|
28
|
+
else
|
29
|
+
@fast.to_s(format: format).sub(@fast.time.to_s, "#{@fast.time}-#{@slow.time}")
|
30
|
+
end
|
13
31
|
end
|
14
32
|
end
|
15
|
-
end
|
33
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Runby
|
4
|
+
# An assortment of mathematical functions related to running.
|
5
|
+
class RunMath
|
6
|
+
def self.predict_race_time(race1_distance, race1_time, target_distance)
|
7
|
+
race1_distance = Runby.sanitize(race1_distance).as(Distance)
|
8
|
+
race1_time = Runby.sanitize(race1_time).as(RunbyTime)
|
9
|
+
target_distance = Runby.sanitize(target_distance).as(Distance)
|
10
|
+
|
11
|
+
race1_time * (target_distance / race1_distance)**1.06
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/runby_pace/run_type.rb
CHANGED
@@ -1,18 +1,26 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
module Runby
|
4
|
+
# Base class for all run types
|
3
5
|
class RunType
|
4
6
|
def description
|
5
7
|
'No description'
|
6
8
|
end
|
7
9
|
|
8
|
-
def
|
10
|
+
def explanation
|
11
|
+
'No explanation'
|
9
12
|
end
|
13
|
+
|
14
|
+
def lookup_pace(five_k_time, distance_units = :km) end
|
10
15
|
end
|
11
16
|
|
17
|
+
# Extends RunTypes with additional methods.
|
18
|
+
# Since RunTypes is autogenerated in all_run_types.g.rb, we needed a safe way of adding behavior to it
|
19
|
+
# without complicating the codegen.
|
12
20
|
module RunTypes
|
21
|
+
# Returns an initialized run type, given the name of an existing run type
|
13
22
|
def self.new_from_name(run_type_name)
|
14
|
-
Object
|
23
|
+
Object.const_get("Runby::RunTypes::#{run_type_name}").new
|
15
24
|
end
|
16
25
|
end
|
17
|
-
|
18
26
|
end
|
@@ -1,12 +1,14 @@
|
|
1
|
-
# This file is automatically generated by a rake task
|
2
|
-
|
3
|
-
module
|
4
|
-
|
5
|
-
module RunTypes
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
end
|
1
|
+
# This file is automatically generated by a rake task
|
2
|
+
|
3
|
+
module Runby
|
4
|
+
# Encapsulates data and behavior relating to all run types.
|
5
|
+
module RunTypes
|
6
|
+
def self.all
|
7
|
+
%w[DistanceRun EasyRun FastTempoRun FiveKilometerRaceRun LongRun MileRaceRun SlowTempoRun TempoRun TenKilometerRaceRun]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.all_classes
|
11
|
+
[DistanceRun, EasyRun, FastTempoRun, FiveKilometerRaceRun, LongRun, MileRaceRun, SlowTempoRun, TempoRun, TenKilometerRaceRun]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -1,12 +1,14 @@
|
|
1
1
|
# This file is automatically generated by a rake task
|
2
2
|
|
3
|
-
module
|
4
|
-
|
3
|
+
module Runby
|
4
|
+
# Encapsulates data and behavior relating to all run types.
|
5
5
|
module RunTypes
|
6
|
-
|
7
6
|
def self.all
|
8
|
-
%w
|
7
|
+
%w[__RUN_TYPE_NAMES__]
|
9
8
|
end
|
10
9
|
|
10
|
+
def self.all_classes
|
11
|
+
[__RUN_TYPES__]
|
12
|
+
end
|
11
13
|
end
|
12
14
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Runby
|
4
|
+
module RunTypes
|
5
|
+
# Defines the venerable "distance run", the backbone of any distance running program.
|
6
|
+
# Most of your runs should be at this pace. Harder than an "easy run" but still conversational.
|
7
|
+
class DistanceRun < RunType
|
8
|
+
attr_reader :slow_pace_calculator, :fast_pace_calculator
|
9
|
+
|
10
|
+
def description
|
11
|
+
'Distance Run'
|
12
|
+
end
|
13
|
+
|
14
|
+
def explanation
|
15
|
+
'Most of your weekly training should be comprised of Distance Runs. They are faster than easy runs, but you should still be able to carry on a conversation.'
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@fast_pace_calculator = PaceCalculator.new(GoldenPaces.fast, 3.675)
|
20
|
+
@slow_pace_calculator = PaceCalculator.new(GoldenPaces.slow, 2.175)
|
21
|
+
end
|
22
|
+
|
23
|
+
def lookup_pace(five_k_time, distance_units = :km)
|
24
|
+
fast = @fast_pace_calculator.calc(five_k_time, distance_units)
|
25
|
+
slow = @slow_pace_calculator.calc(five_k_time, distance_units)
|
26
|
+
PaceRange.new(fast, slow)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Used in testing, contains hashes mapping 5K race times with the recommended pace-per-km for this run type.
|
30
|
+
class GoldenPaces
|
31
|
+
def self.fast
|
32
|
+
GoldenPaceSet.new('14:00': '03:44',
|
33
|
+
'15:00': '03:58',
|
34
|
+
'20:00': '05:09',
|
35
|
+
'25:00': '06:18',
|
36
|
+
'30:00': '07:24',
|
37
|
+
'35:00': '08:29',
|
38
|
+
'40:00': '09:33',
|
39
|
+
'42:00': '09:58')
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.slow
|
43
|
+
GoldenPaceSet.new('14:00': '04:17',
|
44
|
+
'15:00': '04:33',
|
45
|
+
'20:00': '05:53',
|
46
|
+
'25:00': '07:09',
|
47
|
+
'30:00': '08:23',
|
48
|
+
'35:00': '09:33',
|
49
|
+
'40:00': '10:42',
|
50
|
+
'42:00': '11:10')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,31 +1,52 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
module Runby
|
3
4
|
module RunTypes
|
4
|
-
|
5
|
+
# An easy run is basically a jog. It should be conversational.
|
5
6
|
class EasyRun < RunType
|
6
|
-
attr_reader :
|
7
|
+
attr_reader :slow_pace_calculator, :fast_pace_calculator
|
7
8
|
|
8
9
|
def description
|
9
10
|
'Easy Run'
|
10
11
|
end
|
11
12
|
|
13
|
+
def explanation
|
14
|
+
'Also called a recovery run, the easy run is harder than jogging, but you should still be able to carry on a conversation.'
|
15
|
+
end
|
16
|
+
|
12
17
|
def initialize
|
13
|
-
@
|
14
|
-
@
|
18
|
+
@fast_pace_calculator = PaceCalculator.new(GoldenPaces.fast, 1.99)
|
19
|
+
@slow_pace_calculator = PaceCalculator.new(GoldenPaces.slow, 1.35)
|
15
20
|
end
|
16
21
|
|
17
|
-
def
|
18
|
-
fast = @
|
19
|
-
slow = @
|
22
|
+
def lookup_pace(five_k_time, distance_units = :km)
|
23
|
+
fast = @fast_pace_calculator.calc(five_k_time, distance_units)
|
24
|
+
slow = @slow_pace_calculator.calc(five_k_time, distance_units)
|
20
25
|
PaceRange.new(fast, slow)
|
21
26
|
end
|
22
27
|
|
28
|
+
# Used in testing, contains hashes mapping 5K race times with the recommended pace-per-km for this run type.
|
23
29
|
class GoldenPaces
|
24
30
|
def self.fast
|
25
|
-
|
31
|
+
GoldenPaceSet.new('14:00': '04:17',
|
32
|
+
'15:00': '04:33',
|
33
|
+
'20:00': '05:53',
|
34
|
+
'25:00': '07:09',
|
35
|
+
'30:00': '08:23',
|
36
|
+
'35:00': '09:33',
|
37
|
+
'40:00': '10:41',
|
38
|
+
'42:00': '11:08')
|
26
39
|
end
|
40
|
+
|
27
41
|
def self.slow
|
28
|
-
|
42
|
+
GoldenPaceSet.new('14:00': '05:01',
|
43
|
+
'15:00': '05:20',
|
44
|
+
'20:00': '06:51',
|
45
|
+
'25:00': '08:17',
|
46
|
+
'30:00': '09:38',
|
47
|
+
'35:00': '10:56',
|
48
|
+
'40:00': '12:10',
|
49
|
+
'42:00': '12:39')
|
29
50
|
end
|
30
51
|
end
|
31
52
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'tempo_run'
|
3
|
+
|
4
|
+
module Runby
|
5
|
+
module RunTypes
|
6
|
+
# The "fast tempo" pace roughly equates to your half-marathon pace.
|
7
|
+
# It's a pace you could maintain for about an hour, if pressed.
|
8
|
+
class FastTempoRun < TempoRun
|
9
|
+
def description
|
10
|
+
'Fast Tempo Run'
|
11
|
+
end
|
12
|
+
|
13
|
+
def explanation
|
14
|
+
'The fast tempo run is an interval workout of 15-25 minutes per repetition. The pace roughly corresponds to that of your half-marathon race pace.'
|
15
|
+
end
|
16
|
+
|
17
|
+
def lookup_pace(five_k_time, distance_units = :km)
|
18
|
+
fast = @fast_pace_calculator.calc(five_k_time, distance_units)
|
19
|
+
PaceRange.new(fast, fast)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|