measured 1.6.0 → 2.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|