ruby-measurement 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/CI.yml +30 -0
  3. data/CHANGELOG.md +25 -10
  4. data/README.md +13 -7
  5. data/lib/ruby-measurement/core_ext/string.rb +2 -2
  6. data/lib/ruby-measurement/core_ext/symbol.rb +1 -1
  7. data/lib/ruby-measurement/measurement.rb +101 -42
  8. data/lib/ruby-measurement/unit.rb +34 -28
  9. data/lib/ruby-measurement/version.rb +1 -1
  10. data/ruby-measurement.gemspec +5 -4
  11. data/spec/ruby-measurement/core_ext/numeric_spec.rb +2 -2
  12. data/spec/ruby-measurement/core_ext/string_spec.rb +12 -12
  13. data/spec/ruby-measurement/core_ext/symbol_spec.rb +3 -3
  14. data/spec/ruby-measurement/definitions/metric/area_spec.rb +32 -32
  15. data/spec/ruby-measurement/definitions/metric/capacity_spec.rb +18 -18
  16. data/spec/ruby-measurement/definitions/metric/length_spec.rb +128 -128
  17. data/spec/ruby-measurement/definitions/metric/volume_spec.rb +128 -128
  18. data/spec/ruby-measurement/definitions/metric/weight_spec.rb +160 -160
  19. data/spec/ruby-measurement/definitions/us_customary/area_spec.rb +72 -72
  20. data/spec/ruby-measurement/definitions/us_customary/capacity_spec.rb +32 -32
  21. data/spec/ruby-measurement/definitions/us_customary/length_spec.rb +128 -128
  22. data/spec/ruby-measurement/definitions/us_customary/volume_spec.rb +98 -98
  23. data/spec/ruby-measurement/definitions/us_customary/weight_spec.rb +72 -72
  24. data/spec/ruby-measurement/measurement_spec.rb +238 -164
  25. data/spec/ruby-measurement/unit_builder_spec.rb +35 -35
  26. data/spec/ruby-measurement/unit_spec.rb +84 -41
  27. data/tasks/rspec.rake +1 -1
  28. metadata +29 -25
  29. data/.travis.yml +0 -4
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e9ad871d0be538a1fe0b81b6f7e30d88c843a6d8c26b0682dd4ce68289ebfdf8
4
+ data.tar.gz: fad2d4f562bbca29e685fd64f36ce7473b2b470479daaeb3a02bd707d409c198
5
+ SHA512:
6
+ metadata.gz: 9476515fecd08634e5f194ec1333e8d23ed7978046c99f72aa1a120b462961b711c2c49948bd4927eacfea9add0f378dfd1da865aded3f0f4ec9cff345a0a562
7
+ data.tar.gz: b14f4807067541c3d498f021e0cf79108a768358b8969258ee2452988cb70a3cc06a1cec83bfc6236ecf399f69459845dbb0b98e9cdf9226396cdd28f173d9e5
@@ -0,0 +1,30 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ ruby:
16
+ - head
17
+ - '3.0'
18
+ - '2.7'
19
+ - '2.6'
20
+ - '2.5'
21
+ continue-on-error: ${{ matrix.ruby == 'head' }}
22
+ name: Ruby ${{ matrix.ruby }}
23
+ steps:
24
+ - uses: actions/checkout@v2
25
+ - uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: ${{ matrix.ruby }}
28
+ bundler-cache: true
29
+ - run: |
30
+ bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,24 +1,39 @@
1
+ ## ruby-measurement 1.3.0 (Aug 1, 2021)
2
+
3
+ * Added `Comparable` module to `Measurement`, supporting `<` and `>` operators. *Edmund Lee*
4
+
5
+ ## ruby-measurement 1.2.3 (Mar 27, 2016)
6
+
7
+ * Updated parsing to accept UTF-8 fractions (½, ⅓, etc.). *Matt Huggins*
8
+
9
+ ## ruby-measurement 1.2.2 (Jul 13, 2014)
10
+
11
+ * Fixed `Measurement::Unit#==` to properly compare units. *Matt Huggins*
12
+
13
+ ## ruby-measurement 1.2.1 (Jul 27, 2013)
14
+
15
+ * Added `Measurement.names` to list all available units. *Daniele Palombo*
16
+
1
17
  ## ruby-measurement 1.2.0 (Mar 16, 2013)
