unitwise 2.0.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/data/prefix.yaml CHANGED
@@ -3,119 +3,119 @@
3
3
  :symbol: Y
4
4
  :primary_code: Y
5
5
  :secondary_code: YA
6
- :scalar: 1e24
6
+ :scalar: 1000000000000000000000000
7
7
  - :names: zetta
8
8
  :symbol: Z
9
9
  :primary_code: Z
10
10
  :secondary_code: ZA
11
- :scalar: 1e21
11
+ :scalar: 1000000000000000000000
12
12
  - :names: exa
13
13
  :symbol: E
14
14
  :primary_code: E
15
15
  :secondary_code: EX
16
- :scalar: 1e18
16
+ :scalar: 1000000000000000000
17
17
  - :names: peta
18
18
  :symbol: P
19
19
  :primary_code: P
20
20
  :secondary_code: PT
21
- :scalar: 1e15
21
+ :scalar: 1000000000000000
22
22
  - :names: tera
23
23
  :symbol: T
24
24
  :primary_code: T
25
25
  :secondary_code: TR
26
- :scalar: 1e12
26
+ :scalar: 1000000000000
27
27
  - :names: giga
28
28
  :symbol: G
29
29
  :primary_code: G
30
30
  :secondary_code: GA
31
- :scalar: 1e9
31
+ :scalar: 1000000000
32
32
  - :names: mega
33
33
  :symbol: M
34
34
  :primary_code: M
35
35
  :secondary_code: MA
36
- :scalar: 1e6
36
+ :scalar: 1000000
37
37
  - :names: kilo
38
38
  :symbol: k
39
39
  :primary_code: k
40
40
  :secondary_code: K
41
- :scalar: 1e3
41
+ :scalar: 1000
42
42
  - :names: hecto
43
43
  :symbol: h
44
44
  :primary_code: h
45
45
  :secondary_code: H
46
- :scalar: 1e2
46
+ :scalar: 100
47
47
  - :names: deka
48
48
  :symbol: da
49
49
  :primary_code: da
50
50
  :secondary_code: DA
51
- :scalar: 1e1
51
+ :scalar: 10
52
52
  - :names: deci
53
53
  :symbol: d
54
54
  :primary_code: d
55
55
  :secondary_code: D
56
- :scalar: 1e-1
56
+ :scalar: !ruby/object:BigDecimal 18:0.1e0
57
57
  - :names: centi
58
58
  :symbol: c
59
59
  :primary_code: c
60
60
  :secondary_code: C
61
- :scalar: 1e-2
61
+ :scalar: !ruby/object:BigDecimal 18:0.1e-1
62
62
  - :names: milli
63
63
  :symbol: m
64
64
  :primary_code: m
65
65
  :secondary_code: M
66
- :scalar: 1e-3
66
+ :scalar: !ruby/object:BigDecimal 18:0.1e-2
67
67
  - :names: micro
68
- :symbol: "μ"
68
+ :symbol: μ
69
69
  :primary_code: u
70
70
  :secondary_code: U
71
- :scalar: 1e-6
71
+ :scalar: !ruby/object:BigDecimal 18:0.1e-5
72
72
  - :names: nano
73
73
  :symbol: n
74
74
  :primary_code: n
75
75
  :secondary_code: N
76
- :scalar: 1e-9
76
+ :scalar: !ruby/object:BigDecimal 18:0.1e-8
77
77
  - :names: pico
78
78
  :symbol: p
79
79
  :primary_code: p
80
80
  :secondary_code: P
81
- :scalar: 1e-12
81
+ :scalar: !ruby/object:BigDecimal 18:0.1e-11
82
82
  - :names: femto
83
83
  :symbol: f
84
84
  :primary_code: f
85
85
  :secondary_code: F
86
- :scalar: 1e-15
86
+ :scalar: !ruby/object:BigDecimal 18:0.1e-14
87
87
  - :names: atto
88
88
  :symbol: a
89
89
  :primary_code: a
90
90
  :secondary_code: A
91
- :scalar: 1e-18
91
+ :scalar: !ruby/object:BigDecimal 18:0.1e-17
92
92
  - :names: zepto
93
93
  :symbol: z
94
94
  :primary_code: z
