measured 1.6.0 → 2.0.0.pre1
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 +4 -4
- data/.travis.yml +4 -1
- data/README.md +27 -60
- data/lib/measured/arithmetic.rb +14 -15
- data/lib/measured/base.rb +18 -3
- data/lib/measured/case_insensitive_unit.rb +13 -0
- data/lib/measured/case_insensitive_unit_system.rb +11 -0
- data/lib/measured/conversion_table.rb +17 -20
- data/lib/measured/measurable.rb +37 -61
- data/lib/measured/unit.rb +5 -27
- data/lib/measured/unit_system.rb +67 -0
- data/lib/measured/unit_system_builder.rb +38 -0
- data/lib/measured/units/length.rb +7 -25
- data/lib/measured/units/weight.rb +5 -17
- data/lib/measured/version.rb +1 -1
- data/measured.gemspec +1 -1
- data/test/arithmetic_test.rb +22 -59
- data/test/case_insensitive_unit_system_test.rb +98 -0
- data/test/case_insensitive_unit_test.rb +79 -0
- data/test/conversion_table_test.rb +86 -7
- data/test/measurable_test.rb +47 -57
- data/test/support/fake_system.rb +15 -41
- data/test/test_helper.rb +5 -0
- data/test/unit_system_builder_test.rb +58 -0
- data/test/unit_system_test.rb +99 -0
- data/test/unit_test.rb +20 -82
- data/test/units/length_test.rb +13 -6
- data/test/units/weight_test.rb +7 -4
- metadata +18 -12
- data/lib/measured/case_sensitive_measurable.rb +0 -7
- data/lib/measured/conversion.rb +0 -95
- data/test/case_sensitive_measurable_test.rb +0 -227
- data/test/conversion_test.rb +0 -243
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 430f3e67cfe5f11eaa88db5f2b3d666ca5a7d557
|
4
|
+
data.tar.gz: 7ff421483024bec50b0703850d042eae46295b8c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c15fc5aa920d22f6db7c7edd7fb52a66eb0af84b3c3326bbec64a37a387b7a764fe69d7c6d1f2ce41e2335045cfe8af5fb4c581b76ae72507c0eec7e7e21926
|
7
|
+
data.tar.gz: da4b602746ac0d5cb06353cc75deae90bfbcf56cec313196547dec347bbc7db50ff1f50a0d679ac95fa0fbcd715a22d0b678cef943c4ff7163aba36010ed6077
|
data/.travis.yml
CHANGED
@@ -4,7 +4,8 @@ cache: bundler
|
|
4
4
|
rvm:
|
5
5
|
- 2.1.8
|
6
6
|
- 2.2.4
|
7
|
-
- 2.3.
|
7
|
+
- 2.3.1
|
8
|
+
- 2.4.0
|
8
9
|
gemfile:
|
9
10
|
- Gemfile
|
10
11
|
- gemfiles/activesupport-4.2.gemfile
|
@@ -12,3 +13,5 @@ matrix:
|
|
12
13
|
exclude:
|
13
14
|
- gemfile: Gemfile
|
14
15
|
rvm: 2.1.8
|
16
|
+
- gemfile: gemfiles/activesupport-4.2.gemfile
|
17
|
+
rvm: 2.4.0
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Encapsulates measurements with their units. Provides easy conversion between units.
|
4
4
|
|
5
|
-
|
5
|
+
Lightweight and easily extensible to include other units and conversions. Conversions done with `Rational` for precision.
|
6
6
|
|
7
7
|
The adapter to integrate `measured` with Ruby on Rails is in a separate [`measured-rails`](https://github.com/Shopify/measured-rails) gem.
|
8
8
|
|
@@ -16,7 +16,9 @@ gem 'measured'
|
|
16
16
|
|
17
17
|
Or stand alone:
|
18
18
|
|
19
|
-
|
19
|
+
```
|
20
|
+
$ gem install measured
|
21
|
+
```
|
20
22
|
|
21
23
|
## Usage
|
22
24
|
|
@@ -32,12 +34,6 @@ Convert to return a new measurement:
|
|
32
34
|
Measured::Weight.new("12", "g").convert_to("kg")
|
33
35
|
```
|
34
36
|
|
35
|
-
Or convert inline:
|
36
|
-
|
37
|
-
```ruby
|
38
|
-
Measured::Weight.new("12", "g").convert_to!("kg")
|
39
|
-
```
|
40
|
-
|
41
37
|
Agnostic to symbols/strings:
|
42
38
|
|
43
39
|
```ruby
|
@@ -50,18 +46,6 @@ Seamlessly handles aliases:
|
|
50
46
|
Measured::Weight.new(12, :oz) == Measured::Weight.new("12", :ounce)
|
51
47
|
```
|
52
48
|
|
53
|
-
Comparison with zero works without the need to specify units, useful for validations:
|
54
|
-
```ruby
|
55
|
-
Measured::Weight.new(0.001, :kg) > 0
|
56
|
-
> true
|
57
|
-
|
58
|
-
Measured::Length.new(-1, :m) < 0
|
59
|
-
> true
|
60
|
-
|
61
|
-
Measured::Weight.new(0, :oz) == 0
|
62
|
-
> true
|
63
|
-
```
|
64
|
-
|
65
49
|
Raises on unknown units:
|
66
50
|
|
67
51
|
```ruby
|
@@ -72,16 +56,21 @@ rescue Measured::UnitError
|
|
72
56
|
end
|
73
57
|
```
|
74
58
|
|
75
|
-
Perform
|
59
|
+
Perform addition / subtraction against other units, all represented internally as `Rational` or `BigDecimal`:
|
76
60
|
|
77
61
|
```ruby
|
78
62
|
Measured::Weight.new(1, :g) + Measured::Weight.new(2, :g)
|
79
63
|
> #<Measured::Weight 3 g>
|
80
|
-
Measured::Weight.new(2, :g) - Measured::Weight.new(1, :g)
|
81
|
-
> #<Measured::Weight 1 g>
|
82
|
-
|
64
|
+
Measured::Weight.new("2.1", :g) - Measured::Weight.new(1, :g)
|
65
|
+
> #<Measured::Weight 1.1 g>
|
66
|
+
```
|
67
|
+
|
68
|
+
Multiplication and division by units is not supported, but the actual value can be scaled by a scalar:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
Measured::Weight.new(10, :g).scale(0.5)
|
83
72
|
> #<Measured::Weight 5 g>
|
84
|
-
Measured::Weight.new(2, :g)
|
73
|
+
Measured::Weight.new(2, :g).scale(3)
|
85
74
|
> #<Measured::Weight 6 g>
|
86
75
|
```
|
87
76
|
|
@@ -92,13 +81,6 @@ Measured::Weight.new(1000, :g) + Measured::Weight.new(1, :kg)
|
|
92
81
|
> #<Measured::Weight 2000 g>
|
93
82
|
```
|
94
83
|
|
95
|
-
Also perform mathematical operations against `Numeric` things:
|
96
|
-
|
97
|
-
```ruby
|
98
|
-
Measured::Weight.new(3, :g) * 2
|
99
|
-
> #<Measured::Weight 6 g>
|
100
|
-
```
|
101
|
-
|
102
84
|
Converts units only as needed for equality comparison:
|
103
85
|
|
104
86
|
```ruby
|
@@ -174,45 +156,30 @@ Measured::Weight(1, :g)
|
|
174
156
|
|
175
157
|
### Adding new units
|
176
158
|
|
177
|
-
Extending this library to support other units is simple. To add a new conversion,
|
159
|
+
Extending this library to support other units is simple. To add a new conversion, use `Measured.build` to define your base unit and conversion units:
|
178
160
|
|
179
161
|
```ruby
|
180
|
-
|
181
|
-
|
182
|
-
aliases: [:bu]
|
183
|
-
|
184
|
-
conversion.add :another_unit, # Add a second unit to the system
|
185
|
-
aliases: [:au], # All units allow aliases, as long as they are unique
|
186
|
-
value: ["1.5 base_unit"] # The conversion rate to another unit
|
162
|
+
Measured::Thing = Measured.build do
|
163
|
+
unit :base_unit, # Add a unit to the system
|
164
|
+
aliases: [:bu] # Allow it to be aliased to other names/symbols
|
187
165
|
|
188
|
-
|
189
|
-
aliases: [:
|
190
|
-
value: [
|
166
|
+
unit :another_unit, # Add a second unit to the system
|
167
|
+
aliases: [:au], # All units allow aliases, as long as they are unique
|
168
|
+
value: ["1.5 bu"] # The conversion rate to another unit
|
191
169
|
end
|
192
170
|
```
|
193
171
|
|
194
|
-
By default all names and aliases
|
172
|
+
By default all names and aliases are case insensitive. If you would like to create a new unit with names and aliases that are case sensitive, specify the case sensitive flag when building your unit:
|
195
173
|
|
196
174
|
```ruby
|
197
|
-
|
198
|
-
|
199
|
-
aliases: [:bu]
|
175
|
+
Measured::Thing = Measured.build(case_sensitive: true) do
|
176
|
+
unit :base_unit, aliases: [:bu]
|
200
177
|
end
|
201
178
|
```
|
202
179
|
|
203
|
-
|
204
|
-
|
205
|
-
The `case_sensitive` flag, which is false by default, gets taken into account any time you attempt to reference a unit by name or alias.
|
206
|
-
|
207
|
-
You can also open up the existing classes and add a new conversion:
|
180
|
+
Other than case sensitivity, both classes are identical to each other. The `case_sensitive` flag, which is false by default, gets taken into account any time you attempt to reference a unit by name or alias.
|
208
181
|
|
209
|
-
|
210
|
-
class Measured::Length
|
211
|
-
conversion.add :dm,
|
212
|
-
aliases: [:decimeter, :decimetre, :decimeters, :decimetres],
|
213
|
-
value: "0.1 m"
|
214
|
-
end
|
215
|
-
```
|
182
|
+
Values for conversion units can be defined as a string with two tokens `"number unit"` or as an array with two elements. All values will be parsed as / coerced to `Rational`. Conversion paths don't have to be direct as a conversion table will be built for all possible conversions.
|
216
183
|
|
217
184
|
### Namespaces
|
218
185
|
|
@@ -238,7 +205,7 @@ Existing alternatives which were considered:
|
|
238
205
|
|
239
206
|
### Gem: [quantified](https://github.com/Shopify/quantified)
|
240
207
|
* **Pros**
|
241
|
-
*
|
208
|
+
* Lightweight.
|
242
209
|
* Included with ActiveShipping/ActiveUtils.
|
243
210
|
* **Cons**
|
244
211
|
* All math done with floats making it highly lossy.
|
data/lib/measured/arithmetic.rb
CHANGED
@@ -7,31 +7,30 @@ module Measured::Arithmetic
|
|
7
7
|
arithmetic_operation(other, :-)
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
10
|
+
def -@
|
11
|
+
self.class.new(-self.value, self.unit)
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
|
14
|
+
def scale(other)
|
15
|
+
self.class.new(self.value * other, self.unit)
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
19
|
-
|
18
|
+
def coerce(other)
|
19
|
+
if other.is_a?(self.class)
|
20
|
+
[other, self]
|
21
|
+
else
|
22
|
+
raise TypeError, "Cannot coerce #{other.class} to #{self.class}"
|
23
|
+
end
|
20
24
|
end
|
21
25
|
|
22
|
-
def
|
23
|
-
|
26
|
+
def to_i
|
27
|
+
raise TypeError, "#{self.class} cannot be converted to an integer"
|
24
28
|
end
|
25
29
|
|
26
30
|
private
|
27
31
|
|
28
32
|
def arithmetic_operation(other, operator)
|
29
|
-
|
30
|
-
|
31
|
-
elsif other.is_a?(Numeric)
|
32
|
-
self.class.new(self.value.send(operator, other), self.unit)
|
33
|
-
else
|
34
|
-
raise TypeError, "Invalid operation #{ operator } between #{ self.class } to #{ other.class }"
|
35
|
-
end
|
33
|
+
other, _ = coerce(other)
|
34
|
+
self.class.new(self.value.public_send(operator, other.convert_to(self.unit).value), self.unit)
|
36
35
|
end
|
37
36
|
end
|
data/lib/measured/base.rb
CHANGED
@@ -6,10 +6,23 @@ module Measured
|
|
6
6
|
class UnitError < StandardError ; end
|
7
7
|
|
8
8
|
class << self
|
9
|
+
def build(**kwargs, &block)
|
10
|
+
builder = UnitSystemBuilder.new(**kwargs)
|
11
|
+
builder.instance_eval(&block)
|
12
|
+
|
13
|
+
Class.new(Measurable) do
|
14
|
+
class << self
|
15
|
+
attr_reader :unit_system
|
16
|
+
end
|
17
|
+
|
18
|
+
@unit_system = builder.build
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
9
22
|
def method_missing(method, *args)
|
10
23
|
class_name = "Measured::#{ method }"
|
11
24
|
|
12
|
-
if
|
25
|
+
if Measurable.subclasses.map(&:to_s).include?(class_name)
|
13
26
|
klass = class_name.constantize
|
14
27
|
|
15
28
|
Measured.define_singleton_method(method) do |value, unit|
|
@@ -26,7 +39,9 @@ end
|
|
26
39
|
|
27
40
|
require "measured/arithmetic"
|
28
41
|
require "measured/unit"
|
29
|
-
require "measured/
|
42
|
+
require "measured/unit_system"
|
43
|
+
require "measured/case_insensitive_unit"
|
44
|
+
require "measured/case_insensitive_unit_system"
|
45
|
+
require "measured/unit_system_builder"
|
30
46
|
require "measured/conversion_table"
|
31
47
|
require "measured/measurable"
|
32
|
-
require "measured/case_sensitive_measurable"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Measured::CaseInsensitiveUnit < Measured::Unit
|
2
|
+
def initialize(name, aliases: [], value: nil)
|
3
|
+
super(name.to_s.downcase, aliases: aliases.map(&:to_s).map!(&:downcase), value: value)
|
4
|
+
end
|
5
|
+
|
6
|
+
def name_eql?(name_to_compare)
|
7
|
+
super(name_to_compare.to_s.downcase)
|
8
|
+
end
|
9
|
+
|
10
|
+
def names_include?(name_to_compare)
|
11
|
+
super(name_to_compare.to_s.downcase)
|
12
|
+
end
|
13
|
+
end
|
@@ -1,18 +1,15 @@
|
|
1
|
-
|
1
|
+
module Measured::ConversionTable
|
2
|
+
extend self
|
2
3
|
|
3
|
-
def
|
4
|
-
@units = units
|
5
|
-
end
|
6
|
-
|
7
|
-
def to_h
|
4
|
+
def build(units)
|
8
5
|
table = {}
|
9
6
|
|
10
|
-
|
7
|
+
units.map{|u| u.name}.each do |to_unit|
|
11
8
|
to_table = {to_unit => BigDecimal("1")}
|
12
9
|
|
13
10
|
table.each do |from_unit, from_table|
|
14
|
-
to_table[from_unit] = find_conversion(to: from_unit, from: to_unit)
|
15
|
-
from_table[to_unit] = find_conversion(to: to_unit, from: from_unit)
|
11
|
+
to_table[from_unit] = find_conversion(units, to: from_unit, from: to_unit)
|
12
|
+
from_table[to_unit] = find_conversion(units, to: to_unit, from: from_unit)
|
16
13
|
end
|
17
14
|
|
18
15
|
table[to_unit] = to_table
|
@@ -23,40 +20,40 @@ class Measured::ConversionTable
|
|
23
20
|
|
24
21
|
private
|
25
22
|
|
26
|
-
def find_conversion(to:, from:)
|
27
|
-
conversion = find_direct_conversion(to: to, from: from) || find_tree_traversal_conversion(to: to, from: from)
|
23
|
+
def find_conversion(units, to:, from:)
|
24
|
+
conversion = find_direct_conversion(units, to: to, from: from) || find_tree_traversal_conversion(units, to: to, from: from)
|
28
25
|
|
29
26
|
raise Measured::UnitError, "Cannot find conversion path from #{ from } to #{ to }." unless conversion
|
30
27
|
|
31
28
|
conversion
|
32
29
|
end
|
33
30
|
|
34
|
-
def find_direct_conversion(to:, from:)
|
35
|
-
|
31
|
+
def find_direct_conversion(units, to:, from:)
|
32
|
+
units.each do |unit|
|
36
33
|
return unit.conversion_amount if unit.name == from && unit.conversion_unit == to
|
37
34
|
end
|
38
35
|
|
39
|
-
|
36
|
+
units.each do |unit|
|
40
37
|
return unit.inverse_conversion_amount if unit.name == to && unit.conversion_unit == from
|
41
38
|
end
|
42
39
|
|
43
40
|
nil
|
44
41
|
end
|
45
42
|
|
46
|
-
def find_tree_traversal_conversion(to:, from:)
|
47
|
-
traverse(from: from, to: to, unit_names:
|
43
|
+
def find_tree_traversal_conversion(units, to:, from:)
|
44
|
+
traverse(units, from: from, to: to, unit_names: units.map(&:name), amount: 1)
|
48
45
|
end
|
49
46
|
|
50
|
-
def traverse(from:, to:, unit_names:, amount:)
|
47
|
+
def traverse(units, from:, to:, unit_names:, amount:)
|
51
48
|
unit_names = unit_names - [from]
|
52
49
|
|
53
50
|
unit_names.each do |name|
|
54
|
-
if conversion = find_direct_conversion(from: from, to: name)
|
55
|
-
new_amount = amount * conversion
|
51
|
+
if conversion = find_direct_conversion(units, from: from, to: name)
|
52
|
+
new_amount = amount * conversion
|
56
53
|
if name == to
|
57
54
|
return new_amount
|
58
55
|
else
|
59
|
-
result = traverse(from: name, to: to, unit_names: unit_names, amount: new_amount)
|
56
|
+
result = traverse(units, from: name, to: to, unit_names: unit_names, amount: new_amount)
|
60
57
|
return result if result
|
61
58
|
end
|
62
59
|
end
|
data/lib/measured/measurable.rb
CHANGED
@@ -1,102 +1,78 @@
|
|
1
|
-
class Measured::Measurable
|
2
|
-
include Comparable
|
1
|
+
class Measured::Measurable < Numeric
|
3
2
|
include Measured::Arithmetic
|
4
3
|
|
5
4
|
attr_reader :unit, :value
|
6
5
|
|
7
|
-
delegate :zero?, to: :value
|
8
|
-
|
9
6
|
def initialize(value, unit)
|
10
|
-
raise Measured::UnitError, "Unit cannot be blank" if
|
11
|
-
raise Measured::UnitError, "Unit #{ unit } does not exist" unless self.class.conversion.unit_or_alias?(unit)
|
7
|
+
raise Measured::UnitError, "Unit value cannot be blank" if value.blank?
|
12
8
|
|
9
|
+
@unit = self.class.unit_system.to_unit_name!(unit)
|
13
10
|
@value = case value
|
14
|
-
when NilClass
|
15
|
-
raise Measured::UnitError, "Unit value cannot be nil"
|
16
11
|
when Float
|
17
|
-
BigDecimal(value, Float::DIG+1)
|
18
|
-
when BigDecimal
|
12
|
+
BigDecimal(value, Float::DIG + 1)
|
13
|
+
when BigDecimal, Rational
|
19
14
|
value
|
15
|
+
when Integer
|
16
|
+
Rational(value)
|
20
17
|
else
|
21
|
-
|
22
|
-
raise Measured::UnitError, "Unit value cannot be blank"
|
23
|
-
else
|
24
|
-
BigDecimal(value)
|
25
|
-
end
|
18
|
+
BigDecimal(value)
|
26
19
|
end
|
27
|
-
|
28
|
-
@unit = self.class.conversion.to_unit_name(unit)
|
29
20
|
end
|
30
21
|
|
31
22
|
def convert_to(new_unit)
|
32
|
-
new_unit_name = self.class.
|
33
|
-
return self if new_unit_name ==
|
23
|
+
new_unit_name = self.class.unit_system.to_unit_name(new_unit)
|
24
|
+
return self if new_unit_name == @unit
|
34
25
|
|
35
|
-
value = self.class.
|
26
|
+
value = self.class.unit_system.convert(@value, from: @unit, to: new_unit_name)
|
36
27
|
|
37
28
|
self.class.new(value, new_unit)
|
38
29
|
end
|
39
30
|
|
40
|
-
def convert_to!(new_unit)
|
41
|
-
converted = convert_to(new_unit)
|
42
|
-
|
43
|
-
@value = converted.value
|
44
|
-
@unit = converted.unit
|
45
|
-
|
46
|
-
self
|
47
|
-
end
|
48
|
-
|
49
31
|
def to_s
|
50
|
-
|
32
|
+
@to_s ||= "#{value_string} #{unit}"
|
51
33
|
end
|
52
34
|
|
53
35
|
def inspect
|
54
|
-
"#<#{
|
36
|
+
@inspect ||= "#<#{self.class}: #{value_string} #{unit}>"
|
55
37
|
end
|
56
38
|
|
57
39
|
def <=>(other)
|
58
40
|
if other.is_a?(self.class)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
other_converted = self.class.new(0, unit)
|
63
|
-
value <=> other_converted.value
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def ==(other)
|
68
|
-
if other.is_a?(self.class)
|
69
|
-
other_converted = other.convert_to(unit)
|
70
|
-
value == other_converted.value
|
71
|
-
elsif other == 0
|
72
|
-
other_converted = self.class.new(0, unit)
|
73
|
-
value == other_converted.value
|
41
|
+
value <=> other.convert_to(unit).value
|
42
|
+
else
|
43
|
+
nil
|
74
44
|
end
|
75
45
|
end
|
76
46
|
|
77
|
-
alias_method :eql?, :==
|
78
|
-
|
79
47
|
class << self
|
48
|
+
extend Forwardable
|
80
49
|
|
81
|
-
def
|
82
|
-
|
83
|
-
end
|
84
|
-
|
85
|
-
def units
|
86
|
-
conversion.unit_names
|
87
|
-
end
|
88
|
-
|
89
|
-
def valid_unit?(unit)
|
90
|
-
conversion.unit_or_alias?(unit)
|
50
|
+
def unit_system
|
51
|
+
raise "`Measurable` does not have a `unit_system` object. You cannot directly subclass `Measurable`. Instead, build a new unit system by calling `Measured.build`."
|
91
52
|
end
|
92
53
|
|
93
|
-
|
94
|
-
|
95
|
-
|
54
|
+
delegate unit_names: :unit_system
|
55
|
+
delegate unit_names_with_aliases: :unit_system
|
56
|
+
delegate unit_or_alias?: :unit_system
|
96
57
|
|
97
58
|
def name
|
98
59
|
to_s.split("::").last.underscore.humanize.downcase
|
99
60
|
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
100
64
|
|
65
|
+
def value_string
|
66
|
+
@value_string ||= begin
|
67
|
+
str = case value
|
68
|
+
when Rational
|
69
|
+
value.denominator == 1 ? value.numerator.to_s : value.to_f.to_s
|
70
|
+
when BigDecimal
|
71
|
+
value.to_s("F")
|
72
|
+
else
|
73
|
+
value.to_f.to_s
|
74
|
+
end
|
75
|
+
str.gsub(/\.0*\Z/, "")
|
76
|
+
end
|
101
77
|
end
|
102
78
|
end
|
data/lib/measured/unit.rb
CHANGED
@@ -1,31 +1,14 @@
|
|
1
1
|
class Measured::Unit
|
2
2
|
include Comparable
|
3
3
|
|
4
|
+
attr_reader :name, :names, :conversion_amount, :conversion_unit
|
5
|
+
|
4
6
|
def initialize(name, aliases: [], value: nil)
|
5
7
|
@name = name.to_s
|
6
|
-
@names = ([@name] + aliases.map
|
7
|
-
|
8
|
+
@names = ([@name] + aliases.map(&:to_s)).sort
|
8
9
|
@conversion_amount, @conversion_unit = parse_value(value) if value
|
9
10
|
end
|
10
11
|
|
11
|
-
attr_reader :name, :names, :conversion_amount, :conversion_unit
|
12
|
-
|
13
|
-
def name_eql?(name_to_compare, case_sensitive: false)
|
14
|
-
return false unless name_to_compare.present?
|
15
|
-
name_to_compare = name_to_compare.to_s
|
16
|
-
case_sensitive ? @name.eql?(name_to_compare) : case_insensitive(@name).include?(name_to_compare.downcase)
|
17
|
-
end
|
18
|
-
|
19
|
-
def names_include?(name_to_compare, case_sensitive: false)
|
20
|
-
return false unless name_to_compare.present?
|
21
|
-
name_to_compare = name_to_compare.to_s
|
22
|
-
case_sensitive ? @names.include?(name_to_compare) : case_insensitive(@names).include?(name_to_compare.downcase)
|
23
|
-
end
|
24
|
-
|
25
|
-
def add_alias(aliases)
|
26
|
-
@names = (@names << aliases).flatten.sort unless aliases.nil? || aliases.empty?
|
27
|
-
end
|
28
|
-
|
29
12
|
def to_s
|
30
13
|
if conversion_string
|
31
14
|
"#{ @name } (#{ conversion_string })"
|
@@ -56,20 +39,15 @@ class Measured::Unit
|
|
56
39
|
|
57
40
|
private
|
58
41
|
|
59
|
-
def case_insensitive(comparison)
|
60
|
-
[comparison].flatten.map(&:downcase)
|
61
|
-
end
|
62
|
-
|
63
42
|
def conversion_string
|
64
43
|
"#{ conversion_amount } #{ conversion_unit }" if @conversion_amount || @conversion_unit
|
65
44
|
end
|
66
45
|
|
67
46
|
def parse_value(tokens)
|
68
47
|
tokens = tokens.split(" ") if tokens.is_a?(String)
|
69
|
-
raise Measured::UnitError, "Cannot parse 'number unit' or [number, unit] formatted tokens from #{ tokens }." unless tokens.size == 2
|
70
48
|
|
71
|
-
|
49
|
+
raise Measured::UnitError, "Cannot parse 'number unit' or [number, unit] formatted tokens from #{tokens}." unless tokens.size == 2
|
72
50
|
|
73
|
-
tokens
|
51
|
+
[tokens[0].to_r, tokens[1]]
|
74
52
|
end
|
75
53
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class Measured::UnitSystem
|
2
|
+
attr_reader :units
|
3
|
+
|
4
|
+
def initialize(units)
|
5
|
+
@units = units.dup
|
6
|
+
end
|
7
|
+
|
8
|
+
def unit_names_with_aliases
|
9
|
+
@unit_names_with_aliases ||= @units.flat_map(&:names).sort
|
10
|
+
end
|
11
|
+
|
12
|
+
def unit_names
|
13
|
+
@unit_names ||= @units.map(&:name).sort
|
14
|
+
end
|
15
|
+
|
16
|
+
def unit_or_alias?(name)
|
17
|
+
!!unit_for(name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def unit?(name)
|
21
|
+
unit = unit_for(name)
|
22
|
+
unit ? unit.name == name.to_s : false
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_unit_name(name)
|
26
|
+
to_unit_name!(name)
|
27
|
+
rescue Measured::UnitError
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_unit_name!(name)
|
32
|
+
unit_for!(name).name
|
33
|
+
end
|
34
|
+
|
35
|
+
def convert(value, from:, to:)
|
36
|
+
from_unit = unit_for!(from)
|
37
|
+
to_unit = unit_for!(to)
|
38
|
+
conversion = conversion_table[from][to]
|
39
|
+
|
40
|
+
raise Measured::UnitError, "Cannot find conversion entry from #{from} to #{to}" unless conversion
|
41
|
+
|
42
|
+
value.to_r * conversion
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def conversion_table
|
48
|
+
@conversion_table ||= Measured::ConversionTable.build(@units)
|
49
|
+
end
|
50
|
+
|
51
|
+
def unit_name_to_unit
|
52
|
+
@unit_name_to_unit ||= @units.inject({}) do |hash, unit|
|
53
|
+
unit.names.each { |name| hash[name.to_s] = unit }
|
54
|
+
hash
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def unit_for(name)
|
59
|
+
unit_name_to_unit[name.to_s]
|
60
|
+
end
|
61
|
+
|
62
|
+
def unit_for!(name)
|
63
|
+
unit = unit_for(name)
|
64
|
+
raise Measured::UnitError, "Unit '#{name}' does not exist" unless unit
|
65
|
+
unit
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Measured::UnitSystemBuilder
|
2
|
+
def initialize(case_sensitive: false)
|
3
|
+
@units = []
|
4
|
+
@case_sensitive = case_sensitive
|
5
|
+
end
|
6
|
+
|
7
|
+
def unit(unit_name, aliases: [], value: nil)
|
8
|
+
@units << build_unit(unit_name, aliases: aliases, value: value)
|
9
|
+
nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def build
|
13
|
+
unit_system_class.new(@units)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def build_unit(name, aliases: [], value: nil)
|
19
|
+
unit = unit_class.new(name, aliases: aliases, value: value)
|
20
|
+
check_for_duplicate_unit_names!(unit)
|
21
|
+
unit
|
22
|
+
end
|
23
|
+
|
24
|
+
def unit_class
|
25
|
+
@case_sensitive ? Measured::Unit : Measured::CaseInsensitiveUnit
|
26
|
+
end
|
27
|
+
|
28
|
+
def unit_system_class
|
29
|
+
@case_sensitive ? Measured::UnitSystem : Measured::CaseInsensitiveUnitSystem
|
30
|
+
end
|
31
|
+
|
32
|
+
def check_for_duplicate_unit_names!(unit)
|
33
|
+
names = @units.flat_map(&:names)
|
34
|
+
if names.any? { |name| unit.names.include?(name) }
|
35
|
+
raise Measured::UnitError, "Unit #{unit.name} has already been added."
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|