ruby-units 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/LICENSE +20 -0
  2. data/README +85 -0
  3. data/lib/ruby_units.rb +596 -0
  4. data/lib/units.rb +226 -0
  5. data/test/test_ruby_units.rb +384 -0
  6. metadata +50 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2006 Kevin C. Olbrich
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,85 @@
1
+ =Ruby Units
2
+
3
+ Version: 0.1.0
4
+
5
+ Kevin C. Olbrich, Ph.D.
6
+
7
+ http://www.sciwerks.com
8
+
9
+ Project page: http://rubyforge.org/projects/ruby-units
10
+
11
+ ==Introduction
12
+ Many technical applications make use of specialized calculations at some point. Frequently, these calculations require unit conversions to ensure accurate results. Needless to say, this is a pain to properly keep track of, and is prone to numerous errors.
13
+
14
+ ==Solution
15
+ The 'Ruby units' gem is designed so simplify the handling of units for scientific calculations. The units of each quantity are specified when a Unit object is created and the Unit class will handle all subsequent conversions and manipulations to ensure an accurate result.
16
+
17
+ ==Installation:
18
+ This package may be installed using:
19
+ gem install ruby-units
20
+
21
+ ==Usage:
22
+ unit = Unit.new("1") # constant only
23
+ unit = Unit.new("mm") # unit only (defaults to a value of 1)
24
+ unit = Unit.new("1 mm") # create a simple unit
25
+ unit = Unit.new("1 mm/s") # a compound unit
26
+ unit = Unit.new("1 mm s^-1") # in exponent notation
27
+ unit = Unit.new("1 kg*m^2/s^2") # complex unit
28
+ unit = Unit.new("1 kg m^2 s^-2") # complex unit
29
+ unit = Unit("1 mm") # shorthand
30
+ unit = "1 mm".to_unit # convert string object
31
+ unit = object.to_unit # convert any object using object.to_s
32
+
33
+ ==Rules:
34
+ 1. only 1 quantity per unit (with 2 exceptions... 6'5" and '8 lbs 8 oz')
35
+ 2. use SI notation when possible
36
+ 3. avoid using spaces in unit names
37
+
38
+ ==Unit compatability:
39
+ Many methods require that the units of two operands are compatible. Compatible units are those that can be easily converted into each other, such as 'meters' and 'feet'.
40
+
41
+ unit1 =~ unit2 #=> true if units are compatible
42
+
43
+ ==Unit Math:
44
+
45
+ <b>Method</b>:: <b>Comment</b>
46
+ Unit#+():: Add. only works if units are compatible
47
+ Unit#-():: Subtract. only works if units are compatible
48
+ Unit#*():: Multiply.
49
+ Unit#/():: Divide.
50
+ Unit#**():: Exponentiate. Exponent must be an integer, can be positive, negative, or zero
51
+ Unit#inverse:: Returns 1/unit
52
+ Unit#abs:: Returns absolute value of the unit quantity. Strips off the units
53
+ Unit#ceil:: rounds quantity to next highest integer
54
+ Unit#floor:: rounds quantity down to next lower integer
55
+ Unit#round:: rounds quantity to nearest integer
56
+ Unit#to_int:: returns the quantity as an integer
57
+
58
+ Unit will coerce other objects into a Unit if used in a formula. This means that ..
59
+
60
+ Unit("1 mm") + "2 mm" == Unit("3 mm")
61
+
62
+ This will work as expected so long as you start the formula with a Unit object.
63
+
64
+ ==Conversions & comparisons
65
+
66
+ Units can be converted to other units in a couple of ways.
67
+
68
+ unit1 = unit >> "ft" # => convert to 'feet'
69
+ unit >>= "ft" # => convert and overwrite original object
70
+ unit3 = unit1 + unit2 # => resulting object will have the units of unit1
71
+ unit3 = unit1 - unit2 # => resulting object will have the units of unit1
72
+ unit1 <=> unit2 # => does comparison on quantities in base units, throws an exception if not compatible
73
+ unit1 === unit2 # => true if units and quantity are the same, even if 'equivalent' by <=>
74
+
75
+ ==Text Output
76
+ Units will display themselves nicely based on the preferred abbreviation for the units and prefixes.
77
+ Since Unit implements a Unit#to_s, all that is needed in most cases is:
78
+ "#{Unit.new('1 mm')}" #=> "1 mm"
79
+
80
+ The to_s also accepts some options.
81
+ Unit.new('1.5 mm').to_s("%0.2f") # => "1.50 mm". Enter any valid format string
82
+ Unit.new('1.5 mm').to_s("in") # => converts to inches before printing
83
+ Unit.new("2 m").to_s(:ft) #=> returns 6'7"
84
+ Unit.new("100 kg").to_s(:lbs) #=> returns 220 lbs, 7 oz
85
+
data/lib/ruby_units.rb ADDED
@@ -0,0 +1,596 @@
1
+
2
+ # = Ruby Units
3
+ #
4
+ # Copyright 2006 by Kevin C. Olbrich, Ph.D.
5
+ #
6
+ # See http://rubyforge.org/ruby-units/
7
+ #
8
+ # http://www.sciwerks.org
9
+ #
10
+ # mailto://kevin.olbrich+ruby-units@gmail.com
11
+ #
12
+ # See README for detailed usage instructions and examples
13
+ #
14
+ # =Unit Definition Format
15
+ #
16
+ # '<name>' => [%w{prefered_name synonyms}, conversion_to_base, :classification, %w{<base> <units> <in> <numerator>} , %w{<base> <units> <in> <denominator>} ],
17
+ #
18
+ # Prefixes (e.g., a :prefix classification) get special handling
19
+ # Note: The accuracy of unit conversions depends on the precision of the conversion factor.
20
+ # If you have more accurate estimates for particular conversion factors, please send them
21
+ # to me and I will incorporate them into the next release. It is also incumbent on the end-user
22
+ # to ensure that the accuracy of any conversions is sufficient for their intended application.
23
+ #
24
+ # While there are a large number of unit specified in the base package,
25
+ # there are also a large number of units that are not included.
26
+ # This package covers nearly all SI, Imperial, and units commonly used
27
+ # in the United States. If your favorite units are not listed here, send me an email
28
+ #
29
+ # Future versions will provide a simple interface for the end-user to specify
30
+ # custom units.
31
+ class Unit < Numeric
32
+ require 'units'
33
+ # pre-generate hashes from unit definitions for performance.
34
+ @@USER_DEFINITIONS = {}
35
+ @@PREFIX_VALUES = {}
36
+ @@PREFIX_MAP = {}
37
+ @@UNIT_MAP = {}
38
+ @@UNIT_VALUES = {}
39
+ @@OUTPUT_MAP = {}
40
+ @@UNIT_VECTORS = {}
41
+
42
+ def self.setup
43
+ (UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS)).each do |key, value|
44
+ if value[2] == :prefix then
45
+ @@PREFIX_VALUES[Regexp.escape(key)]=value[1]
46
+ value[0].each {|x| @@PREFIX_MAP[Regexp.escape(x)]=key}
47
+ else
48
+ @@UNIT_VALUES[Regexp.escape(key)]={}
49
+ @@UNIT_VALUES[Regexp.escape(key)][:quantity]=value[1]
50
+ @@UNIT_VALUES[Regexp.escape(key)][:numerator]=value[3] if value[3]
51
+ @@UNIT_VALUES[Regexp.escape(key)][:denominator]=value[4] if value[4]
52
+ value[0].each {|x| @@UNIT_MAP[Regexp.escape(x)]=key}
53
+ @@UNIT_VECTORS[value[2]] = [] unless @@UNIT_VECTORS[value[2]]
54
+ @@UNIT_VECTORS[value[2]] = @@UNIT_VECTORS[value[2]]+[Regexp.escape(key)]
55
+ end
56
+ @@OUTPUT_MAP[Regexp.escape(key)]=value[0][0]
57
+ end
58
+ @@PREFIX_REGEX = @@PREFIX_MAP.keys.sort_by {|prefix| prefix.length}.reverse.join('|')
59
+ @@UNIT_REGEX = @@UNIT_MAP.keys.sort_by {|unit| unit.length}.reverse.join('|')
60
+ end
61
+
62
+ self.setup
63
+
64
+ include Comparable
65
+ attr_reader :quantity, :numerator, :denominator, :signature, :base_quantity
66
+
67
+ # Create a new Unit object. Can be initialized using a string, or a hash
68
+ # Valid formats include:
69
+ # "5.6 kg*m/s^2"
70
+ # "5.6 kg*m*s^-2"
71
+ # "5.6 kilogram*meter*second^-2"
72
+ # "2.2 kPa"
73
+ # "37 degC"
74
+ # "1" -- creates a unitless constant with value 1
75
+ # "GPa" -- creates a unit with quantity 1 with units 'GPa'
76
+ # 6'4" -- recognized as 6 feet + 4 inches
77
+ # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
78
+ #
79
+ def initialize(options)
80
+ if options.kind_of? String
81
+ parse(options)
82
+ elsif options.kind_of? Hash
83
+ @quantity = options[:quantity] || 1
84
+ @numerator = options[:numerator] || ["<1>"]
85
+ @denominator = options[:denominator] || []
86
+ else
87
+ raise ArgumentError, "Unknown Unit Specification"
88
+ end
89
+ self.update_base_quantity
90
+ self.unit_signature
91
+ self.freeze
92
+ end
93
+
94
+ def initialize_copy(other)
95
+ @numerator = other.numerator.clone
96
+ @denominator = other.denominator.clone
97
+ end
98
+
99
+ # Returns 'true' if the Unit is represented in base units
100
+ def is_base?
101
+ n = @numerator + @denominator
102
+ n.each do |x|
103
+ return false unless x == '<1>' || (@@UNIT_VALUES[Regexp.escape(x)] && @@UNIT_VALUES[Regexp.escape(x)][:numerator].include?(Regexp.escape(x)))
104
+ end
105
+ return true
106
+ end
107
+
108
+ #convert to base SI units
109
+ def to_base
110
+ return self if self.is_base?
111
+ num = []
112
+ den = []
113
+ q = @quantity.to_f
114
+ @numerator.each do |unit|
115
+ if @@PREFIX_VALUES[Regexp.escape(unit)]
116
+ q *= @@PREFIX_VALUES[Regexp.escape(unit)]
117
+ else
118
+ q *= @@UNIT_VALUES[Regexp.escape(unit)][:quantity] if @@UNIT_VALUES[Regexp.escape(unit)]
119
+ num << @@UNIT_VALUES[Regexp.escape(unit)][:numerator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:numerator]
120
+ den << @@UNIT_VALUES[Regexp.escape(unit)][:denominator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:denominator]
121
+ end
122
+ end
123
+
124
+ @denominator.each do |unit|
125
+ if @@PREFIX_MAP[Regexp.escape(unit)]
126
+ q /= @@PREFIX_MAP[Regexp.escape(unit)]
127
+ else
128
+ q /= @@UNIT_VALUES[Regexp.escape(unit)][:quantity] if @@UNIT_VALUES[Regexp.escape(unit)]
129
+ den << @@UNIT_VALUES[Regexp.escape(unit)][:numerator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:numerator]
130
+ num << @@UNIT_VALUES[Regexp.escape(unit)][:denominator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:denominator]
131
+ end
132
+ end
133
+ num = num.flatten.compact
134
+ den = den.flatten.compact
135
+ num = ['<1>'] if num.empty?
136
+
137
+ Unit.new(Unit.eliminate_terms(q,num,den))
138
+ end
139
+
140
+ # Generate human readable output.
141
+ # If the name of a unit is passed, the quantity will first be converted to the target unit before output.
142
+ # some named conversions are available
143
+ #
144
+ # :ft - outputs in feet and inches (e.g., 6'4")
145
+ # :lbs - outputs in pounds and ounces (e.g, 8 lbs, 8 oz)
146
+ #
147
+ def to_s(target_units=nil)
148
+ case target_units
149
+ when :ft:
150
+ inches = (self >> "in").to_f
151
+ "#{(inches / 12).truncate}\'#{(inches % 12).round}\""
152
+ when :lbs:
153
+ ounces = (self >> "oz").to_f
154
+ "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz"
155
+ else
156
+ target_units =~ /(%[\w\d#+-.]*)*\s*(.+)*/
157
+ format_string = "#{$1}" if $1
158
+ units = $2
159
+ return (self >> units).to_s(format_string) if units
160
+ "#{(format_string || '%g') % @quantity} #{self.to_unit}".strip
161
+ end
162
+ end
163
+
164
+ # Compare two Unit objects. Throws an exception if they are not of compatible types.
165
+ # Comparisons are done based on the value of the unit in base SI units.
166
+ def <=>(other)
167
+ raise ArgumentError, "Incompatible Units" unless self =~ other
168
+ return self.base_quantity <=> other.base_quantity
169
+ end
170
+
171
+ # check to see if units are compatible, but not the quantity part
172
+ # this check is done by comparing signatures for performance reasons
173
+ # if passed a string, it will create a unit object with the string and then do the comparison
174
+ # this permits a syntax like:
175
+ # unit =~ "mm"
176
+ # if you want to do a regexp on the unit string do this ...
177
+ # unit.to_unit =~ /regexp/
178
+ def =~(other)
179
+ case other
180
+ when Unit : self.signature == other.signature
181
+ else
182
+ x,y = coerce(other)
183
+ x =~ y
184
+ end
185
+ end
186
+
187
+ # Compare two units. Returns true if quantities and units match
188
+ #
189
+ # Unit("100 cm") === Unit("100 cm") # => true
190
+ # Unit("100 cm") === Unit("1 m") # => false
191
+ def ===(other)
192
+ case other
193
+ when Unit: (self.quantity == other.quantity) && (self.to_unit == other.to_unit)
194
+ else
195
+ x,y = coerce(other)
196
+ x === y
197
+ end
198
+ end
199
+
200
+ # Add two units together. Result is same units as receiver and quantity and base_quantity are updated appropriately
201
+ # throws an exception if the units are not compatible.
202
+ def +(other)
203
+ if Unit === other
204
+ if self =~ other then
205
+ q = @quantity + (other >> self).quantity
206
+ Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
207
+ else
208
+ raise ArgumentError, "Incompatible Units"
209
+ end
210
+ else
211
+ x,y = coerce(other)
212
+ x + y
213
+ end
214
+ end
215
+
216
+ # Subtract two units. Result is same units as receiver and quantity and base_quantity are updated appropriately
217
+ # throws an exception if the units are not compatible.
218
+ def -(other)
219
+ if Unit === other
220
+ if self =~ other then
221
+ q = @quantity - (other >> self).quantity
222
+ Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
223
+ else
224
+ raise ArgumentError, "Incompatible Units"
225
+ end
226
+ else
227
+ x,y = coerce(other)
228
+ x - y
229
+ end
230
+ end
231
+
232
+ # Multiply two units.
233
+ # Throws an exception if multiplier is not a Unit or Numeric
234
+ def *(other)
235
+ if Unit === other
236
+ Unit.new(Unit.eliminate_terms(@quantity*other.quantity, @numerator + other.numerator ,@denominator + other.denominator))
237
+ else
238
+ x,y = coerce(other)
239
+ x * y
240
+ end
241
+ end
242
+
243
+ # Divide two units.
244
+ # Throws an exception if divisor is not a Unit or Numeric
245
+ def /(other)
246
+ if Unit === other
247
+ Unit.new(Unit.eliminate_terms(@quantity/other.quantity, @numerator + other.denominator ,@denominator + other.numerator))
248
+ else
249
+ x,y = coerce(other)
250
+ y / x
251
+ end
252
+ end
253
+
254
+ # Exponentiate. Only takes integer powers.
255
+ # Note that anything raised to the power of 0 results in a Unit object with a quantity of 1, and no units.
256
+ # Throws an exception if exponent is not an integer.
257
+ def **(other)
258
+ raise ArgumentError, "Exponent must be Integer" unless Integer === other
259
+ case
260
+ when other.to_i > 0 : (1..other.to_i).inject(Unit.new("1")) {|product, n| product * self}
261
+ when other.to_i == 0 : Unit.new("1")
262
+ when other.to_i < 0 : (1..-other.to_i).inject(Unit.new("1")) {|product, n| product / self}
263
+ end
264
+ end
265
+
266
+ # returns inverse of Unit (1/unit)
267
+ def inverse
268
+ (Unit.new("1") / self)
269
+ end
270
+
271
+ # convert to a specified unit string or to the same units as another Unit
272
+ #
273
+ # unit >> "kg" will covert to kilograms
274
+ # unit1 >> unit2 converts to same units as unit2 object
275
+ #
276
+ # To convert a Unit object to match another Unit object, use:
277
+ # unit1 >>= unit2
278
+ # Throws an exception if the requested target units are incompatible with current Unit.
279
+ #
280
+ # Special handling for temperature conversions is supported. If the Unit object is converted
281
+ # from one temperature unit to another, the proper temperature offsets will be used.
282
+ # Supports Kelvin, Celcius, Farenheit, and Rankine scales.
283
+ #
284
+ # Note that if temperature is part of a compound unit, the temperature will be treated as a differential
285
+ # and the units will be scaled appropriately.
286
+ def >>(other)
287
+ case other.class.to_s
288
+ when 'Unit': target = other
289
+ when 'String': target = Unit.new(other)
290
+ else
291
+ raise ArgumentError, "Unknown target units"
292
+ end
293
+ raise ArgumentError, "Incompatible Units" unless self =~ target
294
+ if target.signature == 400 then # special handling for temperature conversions
295
+ q=case self.numerator[0]
296
+ when '<celcius>':
297
+ case target.numerator[0]
298
+ when '<celcius>' : @quantity
299
+ when '<kelvin>' : @quantity + 273.15
300
+ when '<farenheit>': @quantity * (9.0/5.0) + 32.0
301
+ when '<rankine>' : @quantity * (9.0/5.0) + 491.67
302
+ else
303
+ raise ArgumentError, "Unknown temperature conversion requested"
304
+ end
305
+ when '<kelvin>':
306
+ case target.numerator[0]
307
+ when '<celcius>' : @quantity - 273.15
308
+ when '<kelvin>' : @quantity
309
+ when '<farenheit>': @quantity * (9.0/5.0) - 459.67
310
+ when '<rankine>' : @quantity * (9.0/5.0)
311
+ else
312
+ raise ArgumentError, "Unknown temperature conversion requested"
313
+ end
314
+ when '<farenheit>':
315
+ case target.numerator[0]
316
+ when '<celcius>' : (@quantity-32)*(5.0/9.0)
317
+ when '<kelvin>' : (@quantity+459.67)*(5.0/9.0)
318
+ when '<farenheit>': @quantity
319
+ when '<rankine>' : @quantity + 459.67
320
+ else
321
+ raise ArgumentError, "Unknown temperature conversion requested"
322
+ end
323
+ when '<rankine>':
324
+ case target.numerator[0]
325
+ when '<celcius>' : @quantity*(5.0/9.0) -273.15
326
+ when '<kelvin>' : @quantity*(5.0/9.0)
327
+ when '<farenheit>': @quantity - 459.67
328
+ when '<rankine>' : @quantity
329
+ else
330
+ raise ArgumentError, "Unknown temperature conversion requested"
331
+ end
332
+ else
333
+ raise ArgumentError, "Unknown temperature conversion requested"
334
+ end
335
+ Unit.new(:quantity=>q, :numerator=>target.numerator, :denominator=>target.denominator)
336
+ else
337
+ one = @numerator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:quantity] }.compact
338
+ two = @denominator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:quantity] }.compact
339
+ v = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n}
340
+ one = target.numerator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:quantity] }.compact
341
+ two = target.denominator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:quantity] }.compact
342
+ y = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n}
343
+ q = @quantity * v/y
344
+ Unit.new(:quantity=>q, :numerator=>target.numerator, :denominator=>target.denominator)
345
+ end
346
+ end
347
+
348
+ # calculates the unit signature vector used by unit_signature
349
+ def unit_signature_vector
350
+ result = self.to_base
351
+ y = [:length, :time, :temperature, :mass, :current, :substance, :luminosity, :currency, :memory, :angle]
352
+ vector = Array.new(y.size,0)
353
+ y.each_with_index do |units,index|
354
+ vector[index] = result.numerator.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size
355
+ vector[index] -= result.denominator.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size
356
+ end
357
+ vector
358
+ end
359
+
360
+ # calculates the unit signature id for use in comparing compatible units and simplification
361
+ # the signature is based on a simple classification of units and is based on the following publication
362
+ #
363
+ # Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering,
364
+ # 21(8), Aug 1995, pp.651-661
365
+ # doi://10.1109/32.403789
366
+ # 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.
367
+ #
368
+ def unit_signature
369
+ vector = unit_signature_vector
370
+ vector.each_with_index {|item,index| vector[index] = item * 20**index}
371
+ @signature=vector.inject(0) {|sum,n| sum+n}
372
+ end
373
+
374
+ # Eliminates terms in the passed numerator and denominator. Expands out prefixes and applies them to the
375
+ # quantity. Returns a hash that can be used to initialize a new Unit object.
376
+ def self.eliminate_terms(q, n, d)
377
+ num = n.clone
378
+ den = d.clone
379
+
380
+ # cancel terms in both numerator and denominator
381
+ num.each_with_index do |item,index|
382
+ if i=den.index(item)
383
+ num.delete_at(index)
384
+ den.delete_at(i)
385
+ end
386
+ end
387
+
388
+ num = num.flatten.compact
389
+ den = den.flatten.compact
390
+
391
+ # substitute in SI prefix multipliers and numerical constants
392
+ num.each_with_index do |item, index|
393
+ if item =~ /<([\dEe+-.]+)>/
394
+ q *= $1.to_f
395
+ num.delete_at(index)
396
+ elsif multiplier=@@PREFIX_VALUES[Regexp.escape(item)]
397
+ q *= multiplier
398
+ num.delete_at(index)
399
+ end
400
+ end
401
+
402
+ den.each_with_index do |item, index|
403
+ if item =~ /<([\dEe+-.]+)>/
404
+ q /= $1.to_f
405
+ den.delete_at(index)
406
+ elsif multiplier=@@PREFIX_VALUES[Regexp.escape(item)]
407
+ q /= multiplier
408
+ den.delete_at(index)
409
+ end
410
+ end
411
+ num = ["<1>"] if num.empty?
412
+ den = ["<1>"] if den.empty?
413
+ {:quantity=>q, :numerator=>num, :denominator=>den}
414
+ end
415
+
416
+ # returns the quantity part of the Unit
417
+ def to_f
418
+ @quantity
419
+ end
420
+
421
+ # returns the 'unit' part of the Unit object without the quantity
422
+ def to_unit
423
+ return "" if @numerator == ["<1>"] && @denominator == ["<1>"]
424
+ output_n = []
425
+ num = @numerator.clone
426
+ den = @denominator.clone
427
+ if @numerator == ["<1>"]
428
+ output_n << "1"
429
+ else
430
+ num.each_with_index do |token,index|
431
+ if token && @@PREFIX_VALUES[Regexp.escape(token)] then
432
+ output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(@numerator[index+1])]}"
433
+ num[index+1]=nil
434
+ else
435
+ output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}" if token
436
+ end
437
+ end
438
+ end
439
+ output_d = den.map do |token|
440
+ @@PREFIX_MAP[Regexp.escape(token)] ? @@OUTPUT_MAP[Regexp.escape(token)] : "#{@@OUTPUT_MAP[Regexp.escape(token)]} "
441
+ end
442
+ 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]}" : ''))}
443
+ 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]}" : ''))}
444
+ "#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip
445
+ end
446
+
447
+ # negates the quantity of the Unit
448
+ def -@
449
+ q = -self.quantity
450
+ Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
451
+ end
452
+
453
+ # returns abs of quantity, without the units
454
+ def abs
455
+ return @quantity.abs
456
+ end
457
+
458
+ def ceil
459
+ q = self.quantity.ceil
460
+ Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
461
+ end
462
+
463
+ def floor
464
+ q = self.quantity.floor
465
+ Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
466
+ end
467
+
468
+ def to_int
469
+ q = self.quantity.to_int
470
+ Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
471
+ end
472
+
473
+ alias :to_i :to_int
474
+ alias :truncate :to_int
475
+
476
+ def round
477
+ q = self.quantity.round
478
+ Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
479
+ end
480
+
481
+ # true if quantity is zero
482
+ def zero?
483
+ return @quantity.zero?
484
+ end
485
+
486
+ # returns self if zero? is false, nil otherwise
487
+ #def nonzero?
488
+ # self.zero? ? nil : self
489
+ #end
490
+
491
+ def update_base_quantity
492
+ @base_quantity = self.is_base? ? @quantity : self.to_base.quantity
493
+ self
494
+ end
495
+
496
+ def coerce(other)
497
+ case other
498
+ when Unit : [other, self]
499
+ when String : [Unit.new(other), self]
500
+ else
501
+ [Unit.new(other.to_s), self]
502
+ end
503
+ end
504
+
505
+ private
506
+
507
+ # parse a string into a unit object.
508
+ # Typical formats like :
509
+ # "5.6 kg*m/s^2"
510
+ # "5.6 kg*m*s^-2"
511
+ # "5.6 kilogram*meter*second^-2"
512
+ # "2.2 kPa"
513
+ # "37 degC"
514
+ # "1" -- creates a unitless constant with value 1
515
+ # "GPa" -- creates a unit with quantity 1 with units 'GPa'
516
+ # 6'4" -- recognized as 6 feet + 4 inches
517
+ # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
518
+ def parse(unit_string="0")
519
+ @numerator = ['<1>']
520
+ @denominator = ['<1>']
521
+
522
+ # Special processing for unusual unit strings
523
+ # feet -- 6'5"
524
+ feet, inches = unit_string.scan(/(\d+)[\s*|'|ft|feet\s*](\d+)[\s*|"|in|inches]/)[0]
525
+ if (feet && inches)
526
+ result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches")
527
+ @quantity = result.quantity
528
+ @numerator = result.numerator
529
+ @denominator = result.denominator
530
+ @base_quantity = result.base_quantity
531
+ return self
532
+ end
533
+
534
+ # weight -- 8 lbs 12 oz
535
+ pounds, oz = unit_string.scan(/(\d+)[\s|#|lbs|pounds|,]+(\d+)[\s*|oz|ounces]/)[0]
536
+ if (pounds && oz)
537
+ result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz")
538
+ @quantity = result.quantity
539
+ @numerator = result.numerator
540
+ @denominator = result.denominator
541
+ @base_quantity = result.base_quantity
542
+ return self
543
+ end
544
+
545
+ @quantity, top, bottom = unit_string.scan(/([\dEe+.-]*)\s*([^\/]*)\/*(.+)*/)[0] #parse the string into parts
546
+
547
+ top.scan(/([^ \*]+)\^([\d-]+)/).each do |item|
548
+ n = item[1].to_i
549
+ x = "#{item[0]} "
550
+ case
551
+ when n>=0 : top.gsub!(/([^ \*]+)\^(\d+)/) {|s| x * n}
552
+ when n<0 : bottom = "#{bottom} #{x * -n}"; top.gsub!("#{item[0]}^#{item[1]}","")
553
+ end
554
+ end
555
+
556
+ bottom.gsub!(/([^* ]+)\^(\d+)/) {|s| "#{$1} " * $2.to_i} if bottom
557
+
558
+ if @quantity.empty?
559
+ if top =~ /[\dEe+.-]+/
560
+ @quantity = top.to_f # need this for 'number only' initialization
561
+ else
562
+ @quantity = 1 # need this for 'unit only' intialization
563
+ end
564
+ else
565
+ @quantity = @quantity.to_f
566
+ end
567
+
568
+ @numerator = top.scan(/((#{@@PREFIX_REGEX})*(#{@@UNIT_REGEX}))/).delete_if {|x| x.empty?}.compact if top
569
+ @denominator = bottom.scan(/((#{@@PREFIX_REGEX})*(#{@@UNIT_REGEX}))/).delete_if {|x| x.empty?}.compact if bottom
570
+
571
+ @numerator = @numerator.map do |item|
572
+ item.map {|x| Regexp.escape(x) if x}
573
+ @@UNIT_MAP[item[0]] ? [@@UNIT_MAP[item[0]]] : [@@PREFIX_MAP[item[1]], @@UNIT_MAP[item[2]]]
574
+ end.flatten.compact.delete_if {|x| x.empty?}
575
+
576
+ @denominator = @denominator.map do |item|
577
+ item.map {|x| Regexp.escape(x) if x}
578
+ @@UNIT_MAP[item[0]] ? [@@UNIT_MAP[item[0]]] : [@@PREFIX_MAP[item[1]], @@UNIT_MAP[item[2]]]
579
+ end.flatten.compact.delete_if {|x| x.empty?}
580
+
581
+ @numerator = ['<1>'] if @numerator.empty?
582
+ @denominator = ['<1>'] if @denominator.empty?
583
+ self
584
+ end
585
+
586
+ end
587
+
588
+ class Object
589
+ def Unit(other)
590
+ Unit.new(other.to_s)
591
+ end
592
+
593
+ def to_unit
594
+ Unit.new(self.to_s) unless Unit === self
595
+ end
596
+ end