95
95
  :secondary_code: ZO
96
- :scalar: 1e-21
96
+ :scalar: !ruby/object:BigDecimal 18:0.1e-20
97
97
  - :names: yocto
98
98
  :symbol: y
99
99
  :primary_code: y
100
100
  :secondary_code: YO
101
- :scalar: 1e-24
101
+ :scalar: !ruby/object:BigDecimal 18:0.1e-23
102
102
  - :names: kibi
103
103
  :symbol: Ki
104
104
  :primary_code: Ki
105
105
  :secondary_code: KIB
106
- :scalar: '1024'
106
+ :scalar: 1024
107
107
  - :names: mebi
108
108
  :symbol: Mi
109
109
  :primary_code: Mi
110
110
  :secondary_code: MIB
111
- :scalar: '1048576'
111
+ :scalar: 1048576
112
112
  - :names: gibi
113
113
  :symbol: Gi
114
114
  :primary_code: Gi
115
115
  :secondary_code: GIB
116
- :scalar: '1073741824'
116
+ :scalar: 1073741824
117
117
  - :names: tebi
118
118
  :symbol: Ti
119
119
  :primary_code: Ti
120
120
  :secondary_code: TIB
121
- :scalar: '1099511627776'
121
+ :scalar: 1099511627776
data/lib/unitwise/atom.rb CHANGED
@@ -7,13 +7,13 @@ module Unitwise
7
7
  include Compatible
8
8
 
9
9
  class << self
10
- # Array of hashes representing atom properties.
10
+ # Array of hashes representing default atom properties.
11
11
  # @api private
12
12
  def data
13
13
  @data ||= data_files.map { |file| YAML.load(File.open file) }.flatten
14
14
  end
15
15
 
16
- # Data files containing atom data
16
+ # Data files containing default atom data
17
17
  # @api private
18
18
  def data_files
19
19
  %w(base_unit derived_unit).map { |type| Unitwise.data_file type }
@@ -103,11 +103,11 @@ module Unitwise
103
103
  # @return [Numeric]
104
104
  # @api public
105
105
  def scalar(magnitude = 1)
106
- base? ? BigDecimal(magnitude.to_s) : scale.scalar(magnitude)
106
+ base? ? magnitude : scale.scalar(magnitude)
107
107
  end
108
108
 
109
109
  def magnitude(scalar = scalar())
110
- special? ? scale.magnitude(scalar) : BigDecimal('1')
110
+ special? ? scale.magnitude(scalar) : 1
111
111
  end
112
112
 
113
113
  # An atom may have a complex scale with several base atoms at various
@@ -117,5 +117,34 @@ module Unitwise
117
117
  base? ? [Term.new(:atom_code => primary_code)] : scale.root_terms
118
118
  end
119
119
  memoize :root_terms
120
+
121
+
122
+ # A basic validator for atoms. It checks for the bare minimum properties
123
+ # and that it's scalar and magnitude can be resolved. Note that this method
124
+ # requires the units it depends on to already exist, so it is not used
125
+ # when loading the initial data from UCUM.
126
+ # @return [true] returns true if the atom is valid
127
+ # @raise [Unitwise::DefinitionError]
128
+ def validate!
129
+ missing_properties = %i{primary_code names scale}.select do |prop|
130
+ val = liner_get(prop)
131
+ val.nil? || (val.respond_to?(:empty) && val.empty?)
132
+ end
133
+
134
+ if !missing_properties.empty?
135
+ missing_list = missing_properties.join(',')
136
+ raise Unitwise::DefinitionError,
137
+ "Atom has missing properties: #{missing_list}."
138
+ end
139
+
140
+ msg = "Atom definition could not be resolved. Ensure that it is a base " \
141
+ "unit or is defined relative to existing units."
142
+
143
+ begin
144
+ !scalar.nil? && !magnitude.nil? || raise(Unitwise::DefinitionError, msg)
145
+ rescue Unitwise::ExpressionError
146
+ raise Unitwise::DefinitionError, msg
147
+ end
148
+ end
120
149
  end
121
150
  end
@@ -1,7 +1,10 @@
1
1
  module Unitwise
2
- class ExpressionError < Exception
2
+ class ExpressionError < StandardError
3
3
  end
4
4
 
