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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1bbbedffe293b57223ce3e7795a29c5f0fb58db5
4
- data.tar.gz: 12affb0bf93cffcaa9588ce58249183423c37a18
2
+ SHA256:
3
+ metadata.gz: c865faf1b57fa5d059871913a9ffe259c5cfccc6f5e5c80819c2bcf7c7477ee9
4
+ data.tar.gz: d34065214cd2129a7f433c201150afdaf88bbf845b7214c2c88ec095f1064a4c
5
5
  SHA512:
6
- metadata.gz: da7fc955a4382d455e2deb875c65746498955a7e8816d23867580cbb9522f14c685f0aea24323d269ace2791abf74d3fcba0bf046277e97256da7b8aec0b3600
7
- data.tar.gz: 4d9955c7367220eac7097fbda94f12fc961295f7e07a78663cfc47ede689a8a3107fe2f0d7afdfb53e986f2d8a89d8d3f95a00f6c9a2e5fdea6b5d61f4de7407
6
+ metadata.gz: 1e20444a7feb901736b98135e27ac2f63d6d06e7f0cd0e594bbd5a2853a2e2586dad8338733d5f32c06265ddbc04eaf79ca3e672b08f2e3f495b350bca7d620d
7
+ data.tar.gz: e2405ad249c55e3473b9999cf761373aab346b26d395417921865d6e7d9b44b9f7a26cebebc1aedae27a531feacc2d20f3be98df520458d739cd2d25f944887a
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.4
3
+ Exclude:
4
+ - 'bin/**'
5
+
6
+ Style/Encoding:
7
+ Enabled: false
8
+
9
+ Metrics/LineLength:
10
+ Max: 120
data/.travis.yml CHANGED
@@ -1,11 +1,18 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2.4
4
- before_install: gem install bundler -v 1.11.2
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
@@ -1,5 +1,9 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ group :development do
4
+ gem 'rubocop', require: false
5
+ end
6
+
3
7
  group :test do
4
8
  gem 'coveralls', require: false
5
9
  end
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
- Note that this project is currently being developed, so the gem doesn't even exist yet. Coming soon!
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 as:
40
+ Or install it yourself:
38
41
 
39
42
  $ gem install runby_pace
40
43
 
41
44
  ## Usage
42
45
 
43
- TODO: Coming soon... When the class interfaces are fleshed out.
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`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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 :default => :build
8
+ task default: :build
7
9
 
8
- task :build => [:gen_all_run_types,:spec]
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 { |part|
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!('__RUN_TYPES__', all_run_types.join(' '))
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
- # This file was generated by Bundler.
4
- #
5
- # The application '_guard-core' is installed as part of a gem, and
6
- # this file is here to facilitate running it.
7
- #
8
-
9
- require "pathname"
10
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
11
- Pathname.new(__FILE__).realpath)
12
-
13
- require "rubygems"
14
- require "bundler/setup"
15
-
16
- load Gem.bin_path("guard", "_guard-core")
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
- # This file was generated by Bundler.
4
- #
5
- # The application 'guard' is installed as part of a gem, and
6
- # this file is here to facilitate running it.
7
- #
8
-
9
- require "pathname"
10
- ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
11
- Pathname.new(__FILE__).realpath)
12
-
13
- require "rubygems"
14
- require "bundler/setup"
15
-
16
- load Gem.bin_path("guard", "guard")
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,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require "bundler/setup"
5
+ rescue LoadError
6
+ end
7
+
8
+ begin
9
+ require 'runby_pace'
10
+ rescue LoadError
11
+ require_relative '../lib/runby_pace'
12
+ end
13
+
14
+ cli = Runby::Cli::Cli.new(ARGV)
15
+ cli.run
@@ -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