ruby-units 1.3.0.a → 1.3.1
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.
- data/CHANGELOG.txt +212 -0
- data/Gemfile +8 -0
- data/Manifest.txt +19 -0
- data/RakeFile +83 -0
- data/VERSION +1 -0
- data/lib/ruby-units.rb +14 -0
- data/lib/ruby_units.rb +14 -0
- data/lib/ruby_units/array.rb +9 -0
- data/lib/ruby_units/cache.rb +20 -0
- data/lib/ruby_units/date.rb +53 -0
- data/lib/ruby_units/fixnum.rb +20 -0
- data/lib/ruby_units/math.rb +101 -0
- data/lib/ruby_units/numeric.rb +8 -0
- data/lib/ruby_units/object.rb +8 -0
- data/lib/ruby_units/string.rb +126 -0
- data/lib/ruby_units/time.rb +71 -0
- data/lib/ruby_units/unit.rb +1272 -0
- data/lib/ruby_units/unit_definitions.rb +248 -0
- data/lib/ruby_units/version.rb +5 -0
- data/ruby-units.gemspec +83 -0
- data/spec/ruby-units/string_spec.rb +1 -5
- data/spec/ruby-units/unit_spec.rb +34 -1
- metadata +27 -18
@@ -0,0 +1,20 @@
|
|
1
|
+
# this patch is necessary for ruby 1.8 because cases where
|
2
|
+
# Integers are divided by Units don't work quite right
|
3
|
+
|
4
|
+
if RUBY_VERSION < "1.9"
|
5
|
+
class Fixnum
|
6
|
+
alias quo_without_units quo
|
7
|
+
|
8
|
+
def quo_with_units(other)
|
9
|
+
case other
|
10
|
+
when Unit
|
11
|
+
self * other.inverse
|
12
|
+
else
|
13
|
+
quo_without_units(other)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
alias quo quo_with_units
|
18
|
+
alias / quo_with_units
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# Math will convert unit objects to radians and then attempt to use the value for
|
2
|
+
# trigonometric functions.
|
3
|
+
require 'mathn'
|
4
|
+
|
5
|
+
module Math
|
6
|
+
|
7
|
+
alias :unit_sqrt :sqrt
|
8
|
+
def sqrt(n)
|
9
|
+
if Unit === n
|
10
|
+
(n**(Rational(1,2))).to_unit
|
11
|
+
else
|
12
|
+
unit_sqrt(n)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
module_function :unit_sqrt
|
16
|
+
module_function :sqrt
|
17
|
+
|
18
|
+
#:nocov:
|
19
|
+
if self.respond_to?(:cbrt)
|
20
|
+
alias :unit_cbrt :cbrt
|
21
|
+
def cbrt(n)
|
22
|
+
if Unit === n
|
23
|
+
(n**(Rational(1,3))).to_unit
|
24
|
+
else
|
25
|
+
unit_cbrt(n)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
module_function :unit_cbrt
|
29
|
+
module_function :cbrt
|
30
|
+
end
|
31
|
+
#:nocov:
|
32
|
+
|
33
|
+
alias :unit_sin :sin
|
34
|
+
def sin(n)
|
35
|
+
Unit === n ? unit_sin(n.to('radian').scalar) : unit_sin(n)
|
36
|
+
end
|
37
|
+
module_function :unit_sin
|
38
|
+
module_function :sin
|
39
|
+
|
40
|
+
alias :unit_cos :cos
|
41
|
+
def cos(n)
|
42
|
+
Unit === n ? unit_cos(n.to('radian').scalar) : unit_cos(n)
|
43
|
+
end
|
44
|
+
module_function :unit_cos
|
45
|
+
module_function :cos
|
46
|
+
|
47
|
+
alias :unit_sinh :sinh
|
48
|
+
def sinh(n)
|
49
|
+
Unit === n ? unit_sinh(n.to('radian').scalar) : unit_sinh(n)
|
50
|
+
end
|
51
|
+
module_function :unit_sinh
|
52
|
+
module_function :sinh
|
53
|
+
|
54
|
+
alias :unit_cosh :cosh
|
55
|
+
def cosh(n)
|
56
|
+
Unit === n ? unit_cosh(n.to('radian').scalar) : unit_cosh(n)
|
57
|
+
end
|
58
|
+
module_function :unit_cosh
|
59
|
+
module_function :cosh
|
60
|
+
|
61
|
+
alias :unit_tan :tan
|
62
|
+
def tan(n)
|
63
|
+
Unit === n ? unit_tan(n.to('radian').scalar) : unit_tan(n)
|
64
|
+
end
|
65
|
+
module_function :tan
|
66
|
+
module_function :unit_tan
|
67
|
+
|
68
|
+
alias :unit_tanh :tanh
|
69
|
+
def tanh(n)
|
70
|
+
Unit === n ? unit_tanh(n.to('radian').scalar) : unit_tanh(n)
|
71
|
+
end
|
72
|
+
module_function :unit_tanh
|
73
|
+
module_function :tanh
|
74
|
+
|
75
|
+
alias :unit_hypot :hypot
|
76
|
+
# Convert parameters to consistent units and perform the function
|
77
|
+
def hypot(x,y)
|
78
|
+
if Unit === x && Unit === y
|
79
|
+
(x**2 + y**2)**(1/2)
|
80
|
+
else
|
81
|
+
unit_hypot(x,y)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
module_function :unit_hypot
|
85
|
+
module_function :hypot
|
86
|
+
|
87
|
+
alias :unit_atan2 :atan2
|
88
|
+
def atan2(x,y)
|
89
|
+
case
|
90
|
+
when (x.is_a?(Unit) && y.is_a?(Unit)) && (x !~ y)
|
91
|
+
raise ArgumentError, "Incompatible Units"
|
92
|
+
when (x.is_a?(Unit) && y.is_a?(Unit)) && (x =~ y)
|
93
|
+
Math::unit_atan2(x.base_scalar, y.base_scalar)
|
94
|
+
else
|
95
|
+
Math::unit_atan2(x,y)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
module_function :unit_atan2
|
99
|
+
module_function :atan2
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'time'
|
2
|
+
# make a string into a unit
|
3
|
+
class String
|
4
|
+
def to_unit(other = nil)
|
5
|
+
other ? Unit.new(self).to(other) : Unit.new(self)
|
6
|
+
end
|
7
|
+
alias :unit :to_unit
|
8
|
+
alias :u :to_unit
|
9
|
+
alias :unit_format :%
|
10
|
+
|
11
|
+
# format unit output using formating codes '%0.2f' % '1 mm'.unit => '1.00 mm'
|
12
|
+
def %(*args)
|
13
|
+
return "" if self.empty?
|
14
|
+
case
|
15
|
+
when args.first.is_a?(Unit)
|
16
|
+
args.first.to_s(self)
|
17
|
+
when (!defined?(Uncertain).nil? && args.first.is_a?(Uncertain))
|
18
|
+
args.first.to_s(self)
|
19
|
+
when args.first.is_a?(Complex)
|
20
|
+
args.first.to_s
|
21
|
+
else
|
22
|
+
unit_format(*args)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#needed for compatibility with Rails, which defines a String.from method
|
27
|
+
if self.public_instance_methods.include? 'from'
|
28
|
+
alias :old_from :from
|
29
|
+
end
|
30
|
+
|
31
|
+
# "5 min".from("now")
|
32
|
+
def from(time_point = ::Time.now)
|
33
|
+
return old_from(time_point) if self.respond_to?(:old_from) && time_point.instance_of?(Integer)
|
34
|
+
self.unit.from(time_point)
|
35
|
+
end
|
36
|
+
|
37
|
+
alias :after :from
|
38
|
+
|
39
|
+
def from_now
|
40
|
+
self.from('now')
|
41
|
+
end
|
42
|
+
|
43
|
+
# "5 min".ago
|
44
|
+
def ago
|
45
|
+
self.unit.ago
|
46
|
+
end
|
47
|
+
|
48
|
+
def before(time_point = ::Time.now)
|
49
|
+
self.unit.before(time_point)
|
50
|
+
end
|
51
|
+
|
52
|
+
def before_now
|
53
|
+
self.before('now')
|
54
|
+
end
|
55
|
+
|
56
|
+
def since(time_point = ::Time.now)
|
57
|
+
self.unit.since(time_point)
|
58
|
+
end
|
59
|
+
|
60
|
+
def until(time_point = ::Time.now)
|
61
|
+
self.unit.until(time_point)
|
62
|
+
end
|
63
|
+
|
64
|
+
def to(other)
|
65
|
+
self.unit.to(other)
|
66
|
+
end
|
67
|
+
|
68
|
+
def time(options = {})
|
69
|
+
self.to_time(options) rescue self.to_datetime(options)
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_time(options = {})
|
73
|
+
begin
|
74
|
+
#raises exception when Chronic not defined or when it returns a nil (i.e., can't parse the input)
|
75
|
+
r = Chronic.parse(self,options)
|
76
|
+
raise(ArgumentError, 'Invalid Time String') unless r
|
77
|
+
return r
|
78
|
+
rescue
|
79
|
+
case
|
80
|
+
when self == "now"
|
81
|
+
Time.now
|
82
|
+
when Time.respond_to?(:parse)
|
83
|
+
Time.parse(self)
|
84
|
+
else
|
85
|
+
Time.local(*ParseDate.parsedate(self))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_datetime(options = {})
|
91
|
+
begin
|
92
|
+
# raises an exception if Chronic.parse = nil or if Chronic not defined
|
93
|
+
r = Chronic.parse(self,options).send(:to_datetime)
|
94
|
+
rescue Exception => e
|
95
|
+
r = case
|
96
|
+
when self.to_s == "now"
|
97
|
+
DateTime.now
|
98
|
+
else
|
99
|
+
DateTime.parse(self)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
raise RuntimeError, "Invalid Time String (#{self.to_s})" if r == DateTime.new
|
103
|
+
return r
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_date(options={})
|
107
|
+
begin
|
108
|
+
r = Chronic.parse(self,options).to_date
|
109
|
+
rescue
|
110
|
+
r = case
|
111
|
+
when self == "today"
|
112
|
+
Date.today
|
113
|
+
when RUBY_VERSION < "1.9"
|
114
|
+
Date.civil(*ParseDate.parsedate(self)[0..5].compact)
|
115
|
+
else
|
116
|
+
Date.parse(self)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
raise RuntimeError, 'Invalid Date String' if r == Date.new
|
120
|
+
return r
|
121
|
+
end
|
122
|
+
|
123
|
+
def datetime(options = {})
|
124
|
+
self.to_datetime(options) rescue self.to_time(options)
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
#
|
2
|
+
# Time math is handled slightly differently. The difference is considered to be an exact duration if
|
3
|
+
# the subtracted value is in hours, minutes, or seconds. It is rounded to the nearest day if the offset
|
4
|
+
# is in years, decades, or centuries. This leads to less precise values, but ones that match the
|
5
|
+
# calendar better.
|
6
|
+
class Time
|
7
|
+
|
8
|
+
class << self
|
9
|
+
alias unit_time_at at
|
10
|
+
end
|
11
|
+
|
12
|
+
# Convert a duration to a Time value by considering the duration to be the number of seconds since the
|
13
|
+
# epoch
|
14
|
+
def self.at(arg)
|
15
|
+
case arg
|
16
|
+
when Unit
|
17
|
+
unit_time_at(arg.to("s").scalar)
|
18
|
+
else
|
19
|
+
unit_time_at(arg)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_unit(other = nil)
|
24
|
+
other ? Unit.new(self).to(other) : Unit.new(self)
|
25
|
+
end
|
26
|
+
alias :unit :to_unit
|
27
|
+
alias :u :to_unit
|
28
|
+
alias :unit_add :+
|
29
|
+
|
30
|
+
unless Time.instance_methods.include?(:to_date)
|
31
|
+
def to_date
|
32
|
+
x=(Date.civil(1970,1,1)+((self.to_f+self.gmt_offset)/86400.0)-0.5)
|
33
|
+
Date.civil(x.year, x.month, x.day)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def +(other)
|
38
|
+
case other
|
39
|
+
when Unit
|
40
|
+
other = other.to('d').round.to('s') if ['y', 'decade', 'century'].include? other.units
|
41
|
+
begin
|
42
|
+
unit_add(other.to('s').scalar)
|
43
|
+
rescue RangeError
|
44
|
+
self.to_datetime + other
|
45
|
+
end
|
46
|
+
else
|
47
|
+
unit_add(other)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# usage: Time.in '5 min'
|
52
|
+
def self.in(duration)
|
53
|
+
Time.now + duration.to_unit
|
54
|
+
end
|
55
|
+
|
56
|
+
alias :unit_sub :-
|
57
|
+
|
58
|
+
def -(other)
|
59
|
+
case other
|
60
|
+
when Unit
|
61
|
+
other = other.to('d').round.to('s') if ['y', 'decade', 'century'].include? other.units
|
62
|
+
begin
|
63
|
+
unit_sub(other.to('s').scalar)
|
64
|
+
rescue RangeError
|
65
|
+
self.send(:to_datetime) - other
|
66
|
+
end
|
67
|
+
else
|
68
|
+
unit_sub(other)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,1272 @@
|
|
1
|
+
require 'date'
|
2
|
+
if RUBY_VERSION < "1.9"
|
3
|
+
require 'parsedate'
|
4
|
+
require 'rational'
|
5
|
+
end
|
6
|
+
# = Ruby Units
|
7
|
+
#
|
8
|
+
# Copyright 2006-2011 by Kevin C. Olbrich, Ph.D.
|
9
|
+
#
|
10
|
+
# See http://rubyforge.org/ruby-units/
|
11
|
+
#
|
12
|
+
# http://www.sciwerks.org
|
13
|
+
#
|
14
|
+
# mailto://kevin.olbrich+ruby-units@gmail.com
|
15
|
+
#
|
16
|
+
# See README for detailed usage instructions and examples
|
17
|
+
#
|
18
|
+
# ==Unit Definition Format
|
19
|
+
#
|
20
|
+
# '<name>' => [%w{prefered_name synonyms}, conversion_to_base, :classification, %w{<base> <units> <in> <numerator>} , %w{<base> <units> <in> <denominator>} ],
|
21
|
+
#
|
22
|
+
# Prefixes (e.g., a :prefix classification) get special handling
|
23
|
+
# Note: The accuracy of unit conversions depends on the precision of the conversion factor.
|
24
|
+
# If you have more accurate estimates for particular conversion factors, please send them
|
25
|
+
# to me and I will incorporate them into the next release. It is also incumbent on the end-user
|
26
|
+
# to ensure that the accuracy of any conversions is sufficient for their intended application.
|
27
|
+
#
|
28
|
+
# While there are a large number of unit specified in the base package,
|
29
|
+
# there are also a large number of units that are not included.
|
30
|
+
# This package covers nearly all SI, Imperial, and units commonly used
|
31
|
+
# in the United States. If your favorite units are not listed here, send me an email
|
32
|
+
#
|
33
|
+
# To add / override a unit definition, add a code block like this..
|
34
|
+
#
|
35
|
+
# class Unit < Numeric
|
36
|
+
# @@USER_DEFINITIONS = {
|
37
|
+
# <name>' => [%w{prefered_name synonyms}, conversion_to_base, :classification, %w{<base> <units> <in> <numerator>} , %w{<base> <units> <in> <denominator>} ]
|
38
|
+
# }
|
39
|
+
# end
|
40
|
+
# Unit.setup
|
41
|
+
class Unit < Numeric
|
42
|
+
# pre-generate hashes from unit definitions for performance.
|
43
|
+
VERSION = Unit::Version::STRING
|
44
|
+
@@USER_DEFINITIONS = {}
|
45
|
+
@@PREFIX_VALUES = {}
|
46
|
+
@@PREFIX_MAP = {}
|
47
|
+
@@UNIT_MAP = {}
|
48
|
+
@@UNIT_VALUES = {}
|
49
|
+
@@OUTPUT_MAP = {}
|
50
|
+
@@BASE_UNITS = ['<meter>','<kilogram>','<second>','<mole>', '<farad>', '<ampere>','<radian>','<kelvin>','<temp-K>','<byte>','<dollar>','<candela>','<each>','<steradian>','<decibel>']
|
51
|
+
UNITY = '<1>'
|
52
|
+
UNITY_ARRAY= [UNITY]
|
53
|
+
FEET_INCH_REGEX = /(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/
|
54
|
+
TIME_REGEX = /(\d+)*:(\d+)*:*(\d+)*[:,]*(\d+)*/
|
55
|
+
LBS_OZ_REGEX = /(\d+)\s*(?:#|lbs|pounds|pound-mass)+[\s,]*(\d+)\s*(?:oz|ounces)/
|
56
|
+
SCI_NUMBER = %r{([+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*)}
|
57
|
+
RATIONAL_NUMBER = /([+-]?\d+)\/(\d+)/
|
58
|
+
COMPLEX_NUMBER = /#{SCI_NUMBER}?#{SCI_NUMBER}i\b/
|
59
|
+
NUMBER_REGEX = /#{SCI_NUMBER}*\s*(.+)?/
|
60
|
+
UNIT_STRING_REGEX = /#{SCI_NUMBER}*\s*([^\/]*)\/*(.+)*/
|
61
|
+
TOP_REGEX = /([^ \*]+)(?:\^|\*\*)([\d-]+)/
|
62
|
+
BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/
|
63
|
+
UNCERTAIN_REGEX = /#{SCI_NUMBER}\s*\+\/-\s*#{SCI_NUMBER}\s(.+)/
|
64
|
+
COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(.+)?/
|
65
|
+
RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(.+)?/
|
66
|
+
KELVIN = ['<kelvin>']
|
67
|
+
FAHRENHEIT = ['<fahrenheit>']
|
68
|
+
RANKINE = ['<rankine>']
|
69
|
+
CELSIUS = ['<celsius>']
|
70
|
+
TEMP_REGEX = /(?:temp|deg)[CFRK]/
|
71
|
+
|
72
|
+
SIGNATURE_VECTOR = [:length, :time, :temperature, :mass, :current, :substance, :luminosity, :currency, :memory, :angle, :capacitance]
|
73
|
+
@@KINDS = {
|
74
|
+
-312058=>:resistance,
|
75
|
+
-312038=>:inductance,
|
76
|
+
-152040=>:magnetism,
|
77
|
+
-152038=>:magnetism,
|
78
|
+
-152058=>:potential,
|
79
|
+
-39=>:acceleration,
|
80
|
+
-38=>:radiation,
|
81
|
+
-20=>:frequency,
|
82
|
+
-19=>:speed,
|
83
|
+
-18=>:viscosity,
|
84
|
+
0=>:unitless,
|
85
|
+
1=>:length,
|
86
|
+
2=>:area,
|
87
|
+
3=>:volume,
|
88
|
+
20=>:time,
|
89
|
+
400=>:temperature,
|
90
|
+
7942=>:power,
|
91
|
+
7959=>:pressure,
|
92
|
+
7962=>:energy,
|
93
|
+
7979=>:viscosity,
|
94
|
+
7961=>:force,
|
95
|
+
7997=>:mass_concentration,
|
96
|
+
8000=>:mass,
|
97
|
+
159999=>:magnetism,
|
98
|
+
160000=>:current,
|
99
|
+
160020=>:charge,
|
100
|
+
312058=>:resistance,
|
101
|
+
3199980=>:activity,
|
102
|
+
3199997=>:molar_concentration,
|
103
|
+
3200000=>:substance,
|
104
|
+
63999998=>:illuminance,
|
105
|
+
64000000=>:luminous_power,
|
106
|
+
1280000000=>:currency,
|
107
|
+
25600000000=>:memory,
|
108
|
+
511999999980=>:angular_velocity,
|
109
|
+
512000000000=>:angle,
|
110
|
+
10240000000000=>:capacitance,
|
111
|
+
}
|
112
|
+
|
113
|
+
@@cached_units = {}
|
114
|
+
@@base_unit_cache = {}
|
115
|
+
|
116
|
+
def self.setup
|
117
|
+
@@ALL_UNIT_DEFINITIONS = UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS)
|
118
|
+
for unit in (@@ALL_UNIT_DEFINITIONS) do
|
119
|
+
key, value = unit
|
120
|
+
if value[2] == :prefix then
|
121
|
+
@@PREFIX_VALUES[key]=value[1]
|
122
|
+
for name in value[0] do
|
123
|
+
@@PREFIX_MAP[name]=key
|
124
|
+
end
|
125
|
+
else
|
126
|
+
@@UNIT_VALUES[key]={}
|
127
|
+
@@UNIT_VALUES[key][:scalar]=value[1]
|
128
|
+
@@UNIT_VALUES[key][:numerator]=value[3] if value[3]
|
129
|
+
@@UNIT_VALUES[key][:denominator]=value[4] if value[4]
|
130
|
+
for name in value[0] do
|
131
|
+
@@UNIT_MAP[name]=key
|
132
|
+
end
|
133
|
+
end
|
134
|
+
@@OUTPUT_MAP[key]=value[0][0]
|
135
|
+
end
|
136
|
+
@@PREFIX_REGEX = @@PREFIX_MAP.keys.sort_by {|prefix| [prefix.length, prefix]}.reverse.join('|')
|
137
|
+
@@UNIT_REGEX = @@UNIT_MAP.keys.sort_by {|unit_name| [unit_name.length, unit]}.reverse.join('|')
|
138
|
+
@@UNIT_MATCH_REGEX = /(#{@@PREFIX_REGEX})*?(#{@@UNIT_REGEX})\b/
|
139
|
+
Unit.new(1)
|
140
|
+
end
|
141
|
+
|
142
|
+
include Comparable
|
143
|
+
attr_accessor :scalar, :numerator, :denominator, :signature, :base_scalar, :base_numerator, :base_denominator, :output, :unit_name
|
144
|
+
|
145
|
+
def to_yaml_properties
|
146
|
+
%w{@scalar @numerator @denominator @signature @base_scalar}
|
147
|
+
end
|
148
|
+
|
149
|
+
# needed to make complex units play nice -- otherwise not detected as a complex_generic
|
150
|
+
|
151
|
+
def kind_of?(klass)
|
152
|
+
self.scalar.kind_of?(klass)
|
153
|
+
end
|
154
|
+
|
155
|
+
def copy(from)
|
156
|
+
@scalar = from.scalar
|
157
|
+
@numerator = from.numerator
|
158
|
+
@denominator = from.denominator
|
159
|
+
@is_base = from.is_base?
|
160
|
+
@signature = from.signature
|
161
|
+
@base_scalar = from.base_scalar
|
162
|
+
@unit_name = from.unit_name rescue nil
|
163
|
+
end
|
164
|
+
|
165
|
+
# basically a copy of the basic to_yaml. Needed because otherwise it ends up coercing the object to a string
|
166
|
+
# before YAML'izing it.
|
167
|
+
if RUBY_VERSION < "1.9"
|
168
|
+
def to_yaml( opts = {} )
|
169
|
+
YAML::quick_emit( object_id, opts ) do |out|
|
170
|
+
out.map( taguri, to_yaml_style ) do |map|
|
171
|
+
for m in to_yaml_properties do
|
172
|
+
map.add( m[1..-1], instance_variable_get( m ) )
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Create a new Unit object. Can be initialized using a String, a Hash, an Array, Time, DateTime
|
180
|
+
# Valid formats include:
|
181
|
+
# "5.6 kg*m/s^2"
|
182
|
+
# "5.6 kg*m*s^-2"
|
183
|
+
# "5.6 kilogram*meter*second^-2"
|
184
|
+
# "2.2 kPa"
|
185
|
+
# "37 degC"
|
186
|
+
# "1" -- creates a unitless constant with value 1
|
187
|
+
# "GPa" -- creates a unit with scalar 1 with units 'GPa'
|
188
|
+
# 6'4" -- recognized as 6 feet + 4 inches
|
189
|
+
# 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
|
190
|
+
#
|
191
|
+
def initialize(*options)
|
192
|
+
@scalar = nil
|
193
|
+
@base_scalar = nil
|
194
|
+
@unit_name = nil
|
195
|
+
@signature = nil
|
196
|
+
@output = {}
|
197
|
+
if options.size == 2
|
198
|
+
# options[0] is the scalar
|
199
|
+
# options[1] is a unit string
|
200
|
+
begin
|
201
|
+
cached = @@cached_units[options[1]] * options[0]
|
202
|
+
copy(cached)
|
203
|
+
rescue
|
204
|
+
initialize("#{options[0]} #{(options[1].units rescue options[1])}")
|
205
|
+
end
|
206
|
+
return
|
207
|
+
end
|
208
|
+
if options.size == 3
|
209
|
+
options[1] = options[1].join if options[1].kind_of?(Array)
|
210
|
+
options[2] = options[2].join if options[2].kind_of?(Array)
|
211
|
+
begin
|
212
|
+
cached = @@cached_units["#{options[1]}/#{options[2]}"] * options[0]
|
213
|
+
copy(cached)
|
214
|
+
rescue
|
215
|
+
initialize("#{options[0]} #{options[1]}/#{options[2]}")
|
216
|
+
end
|
217
|
+
return
|
218
|
+
end
|
219
|
+
|
220
|
+
case options[0]
|
221
|
+
when Hash
|
222
|
+
@scalar = options[0][:scalar] || 1
|
223
|
+
@numerator = options[0][:numerator] || UNITY_ARRAY
|
224
|
+
@denominator = options[0][:denominator] || UNITY_ARRAY
|
225
|
+
@signature = options[0][:signature]
|
226
|
+
when Array
|
227
|
+
initialize(*options[0])
|
228
|
+
return
|
229
|
+
when Numeric
|
230
|
+
@scalar = options[0]
|
231
|
+
@numerator = @denominator = UNITY_ARRAY
|
232
|
+
when Time
|
233
|
+
@scalar = options[0].to_f
|
234
|
+
@numerator = ['<second>']
|
235
|
+
@denominator = UNITY_ARRAY
|
236
|
+
when DateTime, Date
|
237
|
+
@scalar = options[0].ajd
|
238
|
+
@numerator = ['<day>']
|
239
|
+
@denominator = UNITY_ARRAY
|
240
|
+
when /^\s*$/
|
241
|
+
raise ArgumentError, "No Unit Specified"
|
242
|
+
when String
|
243
|
+
parse(options[0])
|
244
|
+
else
|
245
|
+
raise ArgumentError, "Invalid Unit Format"
|
246
|
+
end
|
247
|
+
self.update_base_scalar
|
248
|
+
raise ArgumentError, "Temperatures must not be less than absolute zero" if self.is_temperature? && self.base_scalar < 0
|
249
|
+
|
250
|
+
unary_unit = self.units || ""
|
251
|
+
if options.first.instance_of?(String)
|
252
|
+
opt_scalar, opt_units = Unit.parse_into_numbers_and_units(options[0])
|
253
|
+
unless @@cached_units.keys.include?(opt_units) || (opt_units =~ /(#{TEMP_REGEX})|(pounds|lbs[ ,]\d+ ounces|oz)|('\d+")|(ft|feet[ ,]\d+ in|inch|inches)|%|(#{TIME_REGEX})|i\s?(.+)?|±|\+\/-/)
|
254
|
+
@@cached_units[opt_units] = (self.scalar == 1 ? self : opt_units.unit) if opt_units && !opt_units.empty?
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /#{TEMP_REGEX}/) then
|
259
|
+
@@cached_units[unary_unit] = (self.scalar == 1 ? self : unary_unit.unit)
|
260
|
+
end
|
261
|
+
|
262
|
+
[@scalar, @numerator, @denominator, @base_scalar, @signature, @is_base].each {|x| x.freeze}
|
263
|
+
self
|
264
|
+
end
|
265
|
+
|
266
|
+
def kind
|
267
|
+
return @@KINDS[self.signature]
|
268
|
+
end
|
269
|
+
|
270
|
+
def self.cached
|
271
|
+
return @@cached_units
|
272
|
+
end
|
273
|
+
|
274
|
+
def self.clear_cache
|
275
|
+
@@cached_units = {}
|
276
|
+
@@base_unit_cache = {}
|
277
|
+
Unit.new(1)
|
278
|
+
end
|
279
|
+
|
280
|
+
def self.base_unit_cache
|
281
|
+
return @@base_unit_cache
|
282
|
+
end
|
283
|
+
|
284
|
+
#
|
285
|
+
# parse strings like "1 minute in seconds"
|
286
|
+
#
|
287
|
+
def self.parse(input)
|
288
|
+
first, second = input.scan(/(.+)\s(?:in|to|as)\s(.+)/i).first
|
289
|
+
second.nil? ? first.unit : first.unit.to(second)
|
290
|
+
end
|
291
|
+
|
292
|
+
def to_unit
|
293
|
+
self
|
294
|
+
end
|
295
|
+
alias :unit :to_unit
|
296
|
+
|
297
|
+
# Returns 'true' if the Unit is represented in base units
|
298
|
+
def is_base?
|
299
|
+
return @is_base if defined? @is_base
|
300
|
+
return @is_base=true if self.degree? && self.numerator.size == 1 && self.denominator == UNITY_ARRAY && self.units =~ /(?:deg|temp)K/
|
301
|
+
n = @numerator + @denominator
|
302
|
+
for x in n.compact do
|
303
|
+
return @is_base=false unless x == UNITY || (@@BASE_UNITS.include?((x)))
|
304
|
+
end
|
305
|
+
return @is_base = true
|
306
|
+
end
|
307
|
+
alias :base? :is_base?
|
308
|
+
|
309
|
+
# convert to base SI units
|
310
|
+
# results of the conversion are cached so subsequent calls to this will be fast
|
311
|
+
def to_base
|
312
|
+
return self if self.is_base?
|
313
|
+
if self.units =~ /\A(?:temp|deg)[CRF]\Z/
|
314
|
+
if RUBY_VERSION < "1.9"
|
315
|
+
@signature = @@KINDS.index(:temperature)
|
316
|
+
else
|
317
|
+
#:nocov:
|
318
|
+
@signature = @@KINDS.key(:temperature)
|
319
|
+
#:nocov:
|
320
|
+
end
|
321
|
+
base = case
|
322
|
+
when self.is_temperature?
|
323
|
+
self.to('tempK')
|
324
|
+
when self.is_degree?
|
325
|
+
self.to('degK')
|
326
|
+
end
|
327
|
+
return base
|
328
|
+
end
|
329
|
+
|
330
|
+
cached = ((@@base_unit_cache[self.units] * self.scalar) rescue nil)
|
331
|
+
return cached if cached
|
332
|
+
|
333
|
+
num = []
|
334
|
+
den = []
|
335
|
+
q = 1
|
336
|
+
for unit in @numerator.compact do
|
337
|
+
if @@PREFIX_VALUES[unit]
|
338
|
+
q *= @@PREFIX_VALUES[unit]
|
339
|
+
else
|
340
|
+
q *= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit]
|
341
|
+
num << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator]
|
342
|
+
den << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator]
|
343
|
+
end
|
344
|
+
end
|
345
|
+
for unit in @denominator.compact do
|
346
|
+
if @@PREFIX_VALUES[unit]
|
347
|
+
q /= @@PREFIX_VALUES[unit]
|
348
|
+
else
|
349
|
+
q /= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit]
|
350
|
+
den << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator]
|
351
|
+
num << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator]
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
num = num.flatten.compact
|
356
|
+
den = den.flatten.compact
|
357
|
+
num = UNITY_ARRAY if num.empty?
|
358
|
+
base= Unit.new(Unit.eliminate_terms(q,num,den))
|
359
|
+
@@base_unit_cache[self.units]=base
|
360
|
+
return base * @scalar
|
361
|
+
end
|
362
|
+
alias :base :to_base
|
363
|
+
|
364
|
+
# Generate human readable output.
|
365
|
+
# If the name of a unit is passed, the unit will first be converted to the target unit before output.
|
366
|
+
# some named conversions are available
|
367
|
+
#
|
368
|
+
# :ft - outputs in feet and inches (e.g., 6'4")
|
369
|
+
# :lbs - outputs in pounds and ounces (e.g, 8 lbs, 8 oz)
|
370
|
+
#
|
371
|
+
# You can also pass a standard format string (i.e., '%0.2f')
|
372
|
+
# or a strftime format string.
|
373
|
+
#
|
374
|
+
# output is cached so subsequent calls for the same format will be fast
|
375
|
+
#
|
376
|
+
def to_s(target_units=nil)
|
377
|
+
out = @output[target_units]
|
378
|
+
if out
|
379
|
+
return out
|
380
|
+
else
|
381
|
+
case target_units
|
382
|
+
when :ft
|
383
|
+
inches = self.to("in").scalar.to_int
|
384
|
+
out = "#{(inches / 12).truncate}\'#{(inches % 12).round}\""
|
385
|
+
when :lbs
|
386
|
+
ounces = self.to("oz").scalar.to_int
|
387
|
+
out = "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz"
|
388
|
+
when String
|
389
|
+
out = case target_units
|
390
|
+
when /(%[\-+\.\w#]+)\s*(.+)*/ #format string like '%0.2f in'
|
391
|
+
begin
|
392
|
+
if $2 #unit specified, need to convert
|
393
|
+
self.to($2).to_s($1)
|
394
|
+
else
|
395
|
+
"#{$1 % @scalar} #{$2 || self.units}".strip
|
396
|
+
end
|
397
|
+
rescue
|
398
|
+
(DateTime.new(0) + self).strftime(target_units)
|
399
|
+
end
|
400
|
+
when /(\S+)/ #unit only 'mm' or '1/mm'
|
401
|
+
"#{self.to($1).to_s}"
|
402
|
+
else
|
403
|
+
raise "unhandled case"
|
404
|
+
end
|
405
|
+
else
|
406
|
+
out = case @scalar
|
407
|
+
when Rational
|
408
|
+
"#{@scalar} #{self.units}"
|
409
|
+
else
|
410
|
+
"#{'%g' % @scalar} #{self.units}"
|
411
|
+
end.strip
|
412
|
+
end
|
413
|
+
@output[target_units] = out
|
414
|
+
return out
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
# Normally pretty prints the unit, but if you really want to see the guts of it, pass ':dump'
|
419
|
+
def inspect(option=nil)
|
420
|
+
return super() if option == :dump
|
421
|
+
self.to_s
|
422
|
+
end
|
423
|
+
|
424
|
+
# true if unit is a 'temperature', false if a 'degree' or anything else
|
425
|
+
def is_temperature?
|
426
|
+
self.is_degree? && (!(self.units =~ /temp[CFRK]/).nil?)
|
427
|
+
end
|
428
|
+
alias :temperature? :is_temperature?
|
429
|
+
|
430
|
+
# true if a degree unit or equivalent.
|
431
|
+
def is_degree?
|
432
|
+
self.kind == :temperature
|
433
|
+
end
|
434
|
+
alias :degree? :is_degree?
|
435
|
+
|
436
|
+
# returns the 'degree' unit associated with a temperature unit
|
437
|
+
# '100 tempC'.unit.temperature_scale #=> 'degC'
|
438
|
+
def temperature_scale
|
439
|
+
return nil unless self.is_temperature?
|
440
|
+
self.units =~ /temp([CFRK])/
|
441
|
+
"deg#{$1}"
|
442
|
+
end
|
443
|
+
|
444
|
+
# returns true if no associated units
|
445
|
+
# false, even if the units are "unitless" like 'radians, each, etc'
|
446
|
+
def unitless?
|
447
|
+
(@numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY)
|
448
|
+
end
|
449
|
+
|
450
|
+
# Compare two Unit objects. Throws an exception if they are not of compatible types.
|
451
|
+
# Comparisons are done based on the value of the unit in base SI units.
|
452
|
+
def <=>(other)
|
453
|
+
case
|
454
|
+
when !self.base_scalar.respond_to?(:<=>)
|
455
|
+
raise NoMethodError, "undefined method `<=>' for #{self.base_scalar.inspect}"
|
456
|
+
when !self.is_temperature? && other.zero?
|
457
|
+
return self.base_scalar <=> 0
|
458
|
+
when other.instance_of?(Unit)
|
459
|
+
raise ArgumentError, "Incompatible Units (#{self.units} !~ #{other.units})" unless self =~ other
|
460
|
+
return self.base_scalar <=> other.base_scalar
|
461
|
+
else
|
462
|
+
x,y = coerce(other)
|
463
|
+
return x <=> y
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
# Compare Units for equality
|
468
|
+
# this is necessary mostly for Complex units. Complex units do not have a <=> operator
|
469
|
+
# so we define this one here so that we can properly check complex units for equality.
|
470
|
+
# Units of incompatible types are not equal, except when they are both zero and neither is a temperature
|
471
|
+
# Equality checks can be tricky since round off errors may make essentially equivalent units
|
472
|
+
# appear to be different.
|
473
|
+
def ==(other)
|
474
|
+
case
|
475
|
+
when other.respond_to?(:zero?) && other.zero?
|
476
|
+
return self.zero?
|
477
|
+
when other.instance_of?(Unit)
|
478
|
+
return false unless self =~ other
|
479
|
+
return self.base_scalar == other.base_scalar
|
480
|
+
else
|
481
|
+
x,y = coerce(other)
|
482
|
+
return x == y
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
# check to see if units are compatible, but not the scalar part
|
487
|
+
# this check is done by comparing signatures for performance reasons
|
488
|
+
# if passed a string, it will create a unit object with the string and then do the comparison
|
489
|
+
# this permits a syntax like:
|
490
|
+
# unit =~ "mm"
|
491
|
+
# if you want to do a regexp on the unit string do this ...
|
492
|
+
# unit.units =~ /regexp/
|
493
|
+
def =~(other)
|
494
|
+
case other
|
495
|
+
when Unit
|
496
|
+
self.signature == other.signature
|
497
|
+
else
|
498
|
+
x,y = coerce(other)
|
499
|
+
x =~ y
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
alias :compatible? :=~
|
504
|
+
alias :compatible_with? :=~
|
505
|
+
|
506
|
+
# Compare two units. Returns true if quantities and units match
|
507
|
+
#
|
508
|
+
# Unit("100 cm") === Unit("100 cm") # => true
|
509
|
+
# Unit("100 cm") === Unit("1 m") # => false
|
510
|
+
def ===(other)
|
511
|
+
case other
|
512
|
+
when Unit
|
513
|
+
(self.scalar == other.scalar) && (self.units == other.units)
|
514
|
+
else
|
515
|
+
x,y = coerce(other)
|
516
|
+
x === y
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
alias :same? :===
|
521
|
+
alias :same_as? :===
|
522
|
+
|
523
|
+
# Add two units together. Result is same units as receiver and scalar and base_scalar are updated appropriately
|
524
|
+
# throws an exception if the units are not compatible.
|
525
|
+
# It is possible to add Time objects to units of time
|
526
|
+
def +(other)
|
527
|
+
case other
|
528
|
+
when Unit
|
529
|
+
case
|
530
|
+
when self.zero?
|
531
|
+
other.dup
|
532
|
+
when self =~ other
|
533
|
+
raise ArgumentError, "Cannot add two temperatures" if ([self, other].all? {|x| x.is_temperature?})
|
534
|
+
if [self, other].any? {|x| x.is_temperature?}
|
535
|
+
if self.is_temperature?
|
536
|
+
Unit.new(:scalar => (self.scalar + other.to(self.temperature_scale).scalar), :numerator => @numerator, :denominator=>@denominator, :signature => @signature)
|
537
|
+
else
|
538
|
+
Unit.new(:scalar => (other.scalar + self.to(other.temperature_scale).scalar), :numerator => other.numerator, :denominator=>other.denominator, :signature => other.signature)
|
539
|
+
end
|
540
|
+
else
|
541
|
+
@q ||= ((@@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar) rescue (self.units.unit.to_base.scalar))
|
542
|
+
Unit.new(:scalar=>(self.base_scalar + other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
|
543
|
+
end
|
544
|
+
else
|
545
|
+
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')"
|
546
|
+
end
|
547
|
+
when Date, Time
|
548
|
+
raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be added to a Unit"
|
549
|
+
else
|
550
|
+
x,y = coerce(other)
|
551
|
+
y + x
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
# Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately
|
556
|
+
# throws an exception if the units are not compatible.
|
557
|
+
def -(other)
|
558
|
+
case other
|
559
|
+
when Unit
|
560
|
+
case
|
561
|
+
when self.zero?
|
562
|
+
-other.dup
|
563
|
+
when self =~ other
|
564
|
+
case
|
565
|
+
when [self, other].all? {|x| x.is_temperature?}
|
566
|
+
Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => KELVIN, :denominator => UNITY_ARRAY, :signature => @signature).to(self.temperature_scale)
|
567
|
+
when self.is_temperature?
|
568
|
+
Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => ['<temp-K>'], :denominator => UNITY_ARRAY, :signature => @signature).to(self)
|
569
|
+
when other.is_temperature?
|
570
|
+
raise ArgumentError, "Cannot subtract a temperature from a differential degree unit"
|
571
|
+
else
|
572
|
+
@q ||= ((@@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar) rescue (self.units.unit.scalar/self.units.unit.to_base.scalar))
|
573
|
+
Unit.new(:scalar=>(self.base_scalar - other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature=>@signature)
|
574
|
+
end
|
575
|
+
else
|
576
|
+
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')"
|
577
|
+
end
|
578
|
+
when Time
|
579
|
+
raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be subtracted from to a Unit, which can only represent time spans"
|
580
|
+
else
|
581
|
+
x,y = coerce(other)
|
582
|
+
y-x
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
# Multiply two units.
|
587
|
+
def *(other)
|
588
|
+
case other
|
589
|
+
when Unit
|
590
|
+
raise ArgumentError, "Cannot multiply by temperatures" if [other,self].any? {|x| x.is_temperature?}
|
591
|
+
opts = Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator)
|
592
|
+
opts.merge!(:signature => @signature + other.signature)
|
593
|
+
Unit.new(opts)
|
594
|
+
when Numeric
|
595
|
+
Unit.new(:scalar=>@scalar*other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
|
596
|
+
else
|
597
|
+
x,y = coerce(other)
|
598
|
+
x * y
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
# Divide two units.
|
603
|
+
# Throws an exception if divisor is 0
|
604
|
+
def /(other)
|
605
|
+
case other
|
606
|
+
when Unit
|
607
|
+
raise ZeroDivisionError if other.zero?
|
608
|
+
raise ArgumentError, "Cannot divide with temperatures" if [other,self].any? {|x| x.is_temperature?}
|
609
|
+
opts = Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator)
|
610
|
+
opts.merge!(:signature=> @signature - other.signature)
|
611
|
+
Unit.new(opts)
|
612
|
+
when Numeric
|
613
|
+
raise ZeroDivisionError if other.zero?
|
614
|
+
Unit.new(:scalar=>@scalar/other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
|
615
|
+
else
|
616
|
+
x,y = coerce(other)
|
617
|
+
y / x
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
# divide two units and return quotient and remainder
|
622
|
+
# when both units are in the same units we just use divmod on the raw scalars
|
623
|
+
# otherwise we use the scalar of the base unit which will be a float
|
624
|
+
def divmod(other)
|
625
|
+
raise ArgumentError, "Incompatible Units" unless self =~ other
|
626
|
+
if self.units == other.units
|
627
|
+
return self.scalar.divmod(other.scalar)
|
628
|
+
else
|
629
|
+
return self.to_base.scalar.divmod(other.to_base.scalar)
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
# perform a modulo on a unit, will raise an exception if the units are not compatible
|
634
|
+
def %(other)
|
635
|
+
self.divmod(other).last
|
636
|
+
end
|
637
|
+
|
638
|
+
# Exponentiate. Only takes integer powers.
|
639
|
+
# Note that anything raised to the power of 0 results in a Unit object with a scalar of 1, and no units.
|
640
|
+
# Throws an exception if exponent is not an integer.
|
641
|
+
# Ideally this routine should accept a float for the exponent
|
642
|
+
# It should then convert the float to a rational and raise the unit by the numerator and root it by the denominator
|
643
|
+
# but, sadly, floats can't be converted to rationals.
|
644
|
+
#
|
645
|
+
# For now, if a rational is passed in, it will be used, otherwise we are stuck with integers and certain floats < 1
|
646
|
+
def **(other)
|
647
|
+
raise ArgumentError, "Cannot raise a temperature to a power" if self.is_temperature?
|
648
|
+
if other.kind_of?(Numeric)
|
649
|
+
return self.inverse if other == -1
|
650
|
+
return self if other == 1
|
651
|
+
return 1 if other.zero?
|
652
|
+
end
|
653
|
+
case other
|
654
|
+
when Rational
|
655
|
+
self.power(other.numerator).root(other.denominator)
|
656
|
+
when Integer
|
657
|
+
self.power(other)
|
658
|
+
when Float
|
659
|
+
return self**(other.to_i) if other == other.to_i
|
660
|
+
valid = (1..9).map {|x| 1/x}
|
661
|
+
raise ArgumentError, "Not a n-th root (1..9), use 1/n" unless valid.include? other.abs
|
662
|
+
self.root((1/other).to_int)
|
663
|
+
when Complex
|
664
|
+
raise ArgumentError, "exponentiation of complex numbers is not yet supported."
|
665
|
+
else
|
666
|
+
raise ArgumentError, "Invalid Exponent"
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
# returns the unit raised to the n-th power. Integers only
|
671
|
+
def power(n)
|
672
|
+
raise ArgumentError, "Cannot raise a temperature to a power" if self.is_temperature?
|
673
|
+
raise ArgumentError, "Exponent must an Integer" unless n.kind_of?(Integer)
|
674
|
+
return self.inverse if n == -1
|
675
|
+
return 1 if n.zero?
|
676
|
+
return self if n == 1
|
677
|
+
if n > 0 then
|
678
|
+
(1..(n-1).to_i).inject(self) {|product, x| product * self}
|
679
|
+
else
|
680
|
+
(1..-(n-1).to_i).inject(self) {|product, x| product / self}
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
# Calculates the n-th root of a unit, where n = (1..9)
|
685
|
+
# if n < 0, returns 1/unit^(1/n)
|
686
|
+
def root(n)
|
687
|
+
raise ArgumentError, "Cannot take the root of a temperature" if self.is_temperature?
|
688
|
+
raise ArgumentError, "Exponent must an Integer" unless n.kind_of?(Integer)
|
689
|
+
raise ArgumentError, "0th root undefined" if n.zero?
|
690
|
+
return self if n == 1
|
691
|
+
return self.root(n.abs).inverse if n < 0
|
692
|
+
|
693
|
+
vec = self.unit_signature_vector
|
694
|
+
vec=vec.map {|x| x % n}
|
695
|
+
raise ArgumentError, "Illegal root" unless vec.max == 0
|
696
|
+
num = @numerator.dup
|
697
|
+
den = @denominator.dup
|
698
|
+
|
699
|
+
for item in @numerator.uniq do
|
700
|
+
x = num.find_all {|i| i==item}.size
|
701
|
+
r = ((x/n)*(n-1)).to_int
|
702
|
+
r.times {|y| num.delete_at(num.index(item))}
|
703
|
+
end
|
704
|
+
|
705
|
+
for item in @denominator.uniq do
|
706
|
+
x = den.find_all {|i| i==item}.size
|
707
|
+
r = ((x/n)*(n-1)).to_int
|
708
|
+
r.times {|y| den.delete_at(den.index(item))}
|
709
|
+
end
|
710
|
+
q = @scalar < 0 ? (-1)**Rational(1,n) * (@scalar.abs)**Rational(1,n) : @scalar**Rational(1,n)
|
711
|
+
Unit.new(:scalar=>q,:numerator=>num,:denominator=>den)
|
712
|
+
end
|
713
|
+
|
714
|
+
# returns inverse of Unit (1/unit)
|
715
|
+
def inverse
|
716
|
+
Unit("1") / self
|
717
|
+
end
|
718
|
+
|
719
|
+
# convert to a specified unit string or to the same units as another Unit
|
720
|
+
#
|
721
|
+
# unit >> "kg" will covert to kilograms
|
722
|
+
# unit1 >> unit2 converts to same units as unit2 object
|
723
|
+
#
|
724
|
+
# To convert a Unit object to match another Unit object, use:
|
725
|
+
# unit1 >>= unit2
|
726
|
+
# Throws an exception if the requested target units are incompatible with current Unit.
|
727
|
+
#
|
728
|
+
# Special handling for temperature conversions is supported. If the Unit object is converted
|
729
|
+
# from one temperature unit to another, the proper temperature offsets will be used.
|
730
|
+
# Supports Kelvin, Celsius, fahrenheit, and Rankine scales.
|
731
|
+
#
|
732
|
+
# Note that if temperature is part of a compound unit, the temperature will be treated as a differential
|
733
|
+
# and the units will be scaled appropriately.
|
734
|
+
def to(other)
|
735
|
+
return self if other.nil?
|
736
|
+
return self if TrueClass === other
|
737
|
+
return self if FalseClass === other
|
738
|
+
if (Unit === other && other.is_temperature?) || (String === other && other =~ /temp[CFRK]/)
|
739
|
+
raise ArgumentError, "Receiver is not a temperature unit" unless self.degree?
|
740
|
+
start_unit = self.units
|
741
|
+
target_unit = other.units rescue other
|
742
|
+
unless @base_scalar
|
743
|
+
@base_scalar = case start_unit
|
744
|
+
when 'tempC'
|
745
|
+
@scalar + 273.15
|
746
|
+
when 'tempK'
|
747
|
+
@scalar
|
748
|
+
when 'tempF'
|
749
|
+
(@scalar+459.67)*Rational(5,9)
|
750
|
+
when 'tempR'
|
751
|
+
@scalar*Rational(5,9)
|
752
|
+
end
|
753
|
+
end
|
754
|
+
q= case target_unit
|
755
|
+
when 'tempC'
|
756
|
+
@base_scalar - 273.15
|
757
|
+
when 'tempK'
|
758
|
+
@base_scalar
|
759
|
+
when 'tempF'
|
760
|
+
@base_scalar * Rational(9,5) - 459.67
|
761
|
+
when 'tempR'
|
762
|
+
@base_scalar * Rational(9,5)
|
763
|
+
end
|
764
|
+
Unit.new("#{q} #{target_unit}")
|
765
|
+
else
|
766
|
+
case other
|
767
|
+
when Unit
|
768
|
+
return self if other.units == self.units
|
769
|
+
target = other
|
770
|
+
when String
|
771
|
+
target = Unit.new(other)
|
772
|
+
else
|
773
|
+
raise ArgumentError, "Unknown target units"
|
774
|
+
end
|
775
|
+
raise ArgumentError, "Incompatible Units" unless self =~ target
|
776
|
+
_numerator1 = @numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact
|
777
|
+
_denominator1 = @denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact
|
778
|
+
_numerator2 = target.numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact
|
779
|
+
_denominator2 = target.denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact
|
780
|
+
|
781
|
+
# eliminate common terms
|
782
|
+
|
783
|
+
(_numerator1 & _denominator2).each do |common|
|
784
|
+
_numerator1.delete(common)
|
785
|
+
_denominator2.delete(common)
|
786
|
+
end
|
787
|
+
|
788
|
+
(_numerator2 & _denominator1).each do |common|
|
789
|
+
_numerator1.delete(common)
|
790
|
+
_denominator2.delete(common)
|
791
|
+
end
|
792
|
+
|
793
|
+
q = @scalar * ( (_numerator1 + _denominator2).inject(1) {|product,n| product*n} ) /
|
794
|
+
( (_numerator2 + _denominator1).inject(1) {|product,n| product*n} )
|
795
|
+
|
796
|
+
|
797
|
+
Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator, :signature => target.signature)
|
798
|
+
end
|
799
|
+
end
|
800
|
+
alias :>> :to
|
801
|
+
alias :convert_to :to
|
802
|
+
|
803
|
+
# converts the unit back to a float if it is unitless. Otherwise raises an exception
|
804
|
+
def to_f
|
805
|
+
return @scalar.to_f if self.unitless?
|
806
|
+
raise RuntimeError, "Cannot convert '#{self.to_s}' to Float unless unitless. Use Unit#scalar"
|
807
|
+
end
|
808
|
+
|
809
|
+
# converts the unit back to a complex if it is unitless. Otherwise raises an exception
|
810
|
+
def to_c
|
811
|
+
return Complex(@scalar) if self.unitless?
|
812
|
+
raise RuntimeError, "Cannot convert '#{self.to_s}' to Complex unless unitless. Use Unit#scalar"
|
813
|
+
end
|
814
|
+
|
815
|
+
# if unitless, returns an int, otherwise raises an error
|
816
|
+
def to_i
|
817
|
+
return @scalar.to_int if self.unitless?
|
818
|
+
raise RuntimeError, "Cannot convert '#{self.to_s}' to Integer unless unitless. Use Unit#scalar"
|
819
|
+
end
|
820
|
+
alias :to_int :to_i
|
821
|
+
|
822
|
+
# if unitless, returns a Rational, otherwise raises an error
|
823
|
+
def to_r
|
824
|
+
return @scalar.to_r if self.unitless?
|
825
|
+
raise RuntimeError, "Cannot convert '#{self.to_s}' to Rational unless unitless. Use Unit#scalar"
|
826
|
+
end
|
827
|
+
|
828
|
+
# returns the 'unit' part of the Unit object without the scalar
|
829
|
+
def units
|
830
|
+
return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
|
831
|
+
return @unit_name unless @unit_name.nil?
|
832
|
+
output_n = []
|
833
|
+
output_d =[]
|
834
|
+
num = @numerator.clone.compact
|
835
|
+
den = @denominator.clone.compact
|
836
|
+
if @numerator == UNITY_ARRAY
|
837
|
+
output_n << "1"
|
838
|
+
else
|
839
|
+
num.each_with_index do |token,index|
|
840
|
+
if token && @@PREFIX_VALUES[token] then
|
841
|
+
output_n << "#{@@OUTPUT_MAP[token]}#{@@OUTPUT_MAP[num[index+1]]}"
|
842
|
+
num[index+1]=nil
|
843
|
+
else
|
844
|
+
output_n << "#{@@OUTPUT_MAP[token]}" if token
|
845
|
+
end
|
846
|
+
end
|
847
|
+
end
|
848
|
+
if @denominator == UNITY_ARRAY
|
849
|
+
output_d = ['1']
|
850
|
+
else
|
851
|
+
den.each_with_index do |token,index|
|
852
|
+
if token && @@PREFIX_VALUES[token] then
|
853
|
+
output_d << "#{@@OUTPUT_MAP[token]}#{@@OUTPUT_MAP[den[index+1]]}"
|
854
|
+
den[index+1]=nil
|
855
|
+
else
|
856
|
+
output_d << "#{@@OUTPUT_MAP[token]}" if token
|
857
|
+
end
|
858
|
+
end
|
859
|
+
end
|
860
|
+
on = output_n.reject {|x| x.empty?}.map {|x| [x, output_n.find_all {|z| z==x}.size]}.uniq.map {|x| ("#{x[0]}".strip+ (x[1] > 1 ? "^#{x[1]}" : ''))}
|
861
|
+
od = output_d.reject {|x| x.empty?}.map {|x| [x, output_d.find_all {|z| z==x}.size]}.uniq.map {|x| ("#{x[0]}".strip+ (x[1] > 1 ? "^#{x[1]}" : ''))}
|
862
|
+
out = "#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip
|
863
|
+
@unit_name = out unless self.kind == :temperature
|
864
|
+
return out
|
865
|
+
end
|
866
|
+
|
867
|
+
# negates the scalar of the Unit
|
868
|
+
def -@
|
869
|
+
return -@scalar if self.unitless?
|
870
|
+
self.dup * -1
|
871
|
+
end
|
872
|
+
|
873
|
+
def abs
|
874
|
+
return @scalar.abs if self.unitless?
|
875
|
+
Unit.new(@scalar.abs, @numerator, @denominator)
|
876
|
+
end
|
877
|
+
|
878
|
+
def ceil
|
879
|
+
return @scalar.ceil if self.unitless?
|
880
|
+
Unit.new(@scalar.ceil, @numerator, @denominator)
|
881
|
+
end
|
882
|
+
|
883
|
+
def floor
|
884
|
+
return @scalar.floor if self.unitless?
|
885
|
+
Unit.new(@scalar.floor, @numerator, @denominator)
|
886
|
+
end
|
887
|
+
|
888
|
+
def round
|
889
|
+
return @scalar.round if self.unitless?
|
890
|
+
Unit.new(@scalar.round, @numerator, @denominator)
|
891
|
+
end
|
892
|
+
|
893
|
+
def truncate
|
894
|
+
return @scalar.truncate if self.unitless?
|
895
|
+
Unit.new(@scalar.truncate, @numerator, @denominator)
|
896
|
+
end
|
897
|
+
|
898
|
+
# returns next unit in a range. '1 mm'.unit.succ #=> '2 mm'.unit
|
899
|
+
# only works when the scalar is an integer
|
900
|
+
def succ
|
901
|
+
raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
|
902
|
+
Unit.new(@scalar.to_i.succ, @numerator, @denominator)
|
903
|
+
end
|
904
|
+
alias :next :succ
|
905
|
+
|
906
|
+
# returns next unit in a range. '1 mm'.unit.succ #=> '2 mm'.unit
|
907
|
+
# only works when the scalar is an integer
|
908
|
+
def pred
|
909
|
+
raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
|
910
|
+
Unit.new(@scalar.to_i.pred, @numerator, @denominator)
|
911
|
+
end
|
912
|
+
|
913
|
+
|
914
|
+
# Tries to make a Time object from current unit. Assumes the current unit hold the duration in seconds from the epoch.
|
915
|
+
def to_time
|
916
|
+
Time.at(self)
|
917
|
+
end
|
918
|
+
alias :time :to_time
|
919
|
+
|
920
|
+
|
921
|
+
# convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date
|
922
|
+
# defined by DateTime
|
923
|
+
def to_datetime
|
924
|
+
DateTime.new!(self.to('d').scalar)
|
925
|
+
end
|
926
|
+
|
927
|
+
def to_date
|
928
|
+
Date.new0(self.to('d').scalar)
|
929
|
+
end
|
930
|
+
|
931
|
+
|
932
|
+
# true if scalar is zero
|
933
|
+
def zero?
|
934
|
+
return self.base_scalar.zero?
|
935
|
+
end
|
936
|
+
|
937
|
+
# '5 min'.unit.ago
|
938
|
+
def ago
|
939
|
+
self.before
|
940
|
+
end
|
941
|
+
|
942
|
+
# '5 min'.before(time)
|
943
|
+
def before(time_point = ::Time.now)
|
944
|
+
raise ArgumentError, "Must specify a Time" unless time_point
|
945
|
+
if String === time_point
|
946
|
+
time_point.time - self rescue time_point.datetime - self
|
947
|
+
else
|
948
|
+
time_point - self rescue time_point.to_datetime - self
|
949
|
+
end
|
950
|
+
end
|
951
|
+
alias :before_now :before
|
952
|
+
|
953
|
+
# 'min'.since(time)
|
954
|
+
def since(time_point = ::Time.now)
|
955
|
+
case time_point
|
956
|
+
when Time
|
957
|
+
(Time.now - time_point).unit('s').to(self)
|
958
|
+
when DateTime, Date
|
959
|
+
(DateTime.now - time_point).unit('d').to(self)
|
960
|
+
when String
|
961
|
+
(DateTime.now - time_point.to_datetime(:context=>:past)).unit('d').to(self)
|
962
|
+
else
|
963
|
+
raise ArgumentError, "Must specify a Time, DateTime, or String"
|
964
|
+
end
|
965
|
+
end
|
966
|
+
|
967
|
+
# 'min'.until(time)
|
968
|
+
def until(time_point = ::Time.now)
|
969
|
+
case time_point
|
970
|
+
when Time
|
971
|
+
(time_point - Time.now).unit('s').to(self)
|
972
|
+
when DateTime, Date
|
973
|
+
(time_point - DateTime.now).unit('d').to(self)
|
974
|
+
when String
|
975
|
+
(time_point.to_datetime(:context=>:future) - DateTime.now).unit('d').to(self)
|
976
|
+
else
|
977
|
+
raise ArgumentError, "Must specify a Time, DateTime, or String"
|
978
|
+
end
|
979
|
+
end
|
980
|
+
|
981
|
+
# '5 min'.from(time)
|
982
|
+
def from(time_point = ::Time.now)
|
983
|
+
raise ArgumentError, "Must specify a Time" unless time_point
|
984
|
+
if String === time_point
|
985
|
+
time_point.time + self rescue time_point.datetime + self
|
986
|
+
else
|
987
|
+
time_point + self rescue time_point.to_datetime + self
|
988
|
+
end
|
989
|
+
end
|
990
|
+
alias :after :from
|
991
|
+
alias :from_now :from
|
992
|
+
|
993
|
+
|
994
|
+
|
995
|
+
# automatically coerce objects to units when possible
|
996
|
+
# if an object defines a 'to_unit' method, it will be coerced using that method
|
997
|
+
def coerce(other)
|
998
|
+
if other.respond_to? :to_unit
|
999
|
+
return [other.to_unit, self]
|
1000
|
+
end
|
1001
|
+
case other
|
1002
|
+
when Unit
|
1003
|
+
[other, self]
|
1004
|
+
else
|
1005
|
+
[Unit.new(other), self]
|
1006
|
+
end
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
# Protected and Private Functions that should only be called from this class
|
1010
|
+
protected
|
1011
|
+
|
1012
|
+
|
1013
|
+
def update_base_scalar
|
1014
|
+
return @base_scalar unless @base_scalar.nil?
|
1015
|
+
if self.is_base?
|
1016
|
+
@base_scalar = @scalar
|
1017
|
+
@signature = unit_signature
|
1018
|
+
else
|
1019
|
+
base = self.to_base
|
1020
|
+
@base_scalar = base.scalar
|
1021
|
+
@signature = base.signature
|
1022
|
+
end
|
1023
|
+
end
|
1024
|
+
|
1025
|
+
# calculates the unit signature vector used by unit_signature
|
1026
|
+
def unit_signature_vector
|
1027
|
+
return self.to_base.unit_signature_vector unless self.is_base?
|
1028
|
+
vector = Array.new(SIGNATURE_VECTOR.size,0)
|
1029
|
+
for element in @numerator
|
1030
|
+
if r=@@ALL_UNIT_DEFINITIONS[element]
|
1031
|
+
n = SIGNATURE_VECTOR.index(r[2])
|
1032
|
+
vector[n] = vector[n] + 1 if n
|
1033
|
+
end
|
1034
|
+
end
|
1035
|
+
for element in @denominator
|
1036
|
+
if r=@@ALL_UNIT_DEFINITIONS[element]
|
1037
|
+
n = SIGNATURE_VECTOR.index(r[2])
|
1038
|
+
vector[n] = vector[n] - 1 if n
|
1039
|
+
end
|
1040
|
+
end
|
1041
|
+
raise ArgumentError, "Power out of range (-20 < net power of a unit < 20)" if vector.any? {|x| x.abs >=20}
|
1042
|
+
vector
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
private
|
1046
|
+
|
1047
|
+
def initialize_copy(other)
|
1048
|
+
@numerator = other.numerator.dup
|
1049
|
+
@denominator = other.denominator.dup
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
# calculates the unit signature id for use in comparing compatible units and simplification
|
1053
|
+
# the signature is based on a simple classification of units and is based on the following publication
|
1054
|
+
#
|
1055
|
+
# Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering,
|
1056
|
+
# 21(8), Aug 1995, pp.651-661
|
1057
|
+
# doi://10.1109/32.403789
|
1058
|
+
# http://ieeexplore.ieee.org/Xplore/login.jsp?url=/iel1/32/9079/00403789.pdf?isnumber=9079&prod=JNL&arnumber=403789&arSt=651&ared=661&arAuthor=Novak%2C+G.S.%2C+Jr.
|
1059
|
+
#
|
1060
|
+
def unit_signature
|
1061
|
+
return @signature unless @signature.nil?
|
1062
|
+
vector = unit_signature_vector
|
1063
|
+
vector.each_with_index {|item,index| vector[index] = item * 20**index}
|
1064
|
+
@signature=vector.inject(0) {|sum,n| sum+n}
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
def self.eliminate_terms(q, n, d)
|
1068
|
+
num = n.dup
|
1069
|
+
den = d.dup
|
1070
|
+
|
1071
|
+
num.delete_if {|v| v == UNITY}
|
1072
|
+
den.delete_if {|v| v == UNITY}
|
1073
|
+
combined = Hash.new(0)
|
1074
|
+
|
1075
|
+
i = 0
|
1076
|
+
loop do
|
1077
|
+
break if i > num.size
|
1078
|
+
if @@PREFIX_VALUES.has_key? num[i]
|
1079
|
+
k = [num[i],num[i+1]]
|
1080
|
+
i += 2
|
1081
|
+
else
|
1082
|
+
k = num[i]
|
1083
|
+
i += 1
|
1084
|
+
end
|
1085
|
+
combined[k] += 1 unless k.nil? || k == UNITY
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
j = 0
|
1089
|
+
loop do
|
1090
|
+
break if j > den.size
|
1091
|
+
if @@PREFIX_VALUES.has_key? den[j]
|
1092
|
+
k = [den[j],den[j+1]]
|
1093
|
+
j += 2
|
1094
|
+
else
|
1095
|
+
k = den[j]
|
1096
|
+
j += 1
|
1097
|
+
end
|
1098
|
+
combined[k] -= 1 unless k.nil? || k == UNITY
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
num = []
|
1102
|
+
den = []
|
1103
|
+
for key, value in combined do
|
1104
|
+
case
|
1105
|
+
when value > 0
|
1106
|
+
value.times {num << key}
|
1107
|
+
when value < 0
|
1108
|
+
value.abs.times {den << key}
|
1109
|
+
end
|
1110
|
+
end
|
1111
|
+
num = UNITY_ARRAY if num.empty?
|
1112
|
+
den = UNITY_ARRAY if den.empty?
|
1113
|
+
{:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact}
|
1114
|
+
end
|
1115
|
+
|
1116
|
+
|
1117
|
+
# parse a string into a unit object.
|
1118
|
+
# Typical formats like :
|
1119
|
+
# "5.6 kg*m/s^2"
|
1120
|
+
# "5.6 kg*m*s^-2"
|
1121
|
+
# "5.6 kilogram*meter*second^-2"
|
1122
|
+
# "2.2 kPa"
|
1123
|
+
# "37 degC"
|
1124
|
+
# "1" -- creates a unitless constant with value 1
|
1125
|
+
# "GPa" -- creates a unit with scalar 1 with units 'GPa'
|
1126
|
+
# 6'4" -- recognized as 6 feet + 4 inches
|
1127
|
+
# 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
|
1128
|
+
def parse(passed_unit_string="0")
|
1129
|
+
unit_string = passed_unit_string.dup
|
1130
|
+
if unit_string =~ /\$\s*(#{NUMBER_REGEX})/
|
1131
|
+
unit_string = "#{$1} USD"
|
1132
|
+
end
|
1133
|
+
unit_string.gsub!(/%/,'percent')
|
1134
|
+
unit_string.gsub!(/'/,'feet')
|
1135
|
+
unit_string.gsub!(/"/,'inch')
|
1136
|
+
unit_string.gsub!(/#/,'pound')
|
1137
|
+
|
1138
|
+
#:nocov:
|
1139
|
+
if defined?(Uncertain) && unit_string =~ /(\+\/-|±)/
|
1140
|
+
value, uncertainty, unit_s = unit_string.scan(UNCERTAIN_REGEX)[0]
|
1141
|
+
result = unit_s.unit * Uncertain.new(value.to_f,uncertainty.to_f)
|
1142
|
+
copy(result)
|
1143
|
+
return
|
1144
|
+
end
|
1145
|
+
#:nocov:
|
1146
|
+
|
1147
|
+
if defined?(Complex) && unit_string =~ COMPLEX_NUMBER
|
1148
|
+
real, imaginary, unit_s = unit_string.scan(COMPLEX_REGEX)[0]
|
1149
|
+
result = Unit(unit_s || '1') * Complex(real.to_f,imaginary.to_f)
|
1150
|
+
copy(result)
|
1151
|
+
return
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
if defined?(Rational) && unit_string =~ RATIONAL_NUMBER
|
1155
|
+
numerator, denominator, unit_s = unit_string.scan(RATIONAL_REGEX)[0]
|
1156
|
+
result = Unit(unit_s || '1') * Rational(numerator.to_i,denominator.to_i)
|
1157
|
+
copy(result)
|
1158
|
+
return
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
unit_string =~ NUMBER_REGEX
|
1162
|
+
unit = @@cached_units[$2]
|
1163
|
+
mult = ($1.empty? ? 1.0 : $1.to_f) rescue 1.0
|
1164
|
+
mult = mult.to_int if (mult.to_int == mult)
|
1165
|
+
if unit
|
1166
|
+
copy(unit)
|
1167
|
+
@scalar *= mult
|
1168
|
+
@base_scalar *= mult
|
1169
|
+
return self
|
1170
|
+
end
|
1171
|
+
unit_string.gsub!(/<(#{@@UNIT_REGEX})><(#{@@UNIT_REGEX})>/, '\1*\2')
|
1172
|
+
unit_string.gsub!(/[<>]/,"")
|
1173
|
+
|
1174
|
+
if unit_string =~ /:/
|
1175
|
+
hours, minutes, seconds, microseconds = unit_string.scan(TIME_REGEX)[0]
|
1176
|
+
raise ArgumentError, "Invalid Duration" if [hours, minutes, seconds, microseconds].all? {|x| x.nil?}
|
1177
|
+
result = "#{hours || 0} h".unit +
|
1178
|
+
"#{minutes || 0} minutes".unit +
|
1179
|
+
"#{seconds || 0} seconds".unit +
|
1180
|
+
"#{microseconds || 0} usec".unit
|
1181
|
+
copy(result)
|
1182
|
+
return
|
1183
|
+
end
|
1184
|
+
|
1185
|
+
|
1186
|
+
# Special processing for unusual unit strings
|
1187
|
+
# feet -- 6'5"
|
1188
|
+
feet, inches = unit_string.scan(FEET_INCH_REGEX)[0]
|
1189
|
+
if (feet && inches)
|
1190
|
+
result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches")
|
1191
|
+
copy(result)
|
1192
|
+
return
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
# weight -- 8 lbs 12 oz
|
1196
|
+
pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0]
|
1197
|
+
if (pounds && oz)
|
1198
|
+
result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz")
|
1199
|
+
copy(result)
|
1200
|
+
return
|
1201
|
+
end
|
1202
|
+
|
1203
|
+
raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1
|
1204
|
+
raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.scan(/\s[02-9]/).size > 0
|
1205
|
+
@scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] #parse the string into parts
|
1206
|
+
top.scan(TOP_REGEX).each do |item|
|
1207
|
+
n = item[1].to_i
|
1208
|
+
x = "#{item[0]} "
|
1209
|
+
case
|
1210
|
+
when n>=0
|
1211
|
+
top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) {|s| x * n}
|
1212
|
+
when n<0
|
1213
|
+
bottom = "#{bottom} #{x * -n}"; top.gsub!(/#{item[0]}(\^|\*\*)#{n}/,"")
|
1214
|
+
end
|
1215
|
+
end
|
1216
|
+
bottom.gsub!(BOTTOM_REGEX) {|s| "#{$1} " * $2.to_i} if bottom
|
1217
|
+
@scalar = @scalar.to_f unless @scalar.nil? || @scalar.empty?
|
1218
|
+
@scalar = 1 unless @scalar.kind_of? Numeric
|
1219
|
+
@scalar = @scalar.to_int if (@scalar.to_int == @scalar)
|
1220
|
+
|
1221
|
+
@numerator ||= UNITY_ARRAY
|
1222
|
+
@denominator ||= UNITY_ARRAY
|
1223
|
+
@numerator = top.scan(@@UNIT_MATCH_REGEX).delete_if {|x| x.empty?}.compact if top
|
1224
|
+
@denominator = bottom.scan(@@UNIT_MATCH_REGEX).delete_if {|x| x.empty?}.compact if bottom
|
1225
|
+
|
1226
|
+
|
1227
|
+
us = "#{(top || '' + bottom || '')}".to_s.gsub(@@UNIT_MATCH_REGEX,'').gsub(/[\d\*, "'_^\/\$]/,'')
|
1228
|
+
raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless us.empty?
|
1229
|
+
|
1230
|
+
@numerator = @numerator.map do |item|
|
1231
|
+
@@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]]
|
1232
|
+
end.flatten.compact.delete_if {|x| x.empty?}
|
1233
|
+
|
1234
|
+
@denominator = @denominator.map do |item|
|
1235
|
+
@@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]]
|
1236
|
+
end.flatten.compact.delete_if {|x| x.empty?}
|
1237
|
+
|
1238
|
+
@numerator = UNITY_ARRAY if @numerator.empty?
|
1239
|
+
@denominator = UNITY_ARRAY if @denominator.empty?
|
1240
|
+
self
|
1241
|
+
end
|
1242
|
+
|
1243
|
+
private
|
1244
|
+
|
1245
|
+
# parse a string consisting of a number and a unit string
|
1246
|
+
def self.parse_into_numbers_and_units(string)
|
1247
|
+
# scientific notation.... 123.234E22, -123.456e-10
|
1248
|
+
sci = %r{[+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*}
|
1249
|
+
# rational numbers.... -1/3, 1/5, 20/100
|
1250
|
+
rational = %r{[+-]?\d+\/\d+}
|
1251
|
+
# complex numbers... -1.2+3i, +1.2-3.3i
|
1252
|
+
complex = %r{#{sci}{2,2}i}
|
1253
|
+
anynumber = %r{(?:(#{complex}|#{rational}|#{sci})\b)?\s?([\D].*)?}
|
1254
|
+
num, unit = string.scan(anynumber).first
|
1255
|
+
[case num
|
1256
|
+
when NilClass
|
1257
|
+
1
|
1258
|
+
when complex
|
1259
|
+
if num.respond_to?(:to_c)
|
1260
|
+
num.to_c
|
1261
|
+
else
|
1262
|
+
Complex(*num.scan(/(#{sci})(#{sci})i/).flatten.map {|n| n.to_i})
|
1263
|
+
end
|
1264
|
+
when rational
|
1265
|
+
Rational(*num.split("/").map {|x| x.to_i})
|
1266
|
+
else
|
1267
|
+
num.to_f
|
1268
|
+
end, unit.to_s.strip]
|
1269
|
+
end
|
1270
|
+
end
|
1271
|
+
|
1272
|
+
Unit.setup
|