5
- class ConversionError < Exception
5
+ class ConversionError < StandardError
6
+ end
7
+
8
+ class DefinitionError < StandardError
6
9
  end
7
10
  end
@@ -5,13 +5,7 @@ module Unitwise
5
5
  # of a string, as well as caching the results.
6
6
  class Decomposer
7
7
 
8
- MODES = [:primary_code, :secondary_code, :names, :slugs, :symbol]
9
-
10
- PARSERS = MODES.reduce({}) do |hash, mode|
11
- hash[mode] = Parser.new(mode); hash
12
- end
13
-
14
- TRANSFORMER = Transformer.new
8
+ MODES = [:primary_code, :secondary_code, :names, :slugs, :symbol].freeze
15
9
 
16
10
  class << self
17
11
 
@@ -25,6 +19,16 @@ module Unitwise
25
19
  end
26
20
  end
27
21
 
22
+ def parsers
23
+ @parsers ||= MODES.reduce({}) do |hash, mode|
24
+ hash[mode] = Parser.new(mode); hash
25
+ end
26
+ end
27
+
28
+ def transformer
29
+ @transformer = Transformer.new
30
+ end
31
+
28
32
  private
29
33
 
30
34
  # A simple cache to prevent re-decomposing the same units
@@ -32,6 +36,14 @@ module Unitwise
32
36
  def cache
33
37
  @cache ||= {}
34
38
  end
39
+
40
+ # Reset memoized data. Allows rebuilding of parsers, transformers, and
41
+ # the cache after list of atoms has been modified.
42
+ def reset
43
+ @parsers = nil
44
+ @transformer = nil
45
+ @cache = nil
46
+ end
35
47
  end
36
48
 
37
49
  attr_reader :expression, :mode
@@ -44,7 +56,7 @@ module Unitwise
44
56
  end
45
57
 
46
58
  def parse
47
- PARSERS.reduce(nil) do |_, (mode, parser)|
59
+ self.class.parsers.reduce(nil) do |_, (mode, parser)|
48
60
  parsed = parser.parse(expression) rescue next
49
61
  @mode = mode
50
62
  break parsed
@@ -52,7 +64,7 @@ module Unitwise
52
64
  end
53
65
 
54
66
  def transform
55
- @transform ||= TRANSFORMER.apply(parse, :mode => mode)
67
+ @transform ||= self.class.transformer.apply(parse, :mode => mode)
56
68
  end
57
69
 
58
70
  def terms
@@ -5,19 +5,15 @@ module Unitwise
5
5
  class Matcher
6
6
  class << self
7
7
  def atom(mode)
8
- @atom ||= {}
9
- @atom[mode] ||= new(Atom.all, mode).alternative
8
+ new(Atom.all, mode).alternative
10
9
  end
11
10
 
12
11
  def metric_atom(mode)
13
- @metric_atom ||= {}
14
- @metric_atom[mode] ||=
15
- new(Atom.all.select(&:metric?), mode).alternative
12
+ new(Atom.all.select(&:metric?), mode).alternative
16
13
  end
17
14
 
18
15
  def prefix(mode)
19
- @prefix ||= {}
20
- @prefix[mode] ||= new(Prefix.all, mode).alternative
16
+ new(Prefix.all, mode).alternative
21
17
  end
22
18
  end
23
19
 
@@ -41,7 +37,6 @@ module Unitwise
41
37
  def alternative
42
38
  Parslet::Atoms::Alternative.new(*matchers)
43
39
  end
44
-
45
40
  end
46
41
  end
47
42
  end
@@ -5,14 +5,21 @@ module Unitwise
5
5
  class Parser < Parslet::Parser
6
6
  attr_reader :key
7
7
  def initialize(key = :primary_code)
8
- @key = key
8
+ @key = key
9
+ @atom_matcher = Matcher.atom(key)
10
+ @metric_atom_matcher = Matcher.metric_atom(key)
11
+ @prefix_matcher = Matcher.prefix(key)
9
12
  end
10
13
 
14
+ private
15
+
16
+ attr_reader :atom_matcher, :metric_atom_matcher, :prefix_matcher
17
+
11
18
  root :expression
12
19
 
