measured 2.0.0 → 2.8.2

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.
Files changed (57) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +31 -0
  3. data/CHANGELOG.md +60 -0
  4. data/README.md +68 -10
  5. data/Rakefile +22 -1
  6. data/cache/.keep +0 -0
  7. data/cache/length.json +2553 -0
  8. data/cache/volume.json +4695 -0
  9. data/cache/weight.json +3195 -0
  10. data/dev.yml +2 -6
  11. data/gemfiles/{activesupport-4.2.gemfile → activesupport-5.2.gemfile} +1 -1
  12. data/gemfiles/activesupport-6.0.gemfile +5 -0
  13. data/gemfiles/activesupport-6.1.gemfile +5 -0
  14. data/lib/measured/arithmetic.rb +10 -0
  15. data/lib/measured/base.rb +13 -6
  16. data/lib/measured/cache/json.rb +55 -0
  17. data/lib/measured/cache/json_writer.rb +9 -0
  18. data/lib/measured/cache/null.rb +16 -0
  19. data/lib/measured/conversion_table_builder.rb +110 -0
  20. data/lib/measured/cycle_detected.rb +11 -0
  21. data/lib/measured/measurable.rb +28 -21
  22. data/lib/measured/missing_conversion_path.rb +12 -0
  23. data/lib/measured/parser.rb +2 -1
  24. data/lib/measured/unit.rb +17 -27
  25. data/lib/measured/unit_already_added.rb +11 -0
  26. data/lib/measured/unit_error.rb +4 -0
  27. data/lib/measured/unit_system.rb +26 -21
  28. data/lib/measured/unit_system_builder.rb +46 -2
  29. data/lib/measured/units/length.rb +6 -3
  30. data/lib/measured/units/volume.rb +20 -0
  31. data/lib/measured/units/weight.rb +10 -2
  32. data/lib/measured/version.rb +2 -1
  33. data/lib/measured.rb +2 -0
  34. data/measured.gemspec +13 -5
  35. data/test/arithmetic_test.rb +36 -0
  36. data/test/cache/json_test.rb +43 -0
  37. data/test/cache/json_writer_test.rb +23 -0
  38. data/test/cache/null_test.rb +20 -0
  39. data/test/cache_consistency_test.rb +19 -0
  40. data/test/conversion_table_builder_test.rb +147 -0
  41. data/test/measurable_test.rb +28 -13
  42. data/test/parser_test.rb +1 -0
  43. data/test/support/always_true_cache.rb +14 -0
  44. data/test/support/fake_system.rb +1 -0
  45. data/test/support/subclasses.rb +7 -0
  46. data/test/test_helper.rb +5 -3
  47. data/test/unit_error_test.rb +1 -0
  48. data/test/unit_system_builder_test.rb +120 -2
  49. data/test/unit_system_test.rb +15 -0
  50. data/test/unit_test.rb +14 -5
  51. data/test/units/length_test.rb +123 -10
  52. data/test/units/volume_test.rb +497 -0
  53. data/test/units/weight_test.rb +290 -5
  54. metadata +49 -18
  55. data/.travis.yml +0 -17
  56. data/lib/measured/conversion_table.rb +0 -65
  57. data/test/conversion_table_test.rb +0 -98
data/dev.yml CHANGED
@@ -2,7 +2,7 @@ name: measured
2
2
 
3
3
  up:
4
4
  - ruby:
5
- version: 2.3.1
5
+ version: 3.0.2
6
6
  - bundler
7
7
 
8
8
  commands:
