unitwise-193 1.0.4

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +21 -0
  5. data/CHANGELOG.md +44 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +297 -0
  9. data/Rakefile +21 -0
  10. data/data/base_unit.yaml +43 -0
  11. data/data/derived_unit.yaml +3542 -0
  12. data/data/prefix.yaml +121 -0
  13. data/lib/unitwise.rb +70 -0
  14. data/lib/unitwise/atom.rb +121 -0
  15. data/lib/unitwise/base.rb +58 -0
  16. data/lib/unitwise/compatible.rb +60 -0
  17. data/lib/unitwise/errors.rb +7 -0
  18. data/lib/unitwise/expression.rb +35 -0
  19. data/lib/unitwise/expression/composer.rb +41 -0
  20. data/lib/unitwise/expression/decomposer.rb +68 -0
  21. data/lib/unitwise/expression/matcher.rb +47 -0
  22. data/lib/unitwise/expression/parser.rb +58 -0
  23. data/lib/unitwise/expression/transformer.rb +37 -0
  24. data/lib/unitwise/ext.rb +2 -0
  25. data/lib/unitwise/ext/numeric.rb +45 -0
  26. data/lib/unitwise/functional.rb +117 -0
  27. data/lib/unitwise/measurement.rb +198 -0
  28. data/lib/unitwise/prefix.rb +24 -0
  29. data/lib/unitwise/scale.rb +139 -0
  30. data/lib/unitwise/search.rb +46 -0
  31. data/lib/unitwise/standard.rb +29 -0
  32. data/lib/unitwise/standard/base.rb +73 -0
  33. data/lib/unitwise/standard/base_unit.rb +21 -0
  34. data/lib/unitwise/standard/derived_unit.rb +49 -0
  35. data/lib/unitwise/standard/extras.rb +17 -0
  36. data/lib/unitwise/standard/function.rb +35 -0
  37. data/lib/unitwise/standard/prefix.rb +17 -0
  38. data/lib/unitwise/standard/scale.rb +25 -0
  39. data/lib/unitwise/term.rb +142 -0
  40. data/lib/unitwise/unit.rb +181 -0
  41. data/lib/unitwise/version.rb +3 -0
  42. data/test/support/scale_tests.rb +117 -0
  43. data/test/test_helper.rb +19 -0
  44. data/test/unitwise/atom_test.rb +129 -0
  45. data/test/unitwise/base_test.rb +6 -0
  46. data/test/unitwise/expression/decomposer_test.rb +45 -0
  47. data/test/unitwise/expression/matcher_test.rb +42 -0
  48. data/test/unitwise/expression/parser_test.rb +109 -0
  49. data/test/unitwise/ext/numeric_test.rb +54 -0
  50. data/test/unitwise/functional_test.rb +17 -0
  51. data/test/unitwise/measurement_test.rb +233 -0
  52. data/test/unitwise/prefix_test.rb +25 -0
  53. data/test/unitwise/scale_test.rb +7 -0
  54. data/test/unitwise/search_test.rb +18 -0
  55. data/test/unitwise/term_test.rb +55 -0
  56. data/test/unitwise/unit_test.rb +87 -0
  57. data/test/unitwise_test.rb +35 -0
  58. data/unitwise.gemspec +33 -0
  59. metadata +246 -0