2
18
 
3
- * Implement `String#to_measurement`, `String#to_unit`,
19
+ * Added `String#to_measurement`, `String#to_unit`,
4
20
  `Symbol#to_unit`, and `Numeric#to_measurement`. *Matt Huggins*
5
21
 
6
22
  ## ruby-measurement 1.1.0 (Feb 6, 2013)
7
23
 
8
- * Move unit definition logic into `Unit` class. *Matt Huggins*
9
- * Create test suite. *Matt Huggins*
10
- * Fix parsing of `'` and `"` for feet and inches. *Matt Huggins*
11
- * Implement missing metric weights and U.S. customary capacities.
12
- *Matt Huggins*
13
- * Fix several conversions that were found to be miscalculating.
24
+ * Moved unit definition logic into `Unit` class. *Matt Huggins*
25
+ * Added test suite. *Matt Huggins*
26
+ * Fixed parsing of `'` and `"` for feet and inches. *Matt Huggins*
27
+ * Added missing metric weights and U.S. customary capacities. *Matt Huggins*
28
+ * Fixed several conversions that were found to be miscalculating.
14
29
  *Matt Huggins*
15
30
 
16
31
  ## ruby-measurement 1.0.0 (Jan 22, 2013)
17
32
 
18
- * Fix parsing of mixed fractions. *Matt Huggins*
19
- * Allow parsing of quantity & unit to work when not separated by a space.
33
+ * Fixed parsing of mixed fractions. *Matt Huggins*
34
+ * Updated parsing of quantity & unit to work when not separated by a space.
20
35
  *Matt Huggins*
21
- * Change `Measurement` initializer to accept quantity & unit instead of a
36
+ * Updated `Measurement` initializer to accept quantity & unit instead of a
22
37
  single string for parsing. *Matt Huggins*
23
38
 
24
39
  ## ruby-measurement 0.0.1 (Jan 21, 2013)
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Measurement
2
2
 
