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.
- 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
|