ruby-units 1.3.0.a → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,8 @@
1
+ # make a unitless unit with a given scalar
2
+ class Numeric
3
+ def to_unit(other = nil)
4
+ other ? Unit.new(self, other) : Unit.new(self)
5
+ end
6
+ alias :unit :to_unit
7
+ alias :u :to_unit
8
+ end
@@ -0,0 +1,8 @@
1
+ class Object
2
+ def Unit(*other)
3
+ other.to_unit
4
+ end
5
+
6
+ alias :U :Unit
7
+ alias :u :Unit
8
+ 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?(.+)?|&plusmn;|\+\/-/)
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 =~ /(\+\/-|&plusmn;)/
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