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.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +10 -0
  3. data/.travis.yml +9 -2
  4. data/Gemfile +4 -0
  5. data/README.md +16 -5
  6. data/Rakefile +40 -6
  7. data/bin/_guard-core +17 -16
  8. data/bin/guard +17 -16
  9. data/bin/runbypace +15 -0
  10. data/lib/runby_pace/cli/cli.rb +127 -0
  11. data/lib/runby_pace/cli/config.rb +82 -0
  12. data/lib/runby_pace/distance.rb +135 -0
  13. data/lib/runby_pace/distance_unit.rb +89 -0
  14. data/lib/runby_pace/golden_pace_set.rb +50 -0
  15. data/lib/runby_pace/pace.rb +152 -0
  16. data/lib/runby_pace/{pace_data.rb → pace_calculator.rb} +29 -13
  17. data/lib/runby_pace/pace_range.rb +27 -9
  18. data/lib/runby_pace/run_math.rb +14 -0
  19. data/lib/runby_pace/run_type.rb +12 -4
  20. data/lib/runby_pace/run_types/all_run_types.g.rb +14 -12
  21. data/lib/runby_pace/run_types/all_run_types.template +6 -4
  22. data/lib/runby_pace/run_types/distance_run.rb +55 -0
  23. data/lib/runby_pace/run_types/easy_run.rb +31 -10
  24. data/lib/runby_pace/run_types/fast_tempo_run.rb +23 -0
  25. data/lib/runby_pace/run_types/find_divisor.rb +13 -17
  26. data/lib/runby_pace/run_types/five_kilometer_race_run.rb +22 -0
  27. data/lib/runby_pace/run_types/long_run.rb +32 -10
  28. data/lib/runby_pace/run_types/mile_race_run.rb +24 -0
  29. data/lib/runby_pace/run_types/slow_tempo_run.rb +22 -0
  30. data/lib/runby_pace/run_types/tempo_run.rb +54 -0
  31. data/lib/runby_pace/run_types/ten_kilometer_race_run.rb +23 -0
  32. data/lib/runby_pace/runby_range.rb +22 -0
  33. data/lib/runby_pace/runby_time.rb +138 -0
  34. data/lib/runby_pace/runby_time_parser.rb +80 -0
  35. data/lib/runby_pace/speed.rb +97 -0
  36. data/lib/runby_pace/speed_range.rb +30 -0
  37. data/lib/runby_pace/utility/parameter_sanitizer.rb +29 -0
  38. data/lib/runby_pace/version.rb +17 -2
  39. data/lib/runby_pace/version.seed +5 -0
  40. data/lib/runby_pace.rb +4 -1
  41. data/misc/runbypace_logo.png +0 -0
  42. data/runby_pace.gemspec +5 -6
  43. metadata +32 -9
  44. 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
- module RunbyPace
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(fastest_pace_km, slowest_pace_km, midpoint_radius_divisor)
30
- @fastest_pace_km = RunbyPace::PaceTime.new(fastest_pace_km)
31
- @slowest_pace_km = RunbyPace::PaceTime.new(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
- def calc(five_k_time)
42
- five_k_time = RunbyPace::PaceTime.new(five_k_time)
43
- x2 = ((five_k_time.total_minutes * 2) - (MIDPOINT_X - 1)) - 1
44
- RunbyPace::PaceTime.from_minutes(slope * x2 + @fastest_pace_km.total_minutes + curve_minutes(x2))
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 == 0
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 < 0
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
- module RunbyPace
1
+ # frozen_string_literal: true
2
2
 
3
- class PaceRange
4
- attr_reader :fast, :slow
3
+ require_relative 'runby_range'
5
4
 
6
- def initialize(fast, slow)
7
- @fast = RunbyPace::PaceTime.new(fast)
8
- @slow = RunbyPace::PaceTime.new(slow)
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 to_s
12
- "#{@fast}-#{@slow}"
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
@@ -1,18 +1,26 @@
1
- module RunbyPace
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 pace(five_k_time)
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::const_get("RunbyPace::RunTypes::#{run_type_name}").new
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 RunbyPace
4
-
5
- module RunTypes
6
-
7
- def self.all
8
- %w(EasyRun LongRun)
9
- end
10
-
11
- end
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 RunbyPace
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(__RUN_TYPES__)
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
- module RunbyPace
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 :slow_pace_data, :fast_pace_data
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
- @fast_pace_data = PaceData.new(GoldenPaces::fast[:'14:00'], GoldenPaces::fast[:'42:00'], 1.99)
14
- @slow_pace_data = PaceData.new(GoldenPaces::slow[:'14:00'], GoldenPaces::slow[:'42:00'], 1.35)
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 pace(five_k_time)
18
- fast = @fast_pace_data.calc(five_k_time)
19
- slow = @slow_pace_data.calc(five_k_time)
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
- { '14:00': '04:17', '15:00': '04:33', '20:00': '05:53', '25:00': '07:09', '30:00': '08:23', '35:00': '09:33', '40:00': '10:41', '42:00': '11:08'}
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
- { '14:00': '05:01', '15:00': '05:20', '20:00': '06:51', '25:00': '08:17', '30:00': '09:38', '35:00': '10:56', '40:00': '12:10', '42:00': '12:39'}
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