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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +31 -0
- data/CHANGELOG.md +60 -0
- data/README.md +68 -10
- data/Rakefile +22 -1
- data/cache/.keep +0 -0
- data/cache/length.json +2553 -0
- data/cache/volume.json +4695 -0
- data/cache/weight.json +3195 -0
- data/dev.yml +2 -6
- data/gemfiles/{activesupport-4.2.gemfile → activesupport-5.2.gemfile} +1 -1
- data/gemfiles/activesupport-6.0.gemfile +5 -0
- data/gemfiles/activesupport-6.1.gemfile +5 -0
- data/lib/measured/arithmetic.rb +10 -0
- data/lib/measured/base.rb +13 -6
- data/lib/measured/cache/json.rb +55 -0
- data/lib/measured/cache/json_writer.rb +9 -0
- data/lib/measured/cache/null.rb +16 -0
- data/lib/measured/conversion_table_builder.rb +110 -0
- data/lib/measured/cycle_detected.rb +11 -0
- data/lib/measured/measurable.rb +28 -21
- data/lib/measured/missing_conversion_path.rb +12 -0
- data/lib/measured/parser.rb +2 -1
- data/lib/measured/unit.rb +17 -27
- data/lib/measured/unit_already_added.rb +11 -0
- data/lib/measured/unit_error.rb +4 -0
- data/lib/measured/unit_system.rb +26 -21
- data/lib/measured/unit_system_builder.rb +46 -2
- data/lib/measured/units/length.rb +6 -3
- data/lib/measured/units/volume.rb +20 -0
- data/lib/measured/units/weight.rb +10 -2
- data/lib/measured/version.rb +2 -1
- data/lib/measured.rb +2 -0
- data/measured.gemspec +13 -5
- data/test/arithmetic_test.rb +36 -0
- data/test/cache/json_test.rb +43 -0
- data/test/cache/json_writer_test.rb +23 -0
- data/test/cache/null_test.rb +20 -0
- data/test/cache_consistency_test.rb +19 -0
- data/test/conversion_table_builder_test.rb +147 -0
- data/test/measurable_test.rb +28 -13
- data/test/parser_test.rb +1 -0
- data/test/support/always_true_cache.rb +14 -0
- data/test/support/fake_system.rb +1 -0
- data/test/support/subclasses.rb +7 -0
- data/test/test_helper.rb +5 -3
- data/test/unit_error_test.rb +1 -0
- data/test/unit_system_builder_test.rb +120 -2
- data/test/unit_system_test.rb +15 -0
- data/test/unit_test.rb +14 -5
- data/test/units/length_test.rb +123 -10
- data/test/units/volume_test.rb +497 -0
- data/test/units/weight_test.rb +290 -5
- metadata +49 -18
- data/.travis.yml +0 -17
- data/lib/measured/conversion_table.rb +0 -65
- 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:
|
|
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
|
|
17
|
+
bundle exec rake test "$@"
|
|
18
18
|
fi
|
|
19
|
-
|
|
20
|
-
packages:
|
|
21
|
-
- git@github.com:Shopify/dev-shopify.git
|
|
22
|
-
|
data/lib/measured/arithmetic.rb
CHANGED
|
@@ -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
|
|
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/
|
|
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,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
|
data/lib/measured/measurable.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
55
|
+
"#{@value_string} #{unit.name}"
|
|
33
56
|
end
|
|
34
57
|
|
|
35
58
|
def humanize
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
data/lib/measured/parser.rb
CHANGED
|
@@ -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
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
data/lib/measured/unit_system.rb
CHANGED
|
@@ -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.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@
|
|
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
|
-
|
|
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
|
|
50
|
-
@
|
|
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::
|
|
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
|
-
|
|
3
|
-
|
|
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
|