13
- rule (:atom) { Matcher.atom(key).as(:atom_code) }
14
- rule (:metric_atom) { Matcher.metric_atom(key).as(:atom_code) }
15
- rule (:prefix) { Matcher.prefix(key).as(:prefix_code) }
20
+ rule (:atom) { atom_matcher.as(:atom_code) }
21
+ rule (:metric_atom) { metric_atom_matcher.as(:atom_code) }
22
+ rule (:prefix) { prefix_matcher.as(:prefix_code) }
16
23
 
17
24
  rule (:simpleton) do
18
25
  (prefix.as(:prefix) >> metric_atom.as(:atom) | atom.as(:atom))
@@ -22,6 +22,14 @@ module Unitwise
22
22
  5.0 / 9.0 * (x + 459.67)
23
23
  end
24
24
 
25
+ def self.to_degre(x)
26
+ 4.0 * x / 5.0 - 218.52
27
+ end
28
+
29
+ def self.from_degre(x)
30
+ 5.0 / 4.0 * (x + 218.52)
31
+ end
32
+
25
33
  def self.to_hpX(x)
26
34
  -log10(x)
27
35
  end
@@ -129,7 +129,7 @@ module Unitwise
129
129
  # measurement.to_r # => (17/4)
130
130
  # @api public
131
131
  def to_r
132
- Rational(value)
132
+ Number.rationalize(value)
133
133
  end
134
134
 
135
135
  # Will attempt to convert to a unit by method name.
@@ -156,10 +156,6 @@ module Unitwise
156
156
  self.class.new(*args)
157
157
  end
158
158
 
159
- # Set the value for the measurement.
160
- # @api private
161
- attr_writer :value
162
-
163
159
  # Determine value of the unit after conversion to another unit
164
160
  # @api private
165
161
  def converted_value(other_unit)
@@ -0,0 +1,62 @@
1
+ module Unitwise
2
+ class Number
3
+ # Attempts to coerce a value to the simplest Numeric that fully expresses
4
+ # it's value. For instance a value of 1.0 would return 1, a value of
5
+ # #<BigDecimal:7f9558d559b8,'0.45E1',18(18)> would return 4.5.
6
+ # @api public
7
+ # @param value [Integer, Float, Rational, String, BigDecimal]
8
+ # @return [Integer, Float, Rational, BigDecimal]
9
+ def self.simplify(value)
10
+ case value
11
+ when Integer
12
+ value
13
+ when Float
14
+ (i = value.to_i) == value ? i : value
15
+ when Rational
16
+ if (i = value.to_i) == value
17
+ i
18
+ elsif (f = value.to_f) && f.to_r == value
19
+ f
20
+ else
21
+ value
22
+ end
23
+ else # String, BigDecimal, Other
24
+ s = value.is_a?(String) ? value : value.to_s
25
+ d = value.is_a?(BigDecimal) ? value : BigDecimal(s)
26
+ if (i = d.to_i) == d
27
+ i
28
+ elsif (f = d.to_f) == d
29
+ f
30
+ else
31
+ d
32
+ end
33
+ end
34
+ end
35
+
36
+ # Coerces a string-like number to a BigDecimal or Integer as appropriate
37
+ # @api public
38
+ # @param value Something that can be represented as a string number
39
+ # @return [Integer, BigDecimal]
40
+ def self.coefficify(value)
41
+ d = BigDecimal.new(value.to_s)
42
+ if (i = d.to_i) == d
43
+ i
44
+ else
45
+ d
46
+ end
47
+ end
48
+
49
+ # Coerce a numeric to a Rational, but avoid inaccurate conversions by
50
+ # jruby. More details here: https://github.com/jruby/jruby/issues/4711.
51
+ # @api public
52
+ # @param number [Numeric]
53
+ # @return Rational
54
+ def self.rationalize(number)
55
+ if number.is_a?(BigDecimal) && RUBY_PLATFORM == 'java'
56
+ number.to_s.to_r
57
+ else
58
+ number.to_r
59
+ end
60
+ end
61
+ end
62
+ end
@@ -14,11 +14,5 @@ module Unitwise
14
14
  def self.data_file
15
15
  Unitwise.data_file 'prefix'
16
16
  end
