ruby-units 0.1.0

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