3
- [![Build Status](https://secure.travis-ci.org/mhuggins/ruby-measurement.png)](http://travis-ci.org/mhuggins/ruby-measurement)
3
+ [![Build Status](https://github.com/mhuggins/ruby-measurement/actions/workflows/CI.yml/badge.svg)](https://github.com/mhuggins/ruby-measurement/actions/workflows/CI.yml)
4
4
  [![Code Climate](https://codeclimate.com/github/mhuggins/ruby-measurement.png)](https://codeclimate.com/github/mhuggins/ruby-measurement)
5
5
 
6
6
  [ruby-measurement](https://github.com/mhuggins/ruby-measurement) is a simple
@@ -108,10 +108,10 @@ To include just one of these systems of measure, require the gem in per the
108
108
  following.
109
109
 
110
110
  require 'ruby-measurement/measurement'
111
-
111
+
112
112
  # Metric units/conversions
113
113
  require 'ruby-measurement/definitions/metric'
114
-
114
+
115
115
  # U.S. customary units/conversions
116
116
  require 'ruby-measurement/definitions/us_customary'
117
117
 
@@ -119,13 +119,13 @@ Additionally, specific categories of units of measure can be included per
119
119
  system.
120
120
 
121
121
  require 'ruby-measurement/measurement'
122
-
122
+
123
123
  # Metric units/conversions
124
124
  require 'ruby-measurement/definitions/metric/area'
125
125
  require 'ruby-measurement/definitions/metric/length'
126
126
  require 'ruby-measurement/definitions/metric/volume'
127
127
  require 'ruby-measurement/definitions/metric/weight'
128
-
128
+
129
129
  # U.S. customary units/conversions
130
130
  require 'ruby-measurement/definitions/us_customary/area'
131
131
  require 'ruby-measurement/definitions/us_customary/length'
@@ -142,13 +142,13 @@ default.
142
142
  unit.convert_to(:week) { |value| value / 7.0 }
143
143
  unit.convert_to(:year) { |value| value / 365.0 }
144
144
  end
145
-
145
+
146
146
  Measurement.define(:wk) do |unit|
147
147
  unit.alias :week, :weeks
148
148
  unit.convert_to(:day) { |value| value * 7.0 }
149
149
  unit.convert_to(:year) { |value| value / 52.0 }
150
150
  end
151
-
151
+
152
152
  Measurement.define(:yr) do |unit|
153
153
  unit.alias :year, :years
154
154
  unit.convert_to(:day) { |value| value * 365.0 }
@@ -162,6 +162,12 @@ that will be used when displaying a measurement.
162
162
  Measurement.parse('3 year') # => 3.0 yr
163
163
  Measurement.parse('3 years') # => 3.0 yr
164
164
 
165
+ ### List all units names defined
166
+
167
+ List all keys you can use as unit
168
+
169
+ Measurement::Unit.names # => ['count','doz','dozen',...]
170
+
165
171
  ## Contributing
166
172
 
167
173
  1. Fork it
@@ -4,8 +4,8 @@ class String
4
4
  def to_measurement
5
5
  Measurement.parse(self)
6
6
  end
7
-
7
+
8
8
  def to_unit
9
- Measurement::Unit[self] or raise ArgumentError, "Invalid unit: #{self}"
9
+ Measurement::Unit[self] or raise ArgumentError, "Invalid unit: '#{self}'"
10
10
  end
11
11
  end
@@ -2,6 +2,6 @@ require 'ruby-measurement/unit'
2
2
 
3
3
  class Symbol
4
4
  def to_unit
5
- Measurement::Unit[self] or raise ArgumentError, "Invalid unit: #{self}"
5
+ Measurement::Unit[self] or raise ArgumentError, "Invalid unit: '#{self}'"
6
6
  end
7
7
  end
@@ -1,34 +1,56 @@
1
+ # encoding: UTF-8
2
+
1
3
  require 'ruby-measurement/unit'
2
4
  require 'ruby-measurement/version'
3
5
 
4
6
  class Measurement
7
+ include Comparable
8
+
5
9
  UNIT_REGEX = /([^\d\s\/].*)/.freeze
6
10
  SCIENTIFIC_NUMBER = /([+-]?\d*\.?\d+(?:[Ee][+-]?)?\d*)/.freeze
7
11
  SCIENTIFIC_REGEX = /\A#{SCIENTIFIC_NUMBER}\s*#{UNIT_REGEX}?\z/.freeze
8
12
  RATIONAL_REGEX = /\A([+-]?\d+\s+)?((\d+)\/(\d+))?\s*#{UNIT_REGEX}?\z/.freeze
9
13
  COMPLEX_REGEX = /\A#{SCIENTIFIC_NUMBER}?#{SCIENTIFIC_NUMBER}i\s*#{UNIT_REGEX}?\z/.freeze
10
-
14
+
15
+ RATIOS = {
16
+ '¼' => '1/4',
17
+ '½' => '1/2',
18
+ '¾' => '3/4',
19
+ '⅓' => '1/3',
20
+ '⅔' => '2/3',
21
+ '⅕' => '1/5',
22
+ '⅖' => '2/5',
23
+ '⅗' => '3/5',
24
+ '⅘' => '4/5',
25
+ '⅙' => '1/6',
26
+ '⅚' => '5/6',
27
+ '⅛' => '1/8',
28
+ '⅜' => '3/8',
29
+ '⅝' => '5/8',
30
+ '⅞' => '7/8',
31
+ }.freeze
32
+
11
33
  attr_reader :quantity, :unit
12
-
34
+
13
35
  def initialize(quantity, unit_name = :count)
14
36
  unit = unit_name
15
37
  unit = Unit[unit_name.to_s] if unit_name.kind_of?(Symbol) || unit_name.kind_of?(String)
16
-
17
- raise ArgumentError, "Invalid quantity: #{quantity}" unless quantity.kind_of?(Numeric)
18
- raise ArgumentError, "Invalid unit: #{unit_name}" unless unit.kind_of?(Unit)
19
-
38
+
39
+ raise ArgumentError, "Invalid quantity: '#{quantity}'" unless quantity.kind_of?(Numeric)
40
+ raise ArgumentError, "Invalid unit: '#{unit_name}'" unless unit.kind_of?(Unit)
41
+
20
42
  @quantity = quantity
21
43
  @unit = unit
22
44
  end
23
-
45
+
24
46
  def inspect
25
47
  to_s
26
48
  end
27
-
49
+
28
50
  def to_s
29
51
  "#{quantity} #{unit}"
30
52
  end
31
-
53
+
32
54
  %w(+ - * /).each do |operator|
33
55
  class_eval <<-END, __FILE__, __LINE__ + 1
34
56
  def #{operator}(obj)
@@ -47,7 +69,7 @@ class Measurement
47
69
  end
48
70
  END
49
71
  end
50
-
72
+
51
73
  def **(obj)
52
74
  case obj
53
75
  when Numeric
@@ -56,41 +78,90 @@ class Measurement
56
78
  raise ArgumentError, "Invalid arithmetic: #{self} ** #{obj}"
57
79
  end
58
80
  end
59
-
60
- def ==(obj)
61
- obj.kind_of?(self.class) && quantity == obj.quantity && unit == obj.unit
81
+
82
+ def <=>(obj)
83
+ if !obj.is_a?(self.class) || obj.unit != self.unit
84
+ return nil
85
+ end
86
+
87
+ if quantity < obj.quantity
88
+ -1
89
+ elsif quantity > obj.quantity
90
+ 1
91
+ else
92
+ 0
93
+ end
62
94
  end
63
-
95
+
64
96
  def convert_to(unit_name)
65
97
  unit = Unit[unit_name]
66
98
  raise ArgumentError, "Invalid unit: '#{unit_name}'" unless unit
67
-
99
+
68
100
  return dup if unit == @unit
69
-
101
+
70
102
  conversion = @unit.conversion(unit.name)
71
103
  raise ArgumentError, "Invalid conversion: '#@unit' to '#{unit.name}'" unless conversion
72
-
104
+
73
105
  self.class.new(conversion.call(@quantity), unit.name)
74
106
  end
75
-
107
+
76
108
  def convert_to!(unit_name)
77
109
  measurement = convert_to(unit_name)
78
110
  @unit, @quantity = measurement.unit, measurement.quantity
79
111
  self
80
112
  end
81
-
82
- def self.parse(str = '0')
83
- str = str.strip
84
-
85
- if str =~ COMPLEX_REGEX
113
+
114
+ class << self
115
+ def parse(str = '0')
116
+ str = normalize(str)
117
+
118
+ case str
119
+ when COMPLEX_REGEX then unit_name, quantity = parse_complex(str)
120
+ when SCIENTIFIC_REGEX then unit_name, quantity = parse_scientific(str)
121
+ when RATIONAL_REGEX then unit_name, quantity = parse_rational(str)
122
+ else raise ArgumentError, "Unable to parse: '#{str}'"
123
+ end
124
+
125
+ unit_name ||= 'count'
126
+ unit = Unit[unit_name.strip.downcase]
127
+ raise ArgumentError, "Invalid unit: '#{unit_name}'" unless unit
128
+
129
+ new(quantity, unit)
130
+ end
131
+
132
+ def define(unit_name, &block)
133
+ Unit.define(unit_name, &block)
134
+ end
135
+
136
+ private
137
+
138
+ def normalize(string)
139
+ string.dup.tap do |str|
140
+ if str =~ Regexp.new(/(#{RATIOS.keys.join('|')})/)
141
+ RATIOS.each do |search, replace|
142
+ str.gsub!(search) { " #{replace}" }
143
+ end
144
+ end
145
+
146
+ str.strip!
147
+ end
148
+ end
149
+
150
+ def parse_complex(str)
86
151
  real, imaginary, unit_name = str.scan(COMPLEX_REGEX).first
87
152
  quantity = Complex(real.to_f, imaginary.to_f).to_f
88
- elsif str =~ SCIENTIFIC_REGEX
153
+ return unit_name, quantity
154
+ end
155
+
156
+ def parse_scientific(str)
89
157
  whole, unit_name = str.scan(SCIENTIFIC_REGEX).first
90
158
  quantity = whole.to_f
91
- elsif str =~ RATIONAL_REGEX
159
+ return unit_name, quantity
160
+ end
161
+
162
+ def parse_rational(str)
92
163
  whole, _, numerator, denominator, unit_name = str.scan(RATIONAL_REGEX).first
93
-
164
+
94
165
  if numerator && denominator
95
166
  numerator = numerator.to_f + (denominator.to_f * whole.to_f)
96
167
  denominator = denominator.to_f
@@ -98,27 +169,15 @@ class Measurement
98
169
  else
99
170
  quantity = whole.to_f
100
171
  end
101
- else
102
- raise ArgumentError, "Unable to parse: '#{str}'"
172
+
173
+ return unit_name, quantity
103
174
  end
104
-
105
- unit_name ||= 'count'
106
- unit = Unit[unit_name.strip.downcase]
107
- raise ArgumentError, "Invalid unit: '#{unit_name}'" unless unit
108
-
109
- new(quantity, unit)
110
175
  end
111
-
112
- def self.define(unit_name, &block)
113
- Unit.define(unit_name, &block)
114
- end
115
-
116
- private
117
-
176
+
118
177
  define(:count) do |unit|
119
178
  unit.convert_to(:dozen) { |value| value / 12.0 }
120
179
  end
121
-
180
+
122
181
  define(:doz) do |unit|
123
182
  unit.alias :dozen
124
183
  unit.convert_to(:count) { |value| value * 12.0 }
@@ -3,79 +3,85 @@ require 'set'
3
3
  class Measurement
4
4
  class Unit
5
5
  attr_reader :name, :aliases, :conversions
6
-
6
+
7
7
  @definitions = {}
8
-
8
+
9
9
  def initialize(name)
10
10
  @name = name.to_s
11
11
  @aliases = Set.new
12
12
  @conversions = {}
13
13
  add_alias(name)
14
14
  end
15
-
15
+
16
16
  def add_alias(*args)
17
17
  args.each do |unit_alias|
18
18
  @aliases << unit_alias.to_s
19
19
  self.class[unit_alias] = self
20
20
  end
21
21
  end
22
-
22
+
23
23
  def add_conversion(unit_name, &block)
24
24
  @conversions[unit_name.to_s] = block
25
25
  end
26
-
26
+
27
27
  def conversion(unit_name)
28
28
  unit = self.class[unit_name]
29
29
  return nil unless unit
30
-
30
+
31
31
  unit.aliases.each do |unit_alias|
32
32
  conversion = @conversions[unit_alias.to_s]
33
33
  return conversion if conversion
34
34
  end
35
-
35
+
36
36
  nil
37
37
  end
38
-
38
+
39
39
  def inspect
40
40
  to_s
41
41
  end
42
-
42
+
43
43
  def to_s
44
44
  name
45
45
  end
46
-
46
+
47
47
  def ==(obj)
48
- obj.kind_of?(self.class) && hash == obj.hash
49
- end
50
-
51
- def self.define(unit_name, &block)
52
- Builder.new(unit_name, &block)
53
- end
54
-
55
- def self.[](unit_name)
56
- @definitions[unit_name.to_s.downcase]
48
+ obj.kind_of?(self.class) && name == obj.name && aliases == obj.aliases && conversions.all? do |key, proc|
49
+ [-2.5, -1, 0, 1, 2.5].all? { |n| proc.call(n) == obj.conversions[key].call(n) }
50
+ end
57
51
  end
58
-
59
- private
60
-
61
- def self.[]=(unit_name, unit)
62
- @definitions[unit_name.to_s.downcase] = unit
52
+
53
+ class << self
54
+ def define(unit_name, &block)
55
+ Builder.new(unit_name, &block)
56
+ end
57
+
58
+ def [](unit_name)
59
+ @definitions[unit_name.to_s.downcase]
60
+ end
61
+
62
+ def []=(unit_name, unit)
63
+ @definitions[unit_name.to_s.downcase] = unit
64
+ end
65
+
66
+ def names
67
+ @definitions.keys
68
+ end
63
69
  end
64
-
70
+
65
71
  class Builder
66
72
  def initialize(unit_name, &block)
67
73
  @unit = Unit.new(unit_name)
68
74
  block.call(self) if block_given?
69
75
  end
70
-
76
+
71
77
  def alias(*args)
72
78
  @unit.add_alias(*args)
73
79
  end
74
-
80
+
75
81
  def convert_to(unit_name, &block)
76
82
  @unit.add_conversion(unit_name, &block)
77
83
  end
78
-
84
+
79
85
  def to_unit
80
86
  @unit
81
87
  end