17
-
18
- # Set the scalar value for the prefix, always as a BigDecimal
19
- # @api semipublic
20
- def scalar=(value)
21
- @scalar = BigDecimal(value.to_s)
22
- end
23
17
  end
24
18
  end
@@ -30,10 +30,6 @@ module Unitwise
30
30
  unit.atoms
31
31
  end
32
32
 
33
- def value=(value)
34
- @value = BigDecimal(value.to_s)
35
- end
36
-
37
33
  # List the terms associated with this scale's unit.
38
34
  # @return [Array]
39
35
  # @api public
@@ -56,7 +52,7 @@ module Unitwise
56
52
  if special?
57
53
  unit.scalar(magnitude)
58
54
  else
59
- value * unit.scalar
55
+ Number.rationalize(value) * Number.rationalize(unit.scalar)
60
56
  end
61
57
  end
62
58
 
@@ -95,17 +91,7 @@ module Unitwise
95
91
  # @return [Numeric]
96
92
  # @api public
97
93
  def simplified_value
98
- if value.is_a?(Integer)
99
- value
100
- elsif (i = Integer(value)) == value
101
- i
102
- elsif value.is_a?(Float) || value.is_a?(Rational)
103
- value
104
- elsif (f = Float(value)) == value
105
- f
106
- else
107
- value
108
- end
94
+ Unitwise::Number.simplify(value)
109
95
  end
110
96
  memoize :simplified_value
111
97
 
@@ -12,7 +12,7 @@ module Unitwise::Standard
12
12
  end
13
13
 
14
14
  def value
15
- attributes["function"]["@value"].to_f
15
+ Unitwise::Number.simplify(attributes["function"]["@value"])
16
16
  end
17
17
 
18
18
  def unit
@@ -32,4 +32,4 @@ module Unitwise::Standard
32
32
  end
33
33
 
34
34
  end
35
- end
35
+ end
@@ -6,12 +6,13 @@ module Unitwise::Standard
6
6
  end
7
7
 
8
8
  def scale
9
- attributes["value"].attributes["value"]
9
+ Unitwise::Number.coefficify(
10
+ attributes.fetch('value').attributes.fetch('value')
11
+ )
10
12
  end
11
13
 
12
14
  def to_hash
13
15
  super().merge(:scalar => scale)
14
16
  end
15
-
16
17
  end
17
- end
18
+ end
@@ -7,7 +7,7 @@ module Unitwise::Standard
7
7
  end
8
8
 
9
9
  def value
10
- nori.attributes["value"].to_f
10
+ Unitwise::Number.coefficify(nori.attributes.fetch('value'))
11
11
  end
12
12
 
13
13
  def primary_unit_code
@@ -22,4 +22,4 @@ module Unitwise::Standard
22
22
  {:value => value, :unit_code => primary_unit_code}
23
23
  end
24
24
  end
25
- end
25
+ end
data/lib/unitwise/term.rb CHANGED
@@ -59,7 +59,7 @@ module Unitwise
59
59
  # @param magnitude [Numeric] The magnitude to calculate the scalar for.
60
60
  # @return [Numeric] The unitless linear scalar value.
61
61
  # @api public
62
- def scalar(magnitude = 1.0)
62
+ def scalar(magnitude = 1)
63
63
  calculate(atom ? atom.scalar(magnitude) : magnitude)
64
64
  end
65
65
 
@@ -68,7 +68,7 @@ module Unitwise
68
68
  # @return [Numeric] The magnitude on this scale.
69
69
  # @api public
70
70
  def magnitude(scalar = scalar())
71
- calculate(atom ? atom.magnitude(scalar) : 1.0)
71
+ calculate(atom ? atom.magnitude(scalar) : 1)
72
72
  end
73
73
 
74
74
  # The base units this term is derived from
data/lib/unitwise/unit.rb CHANGED
@@ -86,8 +86,8 @@ module Unitwise
86
86
  # @param magnitude [Numeric] An optional magnitude on this unit's scale.
87
87
  # @return [Numeric] A scalar value on a linear scale
88
88
  # @api public
89
- def scalar(magnitude = 1.0)
90
- terms.reduce(1.0) do |prod, term|
89
+ def scalar(magnitude = 1)
90
+ terms.reduce(1) do |prod, term|
91
91
  prod * term.scalar(magnitude)
