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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f7cc813b2c0df2dc458ec0d25e02aa5997c21114
4
- data.tar.gz: 751080cdbdba93968fe94d6544fbc9af58537517
3
+ metadata.gz: 430f3e67cfe5f11eaa88db5f2b3d666ca5a7d557
4
+ data.tar.gz: 7ff421483024bec50b0703850d042eae46295b8c
5
5
  SHA512:
6
- metadata.gz: 511935d3142ca49eda093620ff0db7883c217f53c7017027f6dd5c8fbf81717979ca4f15d9271bde87e25850fdf37efadf5a30359ba432f29d9fafffea6aeac4
7
- data.tar.gz: f1c99f0c23bf29ddad5982db8c6546b54702b5baf54c6b5ab214ddd5549c690814134db1283e3a534096546d7e0f7030fc0dd41081ae816bd0a83cc373c4944e
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.0
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
- Light weight and easily extensible to include other units and conversions. Conversions done with `BigDecimal` for precision.
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
- $ gem install measured
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 mathematical operations against other units, all represented internally as `BigDecimal`:
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
- Measured::Weight.new(10, :g) / Measured::Weight.new(2, :g)
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) * Measured::Weight.new(3, :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, subclass `Measured::Measurable`, define your base units, then add your conversion units.
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
- class Measured::Thing < Measured::Measurable
181
- conversion.set_base :base_unit, # Define the basic unit for the system
182
- aliases: [:bu] # Allow it to be aliased to other names/symbols
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
- conversion.add :different_unit
189
- aliases: [:du],
190
- value: [Rational(2,3), "another_unit"] # Conversion rate can be Rational, otherwise it is coerced to BigDecimal
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 and case insensitive. If you would like to create a new unit with names and aliases that are case sensitive, you should subclass `Measured::CaseSensitiveMeasurable` instead. Other than case sensitivity, both classes are identical to each other.
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
- class Measured::Thing < Measured::CaseSensitiveMeasurable
198
- conversion.set_base :base_unit,
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
- The base unit takes no value. Values for conversion units can be defined as a string with two tokens `"number unit"` or as an array with two elements. The numbers must be `Rational` or `BigDecimal`, else they will be coerced to `BigDecimal`. Conversion paths don't have to be direct as a conversion table will be built for all possible conversions using tree traversal.
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
- ```ruby
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
- * Light weight.
208
+ * Lightweight.
242
209
  * Included with ActiveShipping/ActiveUtils.
243
210
  * **Cons**
244
211
  * All math done with floats making it highly lossy.
@@ -7,31 +7,30 @@ module Measured::Arithmetic
7
7
  arithmetic_operation(other, :-)
8
8
  end
9
9
 
10
- def *(other)
11
- arithmetic_operation(other, :*)
10
+ def -@
11
+ self.class.new(-self.value, self.unit)
12
12
  end
13
13
 
14
- def /(other)
15
- arithmetic_operation(other, :/)
14
+ def scale(other)
15
+ self.class.new(self.value * other, self.unit)
16
16
  end
17
17
 
18
- def -@
19
- self.class.new(-self.value, self.unit)
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 coerce(other)
23
- [self, other]
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
- if other.is_a?(self.class)
30
- self.class.new(self.value.send(operator, other.convert_to(self.unit).value), self.unit)
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 Measured::Measurable.subclasses.map(&:to_s).include?(class_name)
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/conversion"
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
@@ -0,0 +1,11 @@
1
+ class Measured::CaseInsensitiveUnitSystem < Measured::UnitSystem
2
+ def unit?(name)
3
+ super(name.to_s.downcase)
4
+ end
5
+
6
+ protected
7
+
8
+ def unit_for(name)
9
+ unit_name_to_unit[name.to_s.downcase]
10
+ end
11
+ end
@@ -1,18 +1,15 @@
1
- class Measured::ConversionTable
1
+ module Measured::ConversionTable
2
+ extend self
2
3
 
3
- def initialize(units)
4
- @units = units
5
- end
6
-
7
- def to_h
4
+ def build(units)
8
5
  table = {}
9
6
 
10
- @units.map{|u| u.name}.each do |to_unit|
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
- @units.each do |unit|
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
- @units.each do |unit|
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: @units.map{|u| u.name }, amount: Rational(1))
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.to_r
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
@@ -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 unit.blank?
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
- if value.blank?
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.conversion.to_unit_name(new_unit)
33
- return self if new_unit_name == self.class.conversion.to_unit_name(unit)
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.conversion.convert(@value, from: @unit, to: new_unit_name)
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
- [value.to_f.to_s.gsub(/\.0\Z/, ""), unit].join(" ")
32
+ @to_s ||= "#{value_string} #{unit}"
51
33
  end
52
34
 
53
35
  def inspect
54
- "#<#{ self.class }: #{ value } #{ unit }>"
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
- other_converted = other.convert_to(unit)
60
- value <=> other_converted.value
61
- elsif other == 0
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 conversion
82
- @conversion ||= Measured::Conversion.new
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
- def units_with_aliases
94
- conversion.unit_names_with_aliases
95
- end
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{|n| n.to_s }).sort
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
- tokens[0] = BigDecimal(tokens[0]) unless tokens[0].is_a?(BigDecimal) || tokens[0].is_a?(Rational)
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