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
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** | [](https://travis-ci.org/tygerbytes/runby-pace) |
|
9
|
+
| **Grade** | [](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** | [](https://coveralls.io/github/tygerbytes/runby-pace?branch=master) |
|
10
11
|
| **Gem** | [](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
|
+
[](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
|