unitwise-193 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
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