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 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