@@ -14,9 +14,5 @@ commands:
14
14
  if [[ $# -eq 0 ]]; then
15
15
  bundle exec rake test
16
16
  else
17
- bundle exec ruby -Itest "$@"
17
+ bundle exec rake test "$@"
18
18
  fi
19
-
20
- packages:
21
- - git@github.com:Shopify/dev-shopify.git
22
-
@@ -2,4 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec path: '..'
4
4
 
5
- gem 'activesupport', '~> 4.2'
5
+ gem 'activesupport', '~> 5.2'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem 'activesupport', '~> 6.0'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem 'activesupport', '~> 6.1'
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  module Measured::Arithmetic
3
+ extend Forwardable
4
+ def_delegators :@value, :zero?, :positive?, :negative?, :finite?, :infinite?
5
+
2
6
  def +(other)
3
7
  arithmetic_operation(other, :+)
4
8
  end
@@ -18,6 +22,8 @@ module Measured::Arithmetic
18
22
  def coerce(other)
19
23
  if other.is_a?(self.class)
20
24
  [other, self]
25
+ elsif other.is_a?(Numeric) && other.zero?
26
+ [self.class.new(other, self.unit), self]
21
27
  else
22
28
  raise TypeError, "Cannot coerce #{other.class} to #{self.class}"
23
29
  end
@@ -27,6 +33,10 @@ module Measured::Arithmetic
27
33
  raise TypeError, "#{self.class} cannot be converted to an integer"
28
34
  end
29
35
 
36
+ def nonzero?
37
+ value.nonzero? ? self : false
38
+ end
39
+
30
40
  private
31
41
 
32
42
  def arithmetic_operation(other, operator)
data/lib/measured/base.rb CHANGED
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
2
+ require "forwardable"
1
3
  require "measured/version"
2
4
  require "active_support/all"
3
5
  require "bigdecimal"
6
+ require "json"
4
7
 
5
8
  module Measured
6
- class UnitError < StandardError ; end
7
-
8
9
  class << self
9
10
  def build(&block)
10
11
  builder = UnitSystemBuilder.new
@@ -21,10 +22,9 @@ module Measured
21
22
 
22
23
  def method_missing(method, *args)
23
24
  class_name = "Measured::#{ method }"
25
+ klass = class_name.safe_constantize
24
26
 
25
- if Measurable.subclasses.map(&:to_s).include?(class_name)
26
- klass = class_name.constantize
27
-
27
+ if klass && klass < Measurable
28
28
  Measured.define_singleton_method(method) do |value, unit|
29
29
  klass.new(value, unit)
30
30
  end
@@ -37,10 +37,17 @@ module Measured
37
37
  end
38
38
  end
39
39
 
40
+ require "measured/unit_error"
41
+ require "measured/cycle_detected"
42
+ require "measured/unit_already_added"
43
+ require "measured/missing_conversion_path"
40
44
  require "measured/arithmetic"
41
45
  require "measured/parser"
42
46
  require "measured/unit"
43
47
  require "measured/unit_system"
44
48
  require "measured/unit_system_builder"
45
- require "measured/conversion_table"
49
+ require "measured/conversion_table_builder"
50
+ require "measured/cache/null"
51
+ require "measured/cache/json_writer"
52
+ require "measured/cache/json"
46
53
  require "measured/measurable"
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ module Measured::Cache
3
+ class Json
4
+ attr_reader :filename, :path
5
+
6
+ def initialize(filename)
7
+ @filename = filename
8
+ @path = Pathname.new(File.join(File.dirname(__FILE__), "../../../cache", @filename)).cleanpath
9
+ end
10
+
11
+ def exist?
12
+ File.exist?(@path)
13
+ end
14
+
15
+ def read
16
+ return unless exist?
17
+ decode(JSON.load(File.read(@path), nil, freeze: true))
18
+ end
19
+
20
+ def write(table)
21
+ raise ArgumentError, "Cannot overwrite file cache at runtime."
22
+ end
23
+
24
+ private
25
+
26
+ # JSON dump and load of Rational objects exists, but it changes the behaviour of JSON globally if required.
27
+ # Instead, the same marshalling technique is rewritten here to prevent changing this behaviour project wide.
28
+ # https://github.com/ruby/ruby/blob/trunk/ext/json/lib/json/add/rational.rb
29
+ def encode(table)
30
+ table.each_with_object(table.dup) do |(k1, v1), accu|
31
+ v1.each do |k2, v2|
32
+ if v2.is_a?(Rational)
33
+ accu[k1][k2] = { "numerator" => v2.numerator, "denominator" => v2.denominator }
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def decode(table)
40
+ table.transform_values do |value1|
41
+ if value1.is_a?(Hash)
42
+ value1.transform_values do |value2|
43
+ if value2.is_a?(Hash)
44
+ Rational(value2["numerator"], value2["denominator"])
45
+ else
46
+ value2
47
+ end
48
+ end
49
+ else
50
+ value1
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ module Measured::Cache::JsonWriter
3
+ def write(table)
4
+ File.open(@path, "w") do |f|
5
+ f.write("// Do not modify this file directly. Regenerate it with 'rake cache:write'.\n")
6
+ f.write(JSON.pretty_generate(encode(table)))
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module Measured::Cache
3
+ class Null
4
+ def exist?
5
+ false
6
+ end
7
+
8
+ def read
9
+ nil
10
+ end
11
+
12
+ def write(table)
13
+ nil
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+ class Measured::ConversionTableBuilder
3
+ attr_reader :units
4
+
5
+ def initialize(units, cache: nil)
6
+ @units = units
7
+ cache ||= { class: Measured::Cache::Null }
8
+ @cache = cache[:class].new(*cache[:args])
9
+ end
10
+
11
+ def to_h
12
+ return @cache.read if cached?
13
+ generate_table
14
+ end
15
+
16
+ def update_cache
17
+ @cache.write(generate_table)
18
+ end
19
+
20
+ def cached?
21
+ @cache.exist?
22
+ end
23
+
24
+ private
25
+
26
+ def generate_table
27
+ validate_no_cycles
28
+
29
+ units.map(&:name).each_with_object({}) do |to_unit, table|
30
+ to_table = {to_unit => Rational(1, 1)}
31
+
32
+ table.each do |from_unit, from_table|
33
+ conversion = find_conversion(to: from_unit, from: to_unit)
34
+ to_table[from_unit] = conversion
35
+ from_table[to_unit] = 1 / conversion
36
+ end
37
+
38
+ table[to_unit] = to_table
39
+ end
40
+ end
41
+
42
+ def validate_no_cycles
43
+ graph = units.select { |unit| unit.conversion_unit.present? }.group_by { |unit| unit.name }
44
+ validate_acyclic_graph(graph, from: graph.keys[0])
45
+ end
46
+
47
+ # This uses a depth-first search algorithm: https://en.wikipedia.org/wiki/Depth-first_search
48
+ def validate_acyclic_graph(graph, from:, visited: [])
49
+ graph[from]&.each do |edge|
50
+ adjacent_node = edge.conversion_unit
51
+ if visited.include?(adjacent_node)
52
+ raise Measured::CycleDetected.new(edge)
53
+ else
54
+ validate_acyclic_graph(graph, from: adjacent_node, visited: visited + [adjacent_node])
55
+ end
56
+ end
57
+ end
58
+
59
+ def find_conversion(to:, from:)
60
+ conversion = find_direct_conversion_cached(to: to, from: from) || find_tree_traversal_conversion(to: to, from: from)
61
+
62
+ raise Measured::MissingConversionPath.new(from, to) unless conversion
63
+
64
+ conversion
65
+ end
66
+
67
+ def find_direct_conversion_cached(to:, from:)
68
+ @direct_conversion_cache ||= {}
69
+ @direct_conversion_cache[to] ||= {}
70
+
71
+ if @direct_conversion_cache[to].key?(from)
72
+ @direct_conversion_cache[to][from]
73
+ else
74
+ @direct_conversion_cache[to][from] = find_direct_conversion(to: to, from: from)
75
+ end
76
+ end
77
+
78
+ def find_direct_conversion(to:, from:)
79
+ units.each do |unit|
80
+ return unit.conversion_amount if unit.name == from && unit.conversion_unit == to
81
+ return unit.inverse_conversion_amount if unit.name == to && unit.conversion_unit == from
82
+ end
83
+
84
+ nil
85
+ end
86
+
87
+ def find_tree_traversal_conversion(to:, from:)
88
+ traverse(from: from, to: to, units_remaining: units.map(&:name), amount: 1)
89
+ end
90
+
91
+ def traverse(from:, to:, units_remaining:, amount:)
92
+ units_remaining = units_remaining - [from]
93
+
94
+ units_remaining.each do |name|
95
+ conversion = find_direct_conversion_cached(from: from, to: name)
96
+
97
+ if conversion
98
+ new_amount = amount * conversion
99
+ if name == to
100
+ return new_amount
101
+ else
102
+ result = traverse(from: name, to: to, units_remaining: units_remaining, amount: new_amount)
103
+ return result if result
104
+ end
105
+ end
106
+ end
107
+
108
+ nil
109
+ end
110
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ module Measured
3
+ class CycleDetected < UnitError
4
+ attr_reader :unit
5
+
6
+ def initialize(unit)
7
+ super("The following conversion introduces cycles in the unit system: #{unit}. Remove the conversion or fix the cycle.")
8
+ @unit = unit
9
+ end
10
+ end
11
+ end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
1
2
  class Measured::Measurable < Numeric
3
+ DEFAULT_FORMAT_STRING = "%.2<value>f %<unit>s"
4
+
2
5
  include Measured::Arithmetic
3
6
 
4
7
  attr_reader :unit, :value
@@ -9,7 +12,7 @@ class Measured::Measurable < Numeric
9
12
  @unit = unit_from_unit_or_name!(unit)
10
13
  @value = case value
11
14
  when Float
12
- BigDecimal(value, Float::DIG + 1)
15
+ BigDecimal(value, Float::DIG)
13
16
  when BigDecimal, Rational
14
17
  value
15
18
  when Integer
@@ -17,6 +20,18 @@ class Measured::Measurable < Numeric
17
20
  else
18
21
  BigDecimal(value)
19
22
  end
23
+
24
+ @value_string = begin
25
+ str = case value
26
+ when Rational
27
+ value.denominator == 1 ? value.numerator.to_s : value.to_f.to_s
28
+ when BigDecimal
29
+ value.to_s("F")
30
+ else
31
+ value.to_f.to_s
32
+ end
33
+ str.gsub(/\.0*\Z/, "")
34
+ end.freeze
20
35
  end
21
36
 
22
37
  def convert_to(new_unit)
@@ -28,19 +43,25 @@ class Measured::Measurable < Numeric
28
43
  self.class.new(new_value, new_unit)
29
44
  end
30
45
 
46
+ def format(format_string=nil, with_conversion_string: true)
47
+ kwargs = {
48
+ value: self.value,
49
+ unit: self.unit.to_s(with_conversion_string: with_conversion_string),
50
+ }
51
+ (format_string || DEFAULT_FORMAT_STRING) % kwargs
52
+ end
53
+
31
54
  def to_s
32
- @to_s ||= "#{value_string} #{unit.name}"
55
+ "#{@value_string} #{unit.name}"
33
56
  end
34
57
 
35
58
  def humanize
36
- @humanize ||= begin
37
- unit_string = value == 1 ? unit.name : ActiveSupport::Inflector.pluralize(unit.name)
38
- "#{value_string} #{unit_string}"
39
- end
59
+ unit_string = value == 1 ? unit.name : ActiveSupport::Inflector.pluralize(unit.name)
60
+ "#{@value_string} #{unit_string}"
40
61
  end
41
62
 
42
63
  def inspect
43
- @inspect ||= "#<#{self.class}: #{value_string} #{unit.inspect}>"
64
+ "#<#{self.class}: #{@value_string} #{unit.inspect}>"
44
65
  end
45
66
 
46
67
  def <=>(other)
@@ -76,18 +97,4 @@ class Measured::Measurable < Numeric
76
97
  def unit_from_unit_or_name!(value)
77
98
  value.is_a?(Measured::Unit) ? value : self.class.unit_system.unit_for!(value)
78
99
  end
79
-
80
- def value_string
81
- @value_string ||= begin
82
- str = case value
83
- when Rational
84
- value.denominator == 1 ? value.numerator.to_s : value.to_f.to_s
85
- when BigDecimal
86
- value.to_s("F")
87
- else
88
- value.to_f.to_s
89
- end
90
- str.gsub(/\.0*\Z/, "")
91
- end
92
- end
93
100
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module Measured
3
+ class MissingConversionPath < UnitError
4
+ attr_reader :from, :to
5
+
6
+ def initialize(from, to)
7
+ super("Cannot find conversion path from #{from} to #{to}.")
8
+ @from = from
9
+ @to = to
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Measured::Parser
2
3
  extend self
3
4
 
@@ -32,6 +33,6 @@ module Measured::Parser
32
33
 
33
34
  raise Measured::UnitError, "Cannot parse measurement from '#{string}'" unless result
34
35
 
35
- [result.captures[0].to_r, result.captures[1]]
36
+ [result.captures[0].to_r, -result.captures[1]]
36
37
  end
37
38
  end
data/lib/measured/unit.rb CHANGED
@@ -1,43 +1,41 @@
1
+ # frozen_string_literal: true
1
2
  class Measured::Unit
2
3
  include Comparable
3
4
 
4
- attr_reader :name, :aliases, :conversion_amount, :conversion_unit, :unit_system
5
+ attr_reader :name, :names, :aliases, :conversion_amount, :conversion_unit, :unit_system, :inverse_conversion_amount
5
6
 
6
7
  def initialize(name, aliases: [], value: nil, unit_system: nil)
7
8
  @name = name.to_s.freeze
8
9
  @aliases = aliases.map(&:to_s).map(&:freeze).freeze
10
+ @names = ([@name] + @aliases).sort!.freeze
9
11
  @conversion_amount, @conversion_unit = parse_value(value) if value
12
+ @inverse_conversion_amount = (1 / conversion_amount if conversion_amount)
13
+ @conversion_string = ("#{conversion_amount} #{conversion_unit}" if conversion_amount || conversion_unit)
10
14
  @unit_system = unit_system
11
15
  end
12
16
 
13
- def with_unit_system(unit_system)
17
+ def with(name: nil, unit_system: nil, aliases: nil, value: nil)
14
18
  self.class.new(
15
- name,
16
- aliases: aliases,
17
- value: conversion_string,
18
- unit_system: unit_system
19
+ name || self.name,
20
+ aliases: aliases || self.aliases,
21
+ value: value || @conversion_string,
22
+ unit_system: unit_system || self.unit_system
19
23
  )
20
24
  end
21
25
 
22
- def names
23
- @names ||= ([name] + aliases).sort!.freeze
24
- end
25
-
26
- def to_s
27
- @to_s ||= if conversion_string
28
- "#{name} (#{conversion_string})".freeze
26
+ def to_s(with_conversion_string: true)
27
+ if with_conversion_string && @conversion_string
28
+ "#{name} (#{@conversion_string})".freeze
29
29
  else
30
30
  name
31
31
  end
32
32
  end
33
33
 
34
34
  def inspect
35
- @inspect ||= begin
36
- pieces = [name]
37
- pieces << "(#{aliases.join(", ")})" if aliases.any?
38
- pieces << conversion_string if conversion_string
39
- "#<#{self.class.name}: #{pieces.join(" ")}>".freeze
40
- end
35
+ pieces = [name]
36
+ pieces << "(#{aliases.join(", ")})" if aliases.any?
37
+ pieces << @conversion_string if @conversion_string
38
+ "#<#{self.class.name}: #{pieces.join(" ")}>".freeze
41
39
  end
42
40
 
43
41
  def <=>(other)
@@ -53,16 +51,8 @@ class Measured::Unit
53
51
  end
54
52
  end
55
53
 
56
- def inverse_conversion_amount
57
- @inverse_conversion_amount ||= 1 / conversion_amount
58
- end
59
-
60
54
  private
61
55
 
62
- def conversion_string
63
- @conversion_string ||= ("#{conversion_amount} #{conversion_unit}" if conversion_amount || conversion_unit)
64
- end
65
-
66
56
  def parse_value(tokens)
67
57
  case tokens
68
58
  when String
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ module Measured
3
+ class UnitAlreadyAdded < UnitError
4
+ attr_reader :unit_name
5
+
6
+ def initialize(unit_name)
7
+ super("Unit #{unit_name} has already been added.")
8
+ @unit_name = unit_name
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Measured
3
+ class UnitError < StandardError ; end
4
+ end
@@ -1,16 +1,22 @@
1
+ # frozen_string_literal: true
1
2
  class Measured::UnitSystem
2
- attr_reader :units
3
-
4
- def initialize(units)
5
- @units = units.map { |unit| unit.with_unit_system(self) }
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
3
+ attr_reader :units, :unit_names, :unit_names_with_aliases
4
+
5
+ def initialize(units, cache: nil)
6
+ @units = units.map { |unit| unit.with(unit_system: self) }
7
+ @units = @units.map do |unit|
8
+ next unit unless unit.conversion_unit
9
+ conversion_unit = @units.find { |u| u.names.include?(unit.conversion_unit) }
10
+ next unit unless conversion_unit
11
+ unit.with(value: [unit.conversion_amount, conversion_unit.name])
12
+ end
13
+ @unit_names = @units.map(&:name).sort.freeze
14
+ @unit_names_with_aliases = @units.flat_map(&:names).sort.freeze
15
+ @unit_name_to_unit = @units.each_with_object({}) do |unit, hash|
16
+ unit.names.each { |name| hash[name.to_s] = unit }
17
+ end
18
+ @conversion_table_builder = Measured::ConversionTableBuilder.new(@units, cache: cache)
19
+ @conversion_table = @conversion_table_builder.to_h.freeze
14
20
  end
15
21
 
16
22
  def unit_or_alias?(name)
@@ -40,16 +46,15 @@ class Measured::UnitSystem
40
46
  value.to_r * conversion
41
47
  end
42
48
 
43
- protected
44
-
45
- def conversion_table
46
- @conversion_table ||= Measured::ConversionTable.build(@units)
49
+ def update_cache
50
+ @conversion_table_builder.update_cache
47
51
  end
48
52
 
49
- def unit_name_to_unit
50
- @unit_name_to_unit ||= @units.inject({}) do |hash, unit|
51
- unit.names.each { |name| hash[name.to_s] = unit }
52
- hash
53
- end
53
+ def cached?
54
+ @conversion_table_builder.cached?
54
55
  end
56
+
57
+ protected
58
+
59
+ attr_reader :unit_name_to_unit, :conversion_table
55
60
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
1
2
  class Measured::UnitSystemBuilder
2
3
  def initialize
3
4
  @units = []
5
+ @cache = nil
4
6
  end
5
7
 
6
8
  def unit(unit_name, aliases: [], value: nil)
@@ -8,12 +10,54 @@ class Measured::UnitSystemBuilder
8
10
  nil
9
11
  end
10
12
 
13
+ def si_unit(unit_name, aliases: [], value: nil)
14
+ @units += build_si_units(unit_name, aliases: aliases, value: value)
15
+ nil
16
+ end
17
+
18
+ def cache(cache_class, *args)
19
+ @cache = {class: cache_class, args: args}
20
+ nil
21
+ end
22
+
11
23
  def build
12
- Measured::UnitSystem.new(@units)
24
+ Measured::UnitSystem.new(@units, cache: @cache)
13
25
  end
14
26
 
15
27
  private
16
28
 
29
+ SI_PREFIXES = [
30
+ ["y", "yocto", -24],
31
+ ["z", "zepto", -21],
32
+ ["a", "atto", -18],
33
+ ["f", "femto", -15],
34
+ ["p", "pico", -12],
35
+ ["n", "nano", -9],
36
+ ["μ", "micro", -6],
37
+ ["m", "milli", -3],
38
+ ["c", "centi", -2],
39
+ ["d", "deci", -1],
40
+ ["da", "deca", 1],
41
+ ["h", "hecto", 2],
42
+ ["k", "kilo", 3],
43
+ ["M", "mega", 6],
44
+ ["G", "giga", 9],
45
+ ["T", "tera", 12],
46
+ ["P", "peta", 15],
47
+ ["E", "exa", 18],
48
+ ["Z", "zetta", 21],
49
+ ["Y", "yotta", 24],
50
+ ].map(&:freeze).freeze
51
+
52
+ def build_si_units(name, aliases: [], value: nil)
53
+ si_units = [build_unit(name, aliases: aliases, value: value)]
54
+ SI_PREFIXES.each do |short, long, exp|
55
+ long_names = aliases.map { |suffix| "#{long}#{suffix}" }
56
+ si_units << build_unit("#{short}#{name}", aliases: long_names, value: "#{10 ** exp} #{name}")
57
+ end
58
+ si_units
59
+ end
60
+
17
61
  def build_unit(name, aliases: [], value: nil)
18
62
  unit = Measured::Unit.new(name, aliases: aliases, value: value)
19
63
  check_for_duplicate_unit_names!(unit)
@@ -23,7 +67,7 @@ class Measured::UnitSystemBuilder
23
67
  def check_for_duplicate_unit_names!(unit)
24
68
  names = @units.flat_map(&:names)
25
69
  if names.any? { |name| unit.names.include?(name) }
26
- raise Measured::UnitError, "Unit #{unit.name} has already been added."
70
+ raise Measured::UnitAlreadyAdded.new(unit.name)
27
71
  end
28
72
  end
29
73
  end
@@ -1,8 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  Measured::Length = Measured.build do
2
- unit :m, aliases: [:meter, :metre, :meters, :metres]
3
- unit :cm, value: "1/100 m", aliases: [:centimeter, :centimetre, :centimeters, :centimetres]
4
- unit :mm, value: "1/1000 m", aliases: [:millimeter, :millimetre, :millimeters, :millimetres]
3
+ si_unit :m, aliases: [:meter, :metre, :meters, :metres]
4
+
5
5
  unit :in, value: "0.0254 m", aliases: [:inch, :inches]
6
6
  unit :ft, value: "12 in", aliases: [:foot, :feet]
7
7
  unit :yd, value: "3 ft", aliases: [:yard, :yards]
8
+ unit :mi, value: "5280 ft", aliases: [:mile, :miles]
9
+
10
+ cache Measured::Cache::Json, "length.json"
8
11
  end