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.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c865faf1b57fa5d059871913a9ffe259c5cfccc6f5e5c80819c2bcf7c7477ee9
|
4
|
+
data.tar.gz: d34065214cd2129a7f433c201150afdaf88bbf845b7214c2c88ec095f1064a4c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e20444a7feb901736b98135e27ac2f63d6d06e7f0cd0e594bbd5a2853a2e2586dad8338733d5f32c06265ddbc04eaf79ca3e672b08f2e3f495b350bca7d620d
|
7
|
+
data.tar.gz: e2405ad249c55e3473b9999cf761373aab346b26d395417921865d6e7d9b44b9f7a26cebebc1aedae27a531feacc2d20f3be98df520458d739cd2d25f944887a
|
data/.rubocop.yml
ADDED
data/.travis.yml
CHANGED
@@ -1,11 +1,18 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
- 2.
|
4
|
-
before_install: gem install bundler -v 1.
|
3
|
+
- 2.4
|
4
|
+
before_install: gem install bundler -v 1.12.5
|
5
5
|
deploy:
|
6
|
+
skip_cleanup: true
|
6
7
|
provider: rubygems
|
7
8
|
api_key:
|
8
9
|
secure: AOOSaXksG1n9LsFbpc5wJWk/Rj7jYFohJMsfLEosWPHw1Kidgng7OlpgWC6+iBRM78Bo6lgeMK/8Ng9KxU9+Cs/+6pKjrMrj4T/cVmY4tlAXhHW3DBJe4KTmmTmTOWsSP4icPIXKCY24ExbIGLnwCGuagezBENyTemLBkQXMBXRtyubr77M2fB0K5zJwx76n1kGM6MkGOdEF4Qj64OswTudHxc98Rs2p4xdf8ft8YqGrGj19Z+ALj1NmOZgPfEXV0SuwtK6Yk3XPvCXojXJRRd4JfUQ/dghmiQkKBS7o7bwnhcWd6H5aJFJVVSjDwerdLpgHU0O16PSU2+gh4QcMDeWnzkciGaQQIrsy8eU3EUMN+N1Pn70eXFb4PtgS7esBTbzwivjZt94DYyvITaoOcZTs6ceX838CAj7QQcNkMZhb7HV49awlIYhJibpB5AWuBulXwTaD+6VozTGh0nVdAXaR8BcItv933rucmrjQx4WRvChjTV+oskW/C2QI7BgAZRQyqHy1UIuWe1r+kWWs1i+L8anshW/MrJ6S6JUUgUiONP1CldTgUezAjiIL6wiuIurjLRRa//wXmedr0x37cw+bwgq4l9KcU0X/0fYx2+8YN3BC5qvmxha7+AUljL1u/Jw8zwIa36g608tZ7RbshgTHDmFXxLRLVuSt3vjDlAI=
|
9
10
|
gem: runby_pace
|
10
11
|
on:
|
11
12
|
repo: tygerbytes/runby-pace
|
13
|
+
git:
|
14
|
+
depth: 1024
|
15
|
+
notifications:
|
16
|
+
email:
|
17
|
+
on_success: always
|
18
|
+
on_failure: always
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -6,6 +6,7 @@ RunbyPace contains the core logic for calculating the target "paces" used by run
|
|
6
6
|
| | |
|
7
7
|
| --- | --- |
|
8
8
|
| **Build** | [![Build Status](https://travis-ci.org/tygerbytes/runby-pace.svg?branch=master)](https://travis-ci.org/tygerbytes/runby-pace) |
|
9
|
+
| **Grade** | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/7c228c8601ff4eeb9dfe0f6c60867f25)](https://www.codacy.com/app/tygerbytes/runby-pace?utm_source=github.com&utm_medium=referral&utm_content=tygerbytes/runby-pace&utm_campaign=badger) |
|
9
10
|
| **Coverage** | [![Coverage Status](https://coveralls.io/repos/github/tygerbytes/runby-pace/badge.svg?branch=master)](https://coveralls.io/github/tygerbytes/runby-pace?branch=master) |
|
10
11
|
| **Gem** | [![gem](https://img.shields.io/gem/v/runby_pace.svg)](https://rubygems.org/gems/runby_pace) |
|
11
12
|
|
@@ -20,7 +21,9 @@ Any sort of running program will include runs at varying paces, easy runs, dista
|
|
20
21
|
So this is great, but a little tedious. RunbyPace automates this whole process by calculating all of the paces for you.
|
21
22
|
All you need is your current 5K time and some Ruby, and you're off running at just the right pace.
|
22
23
|
|
23
|
-
|
24
|
+
RunbyPace also encapsulates the logic and math necessary for many running-related calculations based on time, pace, speed, unit coversions, etc. If you're tired of constantly converting minutes and seconds to decimal and back again, RunbyPace is for you.
|
25
|
+
|
26
|
+
[![RunbyPace logo](misc/runbypace_logo.png)](https://runbypace.com)
|
24
27
|
|
25
28
|
## Installation
|
26
29
|
|
@@ -34,24 +37,32 @@ And then execute:
|
|
34
37
|
|
35
38
|
$ bundle
|
36
39
|
|
37
|
-
Or install it yourself
|
40
|
+
Or install it yourself:
|
38
41
|
|
39
42
|
$ gem install runby_pace
|
40
43
|
|
41
44
|
## Usage
|
42
45
|
|
43
|
-
|
46
|
+
I plan to craft better docs in the future, but for now the **specs** make for excellent class usage documentation: https://github.com/tygerbytes/runby-pace/tree/master/spec/runby_pace
|
47
|
+
|
48
|
+
For a live front end written in **Rails**, see https://runbypace.com. It's code will be open-sourced as well, as soon as we can guarantee secure deployment.
|
49
|
+
|
50
|
+
For an open-source example front end written in **Elm**, see https://github.com/tygerbytes/pacebyelm
|
51
|
+
|
52
|
+
The CLI is still in its infancy, but the gem comes with a basic CLI/REPL (`bin/runbypace`)
|
44
53
|
|
45
54
|
## Development
|
46
55
|
|
47
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
56
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` or `bin/runbypace` for an interactive prompt that will allow you to experiment.
|
48
57
|
|
49
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
58
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
50
59
|
|
51
60
|
## Contributing
|
52
61
|
|
53
62
|
Bug reports and pull requests are welcome on GitHub at https://github.com/tygerbytes/runby-pace.
|
54
63
|
|
64
|
+
Contribute front-end and CLI ideas at [@runbypace](https://twitter.com/runbypace).
|
65
|
+
|
55
66
|
## Acknowledgements
|
56
67
|
|
57
68
|
Crafted with care, with the support of [JetBrains RubyMine](https://www.jetbrains.com/ruby/)
|
data/Rakefile
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'bundler/gem_tasks'
|
2
4
|
require 'rspec/core/rake_task'
|
3
5
|
|
4
6
|
RSpec::Core::RakeTask.new(:spec)
|
5
7
|
|
6
|
-
task :
|
8
|
+
task default: :build
|
7
9
|
|
8
|
-
task :
|
10
|
+
task build: %i[gen_version_number gen_all_run_types spec]
|
9
11
|
|
10
12
|
desc 'Generate the all_run_types.g.rb file'
|
11
13
|
task :gen_all_run_types do
|
12
|
-
puts "\e[32m__TEXT__\e[0m".gsub('__TEXT__', 'Generate all_run_types.g.rb')
|
14
|
+
puts "\e[32m__TEXT__\e[0m".gsub('__TEXT__', '> Generate all_run_types.g.rb')
|
13
15
|
run_types_path = './lib/runby_pace/run_types'
|
14
16
|
|
15
17
|
# Parse *_run.rb file names to generate array of the run type class names
|
@@ -19,16 +21,48 @@ task :gen_all_run_types do
|
|
19
21
|
filename_sans_extension = filename[0, filename.length - 3]
|
20
22
|
parts = filename_sans_extension.to_s.downcase.split(/_|\./)
|
21
23
|
run_type = ''
|
22
|
-
parts.each
|
24
|
+
parts.each do |part|
|
23
25
|
run_type += part[0].upcase + part[-(part.length - 1), part.length - 1]
|
24
|
-
|
26
|
+
end
|
25
27
|
run_type
|
26
28
|
end
|
27
29
|
puts all_run_types.join(' ')
|
28
30
|
|
29
31
|
# Write run types to the generated file, all_run_types.g.rb
|
30
32
|
template = File.read(File.join(run_types_path, 'all_run_types.template'))
|
31
|
-
template.gsub!('
|
33
|
+
template.gsub!('__RUN_TYPE_NAMES__', all_run_types.join(' '))
|
34
|
+
template.gsub!('__RUN_TYPES__', all_run_types.join(', '))
|
32
35
|
File.write(File.join(run_types_path, 'all_run_types.g.rb'), template)
|
33
36
|
puts "\e[32mDone\e[0m\n\n"
|
34
37
|
end
|
38
|
+
|
39
|
+
desc 'Generate version number'
|
40
|
+
task :gen_version_number do
|
41
|
+
puts "\e[32m__TEXT__\e[0m".gsub('__TEXT__', '> Generate version number')
|
42
|
+
|
43
|
+
# Generate "teeny" version number based on the number of commits since the last tagged major/minor release
|
44
|
+
latest_tagged_release = `git describe --tags --abbrev=0 --match v*`.to_s.chomp
|
45
|
+
puts "\e[32m__TEXT__\e[0m".gsub('__TEXT__', "Latest tagged release is #{latest_tagged_release}")
|
46
|
+
version = "#{latest_tagged_release[1..-1]}.#{`git rev-list --count #{latest_tagged_release}..HEAD`}".chomp
|
47
|
+
|
48
|
+
# Write version number to generated file
|
49
|
+
path = './lib/runby_pace'
|
50
|
+
template = File.read(File.join(path, 'version.seed'))
|
51
|
+
template.gsub!('__VERSION__', version)
|
52
|
+
version_file_path = File.join(path, 'version.g.rb')
|
53
|
+
File.write(version_file_path, template)
|
54
|
+
with_no_warnings do
|
55
|
+
# Silencing warnings about redefining constants, since it's intentional
|
56
|
+
load version_file_path
|
57
|
+
Runby::VERSION = Runby::GENERATED_VERSION
|
58
|
+
end
|
59
|
+
puts "\e[32m__TEXT__\e[0m".gsub('__TEXT__', "Version: #{Runby::VERSION}")
|
60
|
+
end
|
61
|
+
|
62
|
+
def with_no_warnings
|
63
|
+
warning_level = $VERBOSE
|
64
|
+
$VERBOSE = nil
|
65
|
+
result = yield
|
66
|
+
$VERBOSE = warning_level
|
67
|
+
result
|
68
|
+
end
|
data/bin/_guard-core
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
require "
|
15
|
-
|
16
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application '_guard-core' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("guard", "_guard-core")
|
data/bin/guard
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
require "
|
15
|
-
|
16
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'guard' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require "pathname"
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "bundler/setup"
|
16
|
+
|
17
|
+
load Gem.bin_path("guard", "guard")
|
data/bin/runbypace
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'readline'
|
4
|
+
require 'optparse'
|
5
|
+
require_relative 'config'
|
6
|
+
|
7
|
+
module Runby
|
8
|
+
module Cli
|
9
|
+
# Command line interface and REPL for RunbyPace
|
10
|
+
class Cli
|
11
|
+
def initialize(args = ARGV)
|
12
|
+
@args = args
|
13
|
+
@config = Config.new
|
14
|
+
@options = parse_options args
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
puts 'Runby Pace REPL!'
|
19
|
+
bnd = binding
|
20
|
+
while (input = Readline.readline('🏃 ', true))
|
21
|
+
begin
|
22
|
+
result = bnd.eval input
|
23
|
+
rescue StandardError => e
|
24
|
+
puts "#{e.class}: #{e.message}"
|
25
|
+
else
|
26
|
+
puts result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def print_targets(five_k_time, distance_units = :mi)
|
32
|
+
five_k_time = @config['five_k_time'] if five_k_time.nil?
|
33
|
+
|
34
|
+
five_k_time = Runby.sanitize(five_k_time).as(RunbyTime)
|
35
|
+
puts "\nIf you can run a 5K in #{five_k_time}, your training paces should be:"
|
36
|
+
paces = []
|
37
|
+
RunTypes.all_classes.each do |run_type|
|
38
|
+
run = run_type.new
|
39
|
+
paces.push(description: run.description, pace: run.lookup_pace(five_k_time, distance_units))
|
40
|
+
end
|
41
|
+
paces.sort_by { |p| p[:pace].fast }.reverse_each { |p| puts " #{p[:description]}: #{p[:pace]}" }
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# -- Shortcuts for the REPL
|
46
|
+
def di(*args)
|
47
|
+
Distance.new(*args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def du(*args)
|
51
|
+
DistanceUnit.new(*args)
|
52
|
+
end
|
53
|
+
|
54
|
+
def pc(*args)
|
55
|
+
Pace.new(*args)
|
56
|
+
end
|
57
|
+
|
58
|
+
def sp(*args)
|
59
|
+
Speed.new(*args)
|
60
|
+
end
|
61
|
+
|
62
|
+
def tm(*args)
|
63
|
+
RunbyTime.new(*args)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def parse_options(options)
|
69
|
+
args = { targets: nil }
|
70
|
+
|
71
|
+
OptionParser.new do |opts|
|
72
|
+
opts.banner = 'Usage: runbypace.rb [options]'
|
73
|
+
|
74
|
+
opts.on('-h', '--help', 'Display this help message') do
|
75
|
+
puts opts
|
76
|
+
exit
|
77
|
+
end
|
78
|
+
|
79
|
+
opts.on('-c', '--config [SETTING][=NEW_VALUE]', 'Get or set a configuration value') do |config|
|
80
|
+
manage_config config
|
81
|
+
exit
|
82
|
+
end
|
83
|
+
|
84
|
+
opts.on('-t', '--targets [5K race time]', 'Show target paces') do |targets|
|
85
|
+
args[:targets] = targets
|
86
|
+
print_targets targets
|
87
|
+
exit
|
88
|
+
end
|
89
|
+
end.parse!(options)
|
90
|
+
args
|
91
|
+
end
|
92
|
+
|
93
|
+
def manage_config(config)
|
94
|
+
c = parse_config_setting config
|
95
|
+
unless c.key
|
96
|
+
# No key specified. Print all settings.
|
97
|
+
@config.pretty_print
|
98
|
+
return
|
99
|
+
end
|
100
|
+
if c.value
|
101
|
+
# Set setting "key" to new "value"
|
102
|
+
@config[c.key] = c.value
|
103
|
+
return
|
104
|
+
end
|
105
|
+
if c.clear_setting
|
106
|
+
@config[c.key] = nil
|
107
|
+
else
|
108
|
+
# Print the value of setting "key"
|
109
|
+
p @config[c.key]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_config_setting(setting)
|
114
|
+
setting = '' if setting.nil?
|
115
|
+
Class.new do
|
116
|
+
attr_reader :key, :value, :clear_setting
|
117
|
+
def initialize(setting)
|
118
|
+
tokens = setting.split('=')
|
119
|
+
@key = tokens[0]
|
120
|
+
@value = tokens[1]
|
121
|
+
@clear_setting = (@value.nil? && setting.include?('='))
|
122
|
+
end
|
123
|
+
end.new(setting)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
module Runby
|
7
|
+
#
|
8
|
+
module Cli
|
9
|
+
class Config
|
10
|
+
USER_CONFIG_PATH = File.expand_path('~/.runbypace').freeze
|
11
|
+
|
12
|
+
VALID_OPTIONS = {
|
13
|
+
five_k_time: { validate_as: RunbyTime }
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@settings = load_user_settings
|
18
|
+
end
|
19
|
+
|
20
|
+
def load_user_settings
|
21
|
+
if File.exist? USER_CONFIG_PATH
|
22
|
+
YAML.load_file USER_CONFIG_PATH
|
23
|
+
else
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def store_user_settings
|
29
|
+
File.open(USER_CONFIG_PATH, 'w') { |file| file.write @settings.to_yaml }
|
30
|
+
end
|
31
|
+
|
32
|
+
def [](key)
|
33
|
+
return unless known_setting?(key)
|
34
|
+
return unless option_configured?(key)
|
35
|
+
"#{key} => #{@settings[key]}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def []=(key, value)
|
39
|
+
return unless known_setting?(key)
|
40
|
+
if value
|
41
|
+
value = sanitize_value key, value
|
42
|
+
return unless value
|
43
|
+
@settings[key] = value.to_s
|
44
|
+
else
|
45
|
+
@settings.delete(key)
|
46
|
+
end
|
47
|
+
store_user_settings
|
48
|
+
end
|
49
|
+
|
50
|
+
def pretty_print
|
51
|
+
pp @settings
|
52
|
+
end
|
53
|
+
|
54
|
+
def known_setting?(key)
|
55
|
+
unless VALID_OPTIONS.key?(key.to_sym)
|
56
|
+
puts "Unknown setting #{key}"
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
def sanitize_value(key, value)
|
63
|
+
cls = VALID_OPTIONS[key.to_sym][:validate_as]
|
64
|
+
begin
|
65
|
+
value = Runby.sanitize(value).as(cls)
|
66
|
+
rescue StandardError => ex
|
67
|
+
value = nil
|
68
|
+
p ex.message
|
69
|
+
end
|
70
|
+
value
|
71
|
+
end
|
72
|
+
|
73
|
+
def option_configured?(key)
|
74
|
+
unless @settings.key? key
|
75
|
+
puts "#{key} not configured. Set with:\n\trunbypace --config #{key} VALUE"
|
76
|
+
false
|
77
|
+
end
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Runby
|
4
|
+
# Represents a distance (distance UOM and multiplier)
|
5
|
+
class Distance
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
attr_reader :uom, :multiplier
|
9
|
+
|
10
|
+
def self.new(uom = :km, multiplier = 1)
|
11
|
+
return uom if uom.is_a? Distance
|
12
|
+
return Distance.parse uom if uom.is_a? String
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(uom = :km, multiplier = 1)
|
17
|
+
case uom
|
18
|
+
when DistanceUnit
|
19
|
+
init_from_distance_unit uom, multiplier
|
20
|
+
when Symbol
|
21
|
+
init_from_symbol uom, multiplier
|
22
|
+
else
|
23
|
+
raise 'Invalid distance unit of measure'
|
24
|
+
end
|
25
|
+
freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
def convert_to(target_uom)
|
29
|
+
target_uom = DistanceUnit.new target_uom unless target_uom.is_a?(DistanceUnit)
|
30
|
+
return self if @uom == target_uom
|
31
|
+
target_multiplier = kilometers / (target_uom.conversion_factor * 1.0)
|
32
|
+
Distance.new target_uom, target_multiplier
|
33
|
+
end
|
34
|
+
|
35
|
+
def meters
|
36
|
+
kilometers * 1000.0
|
37
|
+
end
|
38
|
+
|
39
|
+
def kilometers
|
40
|
+
@multiplier * @uom.conversion_factor
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.parse(str)
|
44
|
+
str = str.strip.chomp.downcase
|
45
|
+
multiplier = str.scan(/[\d,.]+/).first
|
46
|
+
multiplier = multiplier.nil? ? 1 : multiplier.to_f
|
47
|
+
uom = str.scan(/[-_a-z ]+$/).first
|
48
|
+
raise "Unable to find distance unit in '#{str}'" if uom.nil?
|
49
|
+
|
50
|
+
parsed_uom = Runby::DistanceUnit.parse uom
|
51
|
+
raise "'#{uom.strip}' is not recognized as a distance unit" if parsed_uom.nil?
|
52
|
+
|
53
|
+
new parsed_uom, multiplier
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.try_parse(str)
|
57
|
+
distance, error_message = nil
|
58
|
+
begin
|
59
|
+
distance = parse str
|
60
|
+
rescue StandardError => ex
|
61
|
+
error_message = ex.message.to_s
|
62
|
+
end
|
63
|
+
{ distance: distance, error: error_message }
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s(format: :short)
|
67
|
+
formatted_multiplier = format('%g', @multiplier.round(2))
|
68
|
+
case format
|
69
|
+
when :short then "#{formatted_multiplier} #{@uom.to_s(format: format)}"
|
70
|
+
when :long then "#{formatted_multiplier} #{@uom.to_s(format: format, pluralize: (@multiplier > 1))}"
|
71
|
+
else raise "Invalid string format #{format}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @param [Distance, String] other
|
76
|
+
def <=>(other)
|
77
|
+
raise "Unable to compare Runby::Distance to #{other.class}(#{other})" unless [Distance, String].include? other.class
|
78
|
+
if other.is_a?(String)
|
79
|
+
return 0 if to_s == other || to_s(format: :long) == other
|
80
|
+
return self <=> Distance.try_parse(other)[:distance]
|
81
|
+
end
|
82
|
+
kilometers <=> other.kilometers
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param [Distance] other
|
86
|
+
# @return [Distance]
|
87
|
+
def +(other)
|
88
|
+
raise "Cannot add Runby::Distance to #{other.class}" unless other.is_a?(Distance)
|
89
|
+
sum_in_km = Distance.new(:km, kilometers + other.kilometers)
|
90
|
+
sum_in_km.convert_to(@uom)
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param [Distance] other
|
94
|
+
# @return [Distance]
|
95
|
+
def -(other)
|
96
|
+
raise "Cannot add Runby::Distance to #{other.class}" unless other.is_a?(Distance)
|
97
|
+
sum_in_km = Distance.new(:km, kilometers - other.kilometers)
|
98
|
+
sum_in_km.convert_to(@uom)
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param [Numeric] other
|
102
|
+
# @return [Distance]
|
103
|
+
def *(other)
|
104
|
+
raise "Cannot multiply Runby::Distance by #{other.class}" unless other.is_a?(Numeric)
|
105
|
+
product_in_km = Distance.new(:km, kilometers * other)
|
106
|
+
product_in_km.convert_to(@uom)
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param [Numeric, Distance] other
|
110
|
+
# @return [Distance, Numeric]
|
111
|
+
def /(other)
|
112
|
+
raise "Cannot divide Runby::Distance by #{other.class}" unless other.is_a?(Numeric) || other.is_a?(Distance)
|
113
|
+
if other.is_a?(Numeric)
|
114
|
+
quotient_in_km = Distance.new(:km, kilometers / other)
|
115
|
+
return quotient_in_km.convert_to(@uom)
|
116
|
+
elsif other.is_a?(Distance)
|
117
|
+
return kilometers / other.kilometers
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def init_from_distance_unit(uom, multiplier)
|
124
|
+
@uom = uom
|
125
|
+
@multiplier = multiplier
|
126
|
+
end
|
127
|
+
|
128
|
+
def init_from_symbol(distance_uom_symbol, multiplier)
|
129
|
+
raise "Unknown unit of measure #{distance_uom_symbol}" unless Runby::DistanceUnit.known_uom? distance_uom_symbol
|
130
|
+
raise 'Invalid multiplier' unless multiplier.is_a?(Numeric)
|
131
|
+
@uom = DistanceUnit.new distance_uom_symbol
|
132
|
+
@multiplier = multiplier * 1.0
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|