runby_pace 0.2.50 → 0.2.50.111

Sign up to get free protection for your applications and to get access to all the features.
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