92
92
  end
93
93
  end
@@ -98,7 +98,7 @@ module Unitwise
98
98
  # @return [Numeric] The equivalent magnitude on this scale
99
99
  # @api public
100
100
  def magnitude(scalar = scalar())
101
- terms.reduce(1.0) do |prod, term|
101
+ terms.reduce(1) do |prod, term|
102
102
  prod * term.magnitude(scalar)
103
103
  end
104
104
  end
@@ -1,3 +1,3 @@
1
1
  module Unitwise
2
- VERSION = '2.0.0'
2
+ VERSION = '2.2.0'.freeze
3
3
  end
data/lib/unitwise.rb CHANGED
@@ -8,6 +8,7 @@ require 'bigdecimal'
8
8
  require 'unitwise/version'
9
9
  require 'unitwise/base'
10
10
  require 'unitwise/compatible'
11
+ require 'unitwise/number'
11
12
  require 'unitwise/expression'
12
13
  require 'unitwise/scale'
13
14
  require 'unitwise/functional'
@@ -43,7 +44,19 @@ module Unitwise
43
44
  false
44
45
  end
45
46
  end
46
-
47
+
48
+ # Add additional atoms. Useful for registering uncommon or custom units.
49
+ # @param properties [Hash] Properties of the atom
50
+ # @return [Unitwise::Atom] The newly created atom
51
+ # @raise [Unitwise::DefinitionError]
52
+ def self.register(atom_hash)
53
+ atom = Unitwise::Atom.new(atom_hash)
54
+ atom.validate!
55
+ Unitwise::Atom.all.push(atom)
56
+ Unitwise::Expression::Decomposer.send(:reset)
57
+ atom
58
+ end
59
+
47
60
  # The system path for the installed gem
48
61
  # @api private
49
62
  def self.path
@@ -86,32 +86,26 @@ module ScaleTests
86
86
  end
87
87
 
88
88
  describe "#simplified_value" do
89
- describe "when the value is equivalent to an Integer" do
90
- it "should convert from a Rational" do
91
- result = described_class.new(Rational(20,2), 'foot').simplified_value
92
- result.must_equal 10
93
- result.must_be_kind_of(Integer)
94
- end
95
- it "should convert from a Float" do
96
- result = described_class.new(4.0, 'foot').simplified_value
97
- result.must_equal 4
98
- result.must_be_kind_of(Integer)
99
- end
100
- it "should convert from a BigDecimal" do
101
- result = described_class.new(BigDecimal("4.5"), "volt").simplified_value
102
- result.must_equal 4.5
103
- result.must_be_kind_of(Float)
104
- end
89
+ it "must simplify to an Integer" do
90
+ result = described_class.new(4.0, 'foot').simplified_value
91
+ result.must_equal 4
92
+ result.must_be_kind_of(Integer)
105
93
  end
106
- describe "when the value is equivalent to a Float" do
107
- it "should convert from a BigDecimal" do
108
- result = described_class.new(BigDecimal("1.5"), 'foot').simplified_value
109
- result.must_equal 1.5
110
- result.must_be_kind_of(Float)
111
- end
94
+
95
+ it "must simplify to a Float" do
96
+ result = described_class.new(BigDecimal("1.5"), 'foot').simplified_value
97
+ result.must_equal 1.5
98
+ result.must_be_kind_of(Float)
112
99
  end
113
100
  end
114
101
 
102
+ describe "#inspect" do
103
+ it "must show the unit and value" do
104
+ result = described_class.new(12, 'meter').inspect
105
+ result.must_include("value=12")
106
+ result.must_include("unit=meter")
107
+ end
108
+ end
115
109
  end
116
110
  end
117
111
  end
data/test/test_helper.rb CHANGED
@@ -9,9 +9,8 @@ require 'minitest/pride'
9
9
  require 'unitwise'
10
10
 
11
11
  module Minitest::Assertions
12
- def assert_almost_equal(expected, actual)
12
+ def assert_almost_equal(expected, actual, range=0.0001)
13
13
  message = "Expected #{actual} to be almost equal to #{expected}"
14
- range = 0.00001
15
14
  assert expected + range > actual && expected - range < actual, message
16
15
  end
17
16
  end