@@ -0,0 +1,41 @@
1
+ module Unitwise
2
+ module Expression
3
+ # Composer creates string expressions for arrays of terms, following
4
+ # UCUM's conventions.
5
+ class Composer
6
+ attr_reader :terms, :mode
7
+ def initialize(terms, mode)
8
+ @terms = terms
9
+ @mode = mode || :primary_code
10
+ end
11
+
12
+ def set
13
+ @set ||= terms.reduce(SignedMultiset.new) do |s, t|
14
+ identifier = { :f => t.factor,
15
+ :p => (t.prefix.to_s(mode) if t.prefix),
16
+ :a => (t.atom.to_s(mode) if t.atom) }
17
+ s.increment(identifier, t.exponent); s
18
+ end
19
+ end
20
+
21
+ def numerator
22
+ @numerator ||= set.select{ |_, v| v > 0 }.map do |k, v|
23
+ "#{ k[:f] if k[:f] != 1 }#{ k[:p] }#{ k[:a] }#{ v if v != 1 }"
24
+ end.select { |t| !t.empty? }.join('.')
25
+ end
26
+
27
+ def denominator
28
+ @denominator ||= set.select{ |_, v| v < 0 }.map do |k, v|
29
+ "#{ k[:f] if k[:f] != 1 }#{ k[:p] }#{ k[:a] }#{ -v if v != -1 }"
30
+ end.select { |t| !t.empty? }.join('.')
31
+ end
32
+
33
+ def expression
34
+ @expression = []
35
+ @expression << (numerator.empty? ? '1' : numerator)
36
+ (@expression << denominator) unless denominator.empty?
37
+ @expression.join('/')
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,68 @@
1
+ module Unitwise
2
+ module Expression
3
+ # The decomposer is used to turn string expressions into collections of
4
+ # terms. It is responsible for executing the parsing and transformation
5
+ # of a string, as well as caching the results.
6
+ class Decomposer
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
15
+
16
+ class << self
17
+
18
+ # Parse an expression to an array of terms and cache the results
19
+ def parse(expression)
20
+ expression = expression.to_s
21
+ if cache.key?(expression)
22
+ cache[expression]
23
+ elsif decomposer = new(expression)
24
+ cache[expression] = decomposer
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # A simple cache to prevent re-decomposing the same units
31
+ # api private
32
+ def cache
33
+ @cache ||= {}
34
+ end
35
+ end
36
+
37
+ attr_reader :expression, :mode
38
+
39
+ def initialize(expression)
40
+ @expression = expression.to_s
41
+ if expression.empty? || terms.nil? || terms.empty?
42
+ fail(ExpressionError, "Could not evaluate '#{ expression }'.")
43
+ end
44
+ end
45
+
46
+ def parse
47
+ PARSERS.reduce(nil) do |_, (mode, parser)|
48
+ parsed = parser.parse(expression) rescue next
49
+ @mode = mode
50
+ break parsed
51
+ end
52
+ end
53
+
54
+ def transform
55
+ @transform ||= TRANSFORMER.apply(parse, :mode => mode)
56
+ end
57
+
58
+ def terms
59
+ @terms ||= if transform.respond_to?(:terms)
60
+ transform.terms
61
+ else
62
+ Array(transform)
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,47 @@
1
+ module Unitwise
2
+ module Expression
3
+ # Matcher is responsible for building up Parslet alternatives of atoms and
4
+ # prefixes to be used by Unitwise::Expression::Parser.
5
+ class Matcher
6
+ class << self
7
+ def atom(mode)
8
+ @atom ||= {}
9
+ @atom[mode] ||= new(Atom.all, mode).alternative
10
+ end
11
+
12
+ def metric_atom(mode)
13
+ @metric_atom ||= {}
14
+ @metric_atom[mode] ||=
15
+ new(Atom.all.select(&:metric?), mode).alternative
16
+ end
17
+
18
+ def prefix(mode)
19
+ @prefix ||= {}
20
+ @prefix[mode] ||= new(Prefix.all, mode).alternative
21
+ end
22
+ end
23
+
24
+ attr_reader :collection, :mode
25
+
26
+ def initialize(collection, mode = :primary_code)
27
+ @collection = collection
28
+ @mode = mode
29
+ end
30
+
31
+ def strings
32
+ collection.map(&mode).flatten.compact.sort do |x, y|
33
+ y.length <=> x.length
34
+ end
35
+ end
36
+
37
+ def matchers
38
+ strings.map { |s| Parslet::Atoms::Str.new(s) }
39
+ end
40
+
41
+ def alternative
42
+ Parslet::Atoms::Alternative.new(*matchers)
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,58 @@
1
+ module Unitwise
2
+ module Expression
3
+ # Parses a string expression into a hash tree representing the
4
+ # expression's terms, prefixes, and atoms.
5
+ class Parser < Parslet::Parser
6
+ attr_reader :key
7
+ def initialize(key = :primary_code)
8
+ @key = key
9
+ end
10
+
11
+ root :expression
12
+
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) }
16
+
17
+ rule (:simpleton) do
18
+ (prefix.as(:prefix) >> metric_atom.as(:atom) | atom.as(:atom))
19
+ end
20
+
21
+ rule (:annotation) do
22
+ str('{') >> match['^}'].repeat.as(:annotation) >> str('}')
23
+ end
24
+
25
+ rule (:digits) { match['0-9'].repeat(1) }
26
+
27
+ rule (:integer) { (str('-').maybe >> digits).as(:integer) }
28
+
29
+ rule (:fixnum) do
30
+ (str('-').maybe >> digits >> str('.') >> digits).as(:fixnum)
31
+ end
32
+
33
+ rule (:number) { fixnum | integer }
34
+
35
+ rule (:exponent) { integer.as(:exponent) }
36
+
37
+ rule (:factor) { number.as(:factor) }
38
+
39
+ rule (:operator) { (str('.') | str('/')).as(:operator) }
40
+
41
+ rule (:term) do
42
+ ((factor >> simpleton | simpleton | factor) >>
43
+ exponent.maybe >> annotation.maybe).as(:term)
44
+ end
45
+
46
+ rule (:group) do
47
+ (factor.maybe >> str('(') >> expression.as(:nested) >> str(')') >>
48
+ exponent.maybe).as(:group)
49
+ end
50
+
51
+ rule (:expression) do
52
+ ((group | term).as(:left)).maybe >>
53
+ (operator >> expression.as(:right)).maybe
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,37 @@
1
+ module Unitwise
2
+ module Expression
3
+ # Transformer is responsible for turning a Unitwise::Expression::Parser
4
+ # hash result into a collection of Unitwise::Terms.
5
+ class Transformer < Parslet::Transform
6
+
7
+ rule(:integer => simple(:i)) { i.to_i }
8
+ rule(:fixnum => simple(:f)) { f.to_f }
9
+
10
+ rule(:prefix_code => simple(:c)) { |x| Prefix.find(x[:c], x[:mode]) }
11
+ rule(:atom_code => simple(:c)) { |x| Atom.find(x[:c], x[:mode]) }
12
+ rule(:term => subtree(:h)) { Term.new(h) }
13
+
14
+ rule(:operator => simple(:o), :right => simple(:r)) do
15
+ o == '/' ? r ** -1 : r
16
+ end
17
+
18
+ rule(:left => simple(:l), :operator => simple(:o),
19
+ :right => simple(:r)) do
20
+ o == '/' ? l / r : l * r
21
+ end
22
+
23
+ rule(:left => simple(:l)) { l }
24
+
25
+ rule(:group => { :factor => simple(:f),
26
+ :nested => simple(:n), :exponent => simple(:e) }) do
27
+ (n ** e) * f
28
+ end
29
+
30
+ rule(:group => { :nested => simple(:n) , :exponent => simple(:e) }) do
31
+ n ** e
32
+ end
33
+
34
+ rule(:group => { :nested => simple(:n) }) { n }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,2 @@
1
+ require 'unitwise'
2
+ require 'unitwise/ext/numeric'
@@ -0,0 +1,45 @@
1
+ # Unitwise extends Numeric to add these dyanmic method conveniences: `1.meter`,
2
+ # `26.2.to_mile`, and `4.convert_to("Joule")`. These overrides are optional due
3
+ # to their controversial nature. `require 'unitwise/ext'` to enable them.
4
+ class Numeric
5
+ # Converts numeric to a measurement
6
+ # @param unit [Unitwise::Unit, String] The unit to use in the measurement
7
+ # @return [Unitwise::Measurement]
8
+ # @example
9
+ # 26.2.convert_to('mile') # => #<Unitwise::Measurement 1 mile>
10
+ # @api public
11
+ def convert_to(unit)
12
+ Unitwise::Measurement.new(self, unit)
13
+ end
14
+
15
+ # Converts numeric to a measurement by the method name
16
+ # @example
17
+ # 26.2.mile # => #<Unitwise::Measurement 26.2 mile>
18
+ # 100.to_foot # => #<Unitwise::Measurement 100 foot>
19
+ # @api semipublic
20
+ def method_missing(meth, *args, &block)
21
+ if args.empty? && !block_given?
22
+ unit = (match = /\Ato_(\w+)\Z/.match(meth.to_s)) ? match[1] : meth
23
+ converted = begin
24
+ convert_to(unit)
25
+ rescue Unitwise::ExpressionError
26
+ nil
27
+ end
28
+ end
29
+ if converted
30
+ Numeric.define_unit_conversion_methods_for(unit)
31
+ converted
32
+ else
33
+ super(meth, *args, &block)
34
+ end
35
+ end
36
+
37
+ def self.define_unit_conversion_methods_for(name)
38
+ [name.to_sym, "to_#{ name }".to_sym].each do |meth|
39
+ next if method_defined?(meth)
40
+ define_method meth do
41
+ convert_to(name)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,117 @@
1
+ module Unitwise
2
+ # Functional is an alterative function-based scale for atoms with a
3
+ # non-linear (or non-zero y-intercept) scale. This is most commonly used for
4
+ # temperatures. Known functions for converting to and from special atoms
5
+ # are setup as class methods here.
6
+ class Functional < Scale
7
+ extend Math
8
+
9
+ def self.to_cel(x)
10
+ x - 273.15
11
+ end
12
+
13
+ def self.from_cel(x)
14
+ x + 273.15
15
+ end
16
+
17
+ def self.to_degf(x)
18
+ 9.0 * x / 5.0 - 459.67
19
+ end
20
+
21
+ def self.from_degf(x)
22
+ 5.0 / 9.0 * (x + 459.67)
23
+ end
24
+
25
+ def self.to_hpX(x)
26
+ -log10(x)
27
+ end
28
+
29
+ def self.from_hpX(x)
30
+ 10.0 ** -x
31
+ end
32
+
33
+ def self.to_hpC(x)
34
+ -log(x) / log(100.0)
35
+ end
36
+
37
+ def self.from_hpC(x)
38
+ 100.0 ** -x
39
+ end
40
+
41
+ def self.to_tan100(x)
42
+ 100.0 * tan(x)
43
+ end
44
+
45
+ def self.from_tan100(x)
46
+ atan(x / 100.0)
47
+ end
48
+
49
+ def self.to_ph(x)
50
+ to_hpX(x)
51
+ end
52
+
53
+ def self.from_ph(x)
54
+ from_hpX(x)
55
+ end
56
+
57
+ def self.to_ld(x)
58
+ Math.log(x) / Math.log(2.0)
59
+ end
60
+
61
+ def self.from_ld(x)
62
+ 2.0 ** x
63
+ end
64
+
65
+ def self.to_ln(x)
66
+ log(x)
67
+ end
68
+
69
+ def self.from_ln(x)
70
+ Math::E ** x
71
+ end
72
+
73
+ def self.to_lg(x)
74
+ log10(x)
75
+ end
76
+
77
+ def self.from_lg(x)
78
+ 10.0 ** x
79
+ end
80
+
81
+ def self.to_2lg(x)
82
+ 2.0 * log10(x)
83
+ end
84
+
85
+ def self.from_2lg(x)
86
+ 10.0 ** (x / 2.0)
87
+ end
88
+
89
+ attr_reader :function_name
90
+
91
+ # Setup a new functional.
92
+ # @param value [Numeric] The magnitude of the scale
93
+ # @param unit [Unitwise::Unit, String] The unit of the scale
94
+ # @param function_name [String, Symbol] One of the class methods above to be
95
+ # used for conversion
96
+ def initialize(value, unit, function_name)
97
+ @function_name = function_name
98
+ super(value, unit)
99
+ end
100
+
101
+ # Get the equivalent scalar value of a magnitude on this scale
102
+ # @param magnitude [Numeric] The magnitude to find the scalar value for
103
+ # @return [Numeric] Equivalent linear scalar value
104
+ # @api public
105
+ def scalar(magnitude = value)
106
+ self.class.send(:"from_#{function_name}", magnitude)
107
+ end
108
+
109
+ # Get the equivalent magnitude on this scale for a scalar value
110
+ # @param scalar [Numeric] A linear scalar value
111
+ # @return [Numeric] The equivalent magnitude on this scale
112
+ # @api public
113
+ def magnitude(scalar = scalar())
114
+ self.class.send(:"to_#{function_name}", scalar)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,198 @@
1
+ module Unitwise
2
+ # A Measurement is a combination of a numeric value and a unit. You can think
3
+ # of this as a type of vector where the direction is the unit designation and
4
+ # the value is the magnitude. This is the primary class that outside code
5
+ # will interact with. Comes with conversion, comparison, and math methods.
6
+ class Measurement < Scale
7
+ # Create a new Measurement
8
+ # @param value [Numeric] The scalar value for the measurement
9
+ # @param unit [String, Measurement::Unit] Either a string expression, or a
10
+ # Measurement::Unit
11
+ # @example
12
+ # Unitwise::Measurement.new(20, 'm/s')
13
+ # @api public
14
+ def initialize(*args)
15
+ super(*args)
16
+ terms
17
+ end
18
+
19
+ # Convert this measurement to a compatible unit.
20
+ # @param other_unit [String, Measurement::Unit] Either a string expression
21
+ # or a Measurement::Unit
22
+ # @example
23
+ # measurement1.convert_to('foot')
24
+ # measurement2.convert_to('kilogram')
25
+ # @api public
26
+ def convert_to(other_unit)
27
+ other_unit = Unit.new(other_unit)
28
+ if compatible_with?(other_unit)
29
+ new(converted_value(other_unit), other_unit)
30
+ else
31
+ fail ConversionError, "Can't convert #{self} to #{other_unit}."
32
+ end
33
+ end
34
+
35
+ # Multiply this measurement by a number or another measurement
36
+ # @param other [Numeric, Unitwise::Measurement]
37
+ # @example
38
+ # measurent * 5
39
+ # measurement * some_other_measurement
40
+ # @api public
41
+ def *(other)
42
+ operate(:*, other) ||
43
+ fail(TypeError, "Can't multiply #{self} by #{other}.")
44
+ end
45
+
46
+ # Divide this measurement by a number or another measurement
47
+ # @param (see #*)
48
+ # @example
49
+ # measurement / 2
50
+ # measurement / some_other_measurement
51
+ # @api public
52
+ def /(other)
53
+ operate(:/, other) || fail(TypeError, "Can't divide #{self} by #{other}")
54
+ end
55
+
56
+ # Add another measurement to this unit. Units must be compatible.
57
+ # @param other [Unitwise::Measurement]
58
+ # @example
59
+ # measurement + some_other_measurement
60
+ # @api public
61
+ def +(other)
62
+ combine(:+, other) || fail(TypeError, "Can't add #{other} to #{self}.")
63
+ end
64
+
65
+ # Subtract another measurement from this unit. Units must be compatible.
66
+ # @param (see #+)
67
+ # @example
68
+ # measurement - some_other_measurement
69
+ # @api public
70
+ def -(other)
71
+ combine(:-, other) ||
72
+ fail(TypeError, "Can't subtract #{other} from #{self}.")
73
+ end
74
+
75
+ # Raise a measurement to a numeric power.
76
+ # @param number [Numeric]
77
+ # @example
78
+ # measurement ** 2
79
+ # @api public
80
+ def **(other)
81
+ if other.is_a?(Numeric)
82
+ new(value ** other, unit ** other)
83
+ else
84
+ fail TypeError, "Can't raise #{self} to #{other} power."
85
+ end
86
+ end
87
+
88
+ # Round the measurement value. Delegates to the value's class.
89
+ # @return [Integer, Float]
90
+ # @api public
91
+ def round(digits = nil)
92
+ rounded_value = digits ? value.round(digits) : value.round
93
+ self.class.new(rounded_value, unit)
94
+ end
95
+
96
+ # Coerce a numeric to a a measurement for mathematical operations
97
+ # @param other [Numeric]
98
+ # @example
99
+ # 2.5 * measurement
100
+ # 4 / measurement
101
+ # @api public
102
+ def coerce(other)
103
+ case other
104
+ when Numeric
105
+ return self.class.new(other, '1'), self
106
+ else
107
+ fail TypeError, "#{self.class} can't be coerced into #{other.class}"
108
+ end
109
+ end
110
+
111
+ # Convert a measurement to an Integer.
112
+ # @example
113
+ # measurement.to_i # => 4
114
+ # @api public
115
+ def to_i
116
+ Integer(value)
117
+ end
118
+
119
+ # Convert a measurement to a Float.
120
+ # @example
121
+ # measurement.to_f # => 4.25
122
+ # @api public
123
+ def to_f
124
+ Float(value)
125
+ end
126
+
127
+ # Convert a measurement to a Rational.
128
+ # @example
129
+ # measurement.to_r # => (17/4)
130
+ # @api public
131
+ def to_r
132
+ Rational(value)
133
+ end
134
+
135
+ # Will attempt to convert to a unit by method name.
136
+ # @example
137
+ # measurement.to_foot # => <Unitwise::Measurement 4 foot>
138
+ # @api semipublic
139
+ def method_missing(meth, *args, &block)
140
+ if args.empty? && !block_given? && (match = /\Ato_(\w+)\Z/.match(meth.to_s))
141
+ begin
142
+ convert_to(match[1])
143
+ rescue ExpressionError
144
+ super(meth, *args, &block)
145
+ end
146
+ else
147
+ super(meth, *args, &block)
148
+ end
149
+ end
150
+
151
+ private
152
+
153
+ # Helper method to create a new instance from this instance
154
+ # @api private
155
+ def new(*args)
156
+ self.class.new(*args)
157
+ end
158
+
159
+ # Set the value for the measurement.
160
+ # @api private
161
+ attr_writer :value
162
+
163
+ # Determine value of the unit after conversion to another unit
164
+ # @api private
165
+ def converted_value(other_unit)
166
+ if other_unit.special?
167
+ other_unit.magnitude scalar
168
+ else
169
+ scalar / other_unit.scalar
170
+ end
171
+ end
172
+
173
+ # Add or subtract other unit
174
+ # @api private
175
+ def combine(operator, other)
176
+ if other.respond_to?(:composition) && compatible_with?(other)
177
+ new(value.send(operator, other.convert_to(unit).value), unit)
178
+ end
179
+ end
180
+
181
+ # Multiply or divide other unit
182
+ # @api private
183
+ def operate(operator, other)
184
+ if other.is_a?(Numeric)
185
+ new(value.send(operator, other), unit)
186
+ elsif other.respond_to?(:composition)
187
+ if compatible_with?(other)
188
+ converted = other.convert_to(unit)
189
+ new(value.send(operator, converted.value),
190
+ unit.send(operator, converted.unit))
191
+ else
192
+ new(value.send(operator, other.value),
193
+ unit.send(operator, other.unit))
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end