unitwise-193 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +44 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +297 -0
- data/Rakefile +21 -0
- data/data/base_unit.yaml +43 -0
- data/data/derived_unit.yaml +3542 -0
- data/data/prefix.yaml +121 -0
- data/lib/unitwise.rb +70 -0
- data/lib/unitwise/atom.rb +121 -0
- data/lib/unitwise/base.rb +58 -0
- data/lib/unitwise/compatible.rb +60 -0
- data/lib/unitwise/errors.rb +7 -0
- data/lib/unitwise/expression.rb +35 -0
- data/lib/unitwise/expression/composer.rb +41 -0
- data/lib/unitwise/expression/decomposer.rb +68 -0
- data/lib/unitwise/expression/matcher.rb +47 -0
- data/lib/unitwise/expression/parser.rb +58 -0
- data/lib/unitwise/expression/transformer.rb +37 -0
- data/lib/unitwise/ext.rb +2 -0
- data/lib/unitwise/ext/numeric.rb +45 -0
- data/lib/unitwise/functional.rb +117 -0
- data/lib/unitwise/measurement.rb +198 -0
- data/lib/unitwise/prefix.rb +24 -0
- data/lib/unitwise/scale.rb +139 -0
- data/lib/unitwise/search.rb +46 -0
- data/lib/unitwise/standard.rb +29 -0
- data/lib/unitwise/standard/base.rb +73 -0
- data/lib/unitwise/standard/base_unit.rb +21 -0
- data/lib/unitwise/standard/derived_unit.rb +49 -0
- data/lib/unitwise/standard/extras.rb +17 -0
- data/lib/unitwise/standard/function.rb +35 -0
- data/lib/unitwise/standard/prefix.rb +17 -0
- data/lib/unitwise/standard/scale.rb +25 -0
- data/lib/unitwise/term.rb +142 -0
- data/lib/unitwise/unit.rb +181 -0
- data/lib/unitwise/version.rb +3 -0
- data/test/support/scale_tests.rb +117 -0
- data/test/test_helper.rb +19 -0
- data/test/unitwise/atom_test.rb +129 -0
- data/test/unitwise/base_test.rb +6 -0
- data/test/unitwise/expression/decomposer_test.rb +45 -0
- data/test/unitwise/expression/matcher_test.rb +42 -0
- data/test/unitwise/expression/parser_test.rb +109 -0
- data/test/unitwise/ext/numeric_test.rb +54 -0
- data/test/unitwise/functional_test.rb +17 -0
- data/test/unitwise/measurement_test.rb +233 -0
- data/test/unitwise/prefix_test.rb +25 -0
- data/test/unitwise/scale_test.rb +7 -0
- data/test/unitwise/search_test.rb +18 -0
- data/test/unitwise/term_test.rb +55 -0
- data/test/unitwise/unit_test.rb +87 -0
- data/test/unitwise_test.rb +35 -0
- data/unitwise.gemspec +33 -0
- 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
|
data/lib/unitwise/ext.rb
ADDED
@@ -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
|