ruby-units 1.0.1 → 1.0.2

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