ruby-units 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ruby-units.rb CHANGED
@@ -1,4 +1,7 @@
1
- # = Ruby Units 0.1.1
1
+ require 'mathn'
2
+ require 'rational'
3
+
4
+ # = Ruby Units 0.2.0
2
5
  #
3
6
  # Copyright 2006 by Kevin C. Olbrich, Ph.D.
4
7
  #
@@ -51,7 +54,7 @@ class Unit < Numeric
51
54
  value[0].each {|x| @@PREFIX_MAP[Regexp.escape(x)]=key}
52
55
  else
53
56
  @@UNIT_VALUES[Regexp.escape(key)]={}
54
- @@UNIT_VALUES[Regexp.escape(key)][:quantity]=value[1]
57
+ @@UNIT_VALUES[Regexp.escape(key)][:scalar]=value[1]
55
58
  @@UNIT_VALUES[Regexp.escape(key)][:numerator]=value[3] if value[3]
56
59
  @@UNIT_VALUES[Regexp.escape(key)][:denominator]=value[4] if value[4]
57
60
  value[0].each {|x| @@UNIT_MAP[Regexp.escape(x)]=key}
@@ -67,8 +70,25 @@ class Unit < Numeric
67
70
  self.setup
68
71
 
69
72
  include Comparable
70
- attr_reader :quantity, :numerator, :denominator, :signature, :base_quantity
73
+ attr_accessor :scalar, :numerator, :denominator, :signature, :base_scalar
74
+
71
75
 
76
+ def to_yaml_properties
77
+ %w{@scalar @numerator @denominator @signature @base_scalar}
78
+ end
79
+
80
+ # basically a copy of the basic to_yaml. Needed because otherwise it ends up coercing the object to a string
81
+ # before YAML'izing it.
82
+ def to_yaml( opts = {} )
83
+ YAML::quick_emit( object_id, opts ) do |out|
84
+ out.map( taguri, to_yaml_style ) do |map|
85
+ to_yaml_properties.each do |m|
86
+ map.add( m[1..-1], instance_variable_get( m ) )
87
+ end
88
+ end
89
+ end
90
+ end
91
+
72
92
  # Create a new Unit object. Can be initialized using a string, or a hash
73
93
  # Valid formats include:
74
94
  # "5.6 kg*m/s^2"
@@ -77,24 +97,30 @@ class Unit < Numeric
77
97
  # "2.2 kPa"
78
98
  # "37 degC"
79
99
  # "1" -- creates a unitless constant with value 1
80
- # "GPa" -- creates a unit with quantity 1 with units 'GPa'
81
- # 6'4" -- recognized as 6 feet + 4 inches
100
+ # "GPa" -- creates a unit with scalar 1 with units 'GPa'
101
+ # 6'4" -- recognized as 6 feet + 4 inches
82
102
  # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
83
103
  #
84
- def initialize(options)
104
+ def initialize(options)
85
105
  case options
86
106
  when String: parse(options)
87
107
  when Hash:
88
- @quantity = options[:quantity] || 1
108
+ @scalar = options[:scalar] || 1
89
109
  @numerator = options[:numerator] || ["<1>"]
90
110
  @denominator = options[:denominator] || []
91
- when Array: parse("#{options[0]} #{options[1]}/#{options[2]}")
92
- when Numeric: parse(options.to_s)
111
+ when Array:
112
+ parse("#{options[0]} #{options[1]}/#{options[2]}")
113
+ when Numeric:
114
+ @scalar = options
115
+ @numerator = @denominator = ['<1>']
116
+ when Time:
117
+ @scalar = options.to_f
118
+ @numerator = ['<second>']
119
+ @denominator = ['<1>']
93
120
  else
94
121
  raise ArgumentError, "Invalid Unit Format"
95
122
  end
96
- self.update_base_quantity
97
- self.unit_signature
123
+ self.update_base_scalar
98
124
  self.freeze
99
125
  end
100
126
 
@@ -105,9 +131,10 @@ class Unit < Numeric
105
131
 
106
132
  # Returns 'true' if the Unit is represented in base units
107
133
  def is_base?
134
+ return true if @signature == 400 && @numerator.size == 1 && @numerator[0] =~ /(celcius|kelvin|farenheit|rankine)/
108
135
  n = @numerator + @denominator
109
- n.each do |x|
110
- return false unless x == '<1>' || (@@UNIT_VALUES[Regexp.escape(x)] && @@UNIT_VALUES[Regexp.escape(x)][:numerator].include?(Regexp.escape(x)))
136
+ n.compact.each do |x|
137
+ return false unless x == '<1>' || (@@UNIT_VALUES[Regexp.escape(x)] && @@UNIT_VALUES[Regexp.escape(x)][:denominator].nil? && @@UNIT_VALUES[Regexp.escape(x)][:numerator].include?(Regexp.escape(x)))
111
138
  end
112
139
  return true
113
140
  end
@@ -117,21 +144,21 @@ class Unit < Numeric
117
144
  return self if self.is_base?
118
145
  num = []
119
146
  den = []
120
- q = @quantity.to_f
121
- @numerator.each do |unit|
147
+ q = @scalar
148
+ @numerator.compact.each do |unit|
122
149
  if @@PREFIX_VALUES[Regexp.escape(unit)]
123
150
  q *= @@PREFIX_VALUES[Regexp.escape(unit)]
124
151
  else
125
- q *= @@UNIT_VALUES[Regexp.escape(unit)][:quantity] if @@UNIT_VALUES[Regexp.escape(unit)]
152
+ q *= @@UNIT_VALUES[Regexp.escape(unit)][:scalar] if @@UNIT_VALUES[Regexp.escape(unit)]
126
153
  num << @@UNIT_VALUES[Regexp.escape(unit)][:numerator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:numerator]
127
154
  den << @@UNIT_VALUES[Regexp.escape(unit)][:denominator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:denominator]
128
155
  end
129
156
  end
130
- @denominator.each do |unit|
157
+ @denominator.compact.each do |unit|
131
158
  if @@PREFIX_VALUES[Regexp.escape(unit)]
132
159
  q /= @@PREFIX_VALUES[Regexp.escape(unit)]
133
160
  else
134
- q /= @@UNIT_VALUES[Regexp.escape(unit)][:quantity] if @@UNIT_VALUES[Regexp.escape(unit)]
161
+ q /= @@UNIT_VALUES[Regexp.escape(unit)][:scalar] if @@UNIT_VALUES[Regexp.escape(unit)]
135
162
  den << @@UNIT_VALUES[Regexp.escape(unit)][:numerator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:numerator]
136
163
  num << @@UNIT_VALUES[Regexp.escape(unit)][:denominator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:denominator]
137
164
  end
@@ -145,43 +172,56 @@ class Unit < Numeric
145
172
  end
146
173
 
147
174
  # Generate human readable output.
148
- # If the name of a unit is passed, the quantity will first be converted to the target unit before output.
175
+ # If the name of a unit is passed, the scalar will first be converted to the target unit before output.
149
176
  # some named conversions are available
150
177
  #
151
178
  # :ft - outputs in feet and inches (e.g., 6'4")
152
179
  # :lbs - outputs in pounds and ounces (e.g, 8 lbs, 8 oz)
153
- #
154
180
  def to_s(target_units=nil)
155
181
  case target_units
156
182
  when :ft:
157
- inches = (self >> "in").to_f
183
+ inches = self.to("in").scalar
158
184
  "#{(inches / 12).truncate}\'#{(inches % 12).round}\""
159
185
  when :lbs:
160
- ounces = (self >> "oz").to_f
186
+ ounces = self.to("oz").scalar
161
187
  "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz"
162
188
  else
163
189
  target_units =~ /(%[\w\d#+-.]*)*\s*(.+)*/
164
- format_string = "#{$1}" if $1
165
- units = $2
166
- return (self >> units).to_s(format_string) if units
167
- "#{(format_string || '%g') % @quantity} #{self.to_unit}".strip
190
+ return self.to($2).to_s($1) if $2
191
+ "#{($1 || '%g') % @scalar || 0} #{self.units}".strip
168
192
  end
169
193
  end
170
194
 
195
+ def inspect(option=nil)
196
+ return super() if option == :dump
197
+ self.to_s
198
+ end
199
+
200
+ # returns true if no associated units
201
+ def unitless?
202
+ (@numerator == ['<1>'] && @denominator == ['<1>'])
203
+ end
204
+
171
205
  # Compare two Unit objects. Throws an exception if they are not of compatible types.
172
206
  # Comparisons are done based on the value of the unit in base SI units.
173
207
  def <=>(other)
174
- raise ArgumentError, "Incompatible Units" unless self =~ other
175
- return self.base_quantity <=> other.base_quantity
208
+ case other
209
+ when Unit:
210
+ raise ArgumentError, "Incompatible Units" unless self =~ other
211
+ self.base_scalar <=> other.base_scalar
212
+ else
213
+ x,y = coerce(other)
214
+ x <=> y
215
+ end
176
216
  end
177
217
 
178
- # check to see if units are compatible, but not the quantity part
218
+ # check to see if units are compatible, but not the scalar part
179
219
  # this check is done by comparing signatures for performance reasons
180
220
  # if passed a string, it will create a unit object with the string and then do the comparison
181
221
  # this permits a syntax like:
182
222
  # unit =~ "mm"
183
223
  # if you want to do a regexp on the unit string do this ...
184
- # unit.to_unit =~ /regexp/
224
+ # unit.units =~ /regexp/
185
225
  def =~(other)
186
226
  case other
187
227
  when Unit : self.signature == other.signature
@@ -191,26 +231,32 @@ class Unit < Numeric
191
231
  end
192
232
  end
193
233
 
234
+ alias :compatible? :=~
235
+ alias :compatible_with? :=~
236
+
194
237
  # Compare two units. Returns true if quantities and units match
195
238
  #
196
239
  # Unit("100 cm") === Unit("100 cm") # => true
197
240
  # Unit("100 cm") === Unit("1 m") # => false
198
241
  def ===(other)
199
242
  case other
200
- when Unit: (self.quantity == other.quantity) && (self.to_unit == other.to_unit)
243
+ when Unit: (self.scalar == other.scalar) && (self.units == other.units)
201
244
  else
202
245
  x,y = coerce(other)
203
246
  x === y
204
247
  end
205
248
  end
206
249
 
207
- # Add two units together. Result is same units as receiver and quantity and base_quantity are updated appropriately
250
+ alias :same? :===
251
+ alias :same_as? :===
252
+
253
+ # Add two units together. Result is same units as receiver and scalar and base_scalar are updated appropriately
208
254
  # throws an exception if the units are not compatible.
209
255
  def +(other)
210
256
  if Unit === other
211
257
  if self =~ other then
212
- q = @quantity + (other >> self).quantity
213
- Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
258
+ q = @scalar + other.to(self).scalar
259
+ Unit.new(:scalar=>q, :numerator=>@numerator, :denominator=>@denominator)
214
260
  else
215
261
  raise ArgumentError, "Incompatible Units"
216
262
  end
@@ -220,13 +266,13 @@ class Unit < Numeric
220
266
  end
221
267
  end
222
268
 
223
- # Subtract two units. Result is same units as receiver and quantity and base_quantity are updated appropriately
269
+ # Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately
224
270
  # throws an exception if the units are not compatible.
225
271
  def -(other)
226
272
  if Unit === other
227
273
  if self =~ other then
228
- q = @quantity - (other >> self).quantity
229
- Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
274
+ q = @scalar - other.to(self).scalar
275
+ Unit.new(:scalar=>q, :numerator=>@numerator, :denominator=>@denominator)
230
276
  else
231
277
  raise ArgumentError, "Incompatible Units"
232
278
  end
@@ -237,10 +283,9 @@ class Unit < Numeric
237
283
  end
238
284
 
239
285
  # Multiply two units.
240
- # Throws an exception if multiplier is not a Unit or Numeric
241
286
  def *(other)
242
287
  if Unit === other
243
- Unit.new(Unit.eliminate_terms(@quantity*other.quantity, @numerator + other.numerator ,@denominator + other.denominator))
288
+ Unit.new(Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator))
244
289
  else
245
290
  x,y = coerce(other)
246
291
  x * y
@@ -248,10 +293,11 @@ class Unit < Numeric
248
293
  end
249
294
 
250
295
  # Divide two units.
251
- # Throws an exception if divisor is not a Unit or Numeric
296
+ # Throws an exception if divisor is 0
252
297
  def /(other)
253
298
  if Unit === other
254
- Unit.new(Unit.eliminate_terms(@quantity/other.quantity, @numerator + other.denominator ,@denominator + other.numerator))
299
+ raise ZeroDivisionError if other.zero?
300
+ Unit.new(Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator))
255
301
  else
256
302
  x,y = coerce(other)
257
303
  y / x
@@ -259,20 +305,80 @@ class Unit < Numeric
259
305
  end
260
306
 
261
307
  # Exponentiate. Only takes integer powers.
262
- # Note that anything raised to the power of 0 results in a Unit object with a quantity of 1, and no units.
308
+ # Note that anything raised to the power of 0 results in a Unit object with a scalar of 1, and no units.
263
309
  # Throws an exception if exponent is not an integer.
310
+ # Ideally this routine should accept a float for the exponent
311
+ # It should then convert the float to a rational and raise the unit by the numerator and root it by the denominator
312
+ # but, sadly, floats can't be converted to rationals.
313
+ #
314
+ # For now, if a rational is passed in, it will be used, otherwise we are stuck with integers and certain floats < 1
264
315
  def **(other)
265
- raise ArgumentError, "Exponent must be Integer" unless Integer === other
266
- case
267
- when other.to_i > 0 : (1..other.to_i).inject(Unit.new("1")) {|product, n| product * self}
268
- when other.to_i == 0 : Unit.new("1")
269
- when other.to_i < 0 : (1..-other.to_i).inject(Unit.new("1")) {|product, n| product / self}
316
+ if Numeric === other
317
+ return Unit("1") if other.zero?
318
+ return self if other == 1
319
+ return self.inverse if other == -1
320
+ end
321
+ case other
322
+ when Rational:
323
+ self.power(other.numerator).root(other.denominator)
324
+ when Integer:
325
+ self.power(other)
326
+ when Float:
327
+ return self**(other.to_i) if other == other.to_i
328
+ valid = (1..9).map {|x| 1/x}
329
+ raise ArgumentError, "Not a n-th root (1..9), use 1/n" unless valid.include? other.abs
330
+ self.root((1/other).to_int)
331
+ else
332
+ raise ArgumentError, "Invalid Exponent"
270
333
  end
271
334
  end
272
335
 
336
+
337
+ # returns the unit raised to the n-th power. Integers only
338
+ def power(n)
339
+ raise ArgumentError, "Can only use Integer exponenents" unless Integer === n
340
+ return self if n == 1
341
+ return Unit("1") if n == 0
342
+ return self.inverse if n == -1
343
+ if n > 0 then
344
+ (1..n.to_i).inject(Unit.new("1")) {|product, x| product * self}
345
+ else
346
+ (1..-n.to_i).inject(Unit.new("1")) {|product, x| product / self}
347
+ end
348
+ end
349
+
350
+ # Calculates the n-th root of a unit, where n = (1..9)
351
+ # if n < 0, returns 1/unit^(1/n)
352
+ def root(n)
353
+ raise ArgumentError, "Exponent must an Integer" unless Integer === n
354
+ raise ArgumentError, "0th root undefined" if n == 0
355
+ return self if n == 1
356
+ return self.root(n.abs).inverse if n < 0
357
+
358
+ vec = self.unit_signature_vector
359
+ vec=vec.map {|x| x % n}
360
+ raise ArgumentError, "Illegal root" unless vec.max == 0
361
+ num = @numerator.clone
362
+ den = @denominator.clone
363
+
364
+ @numerator.uniq.each do |item|
365
+ x = num.find_all {|i| i==item}.size
366
+ r = ((x/n)*(n-1)).to_int
367
+ r.times {|x| num.delete_at(num.index(item))}
368
+ end
369
+
370
+ @denominator.uniq.each do |item|
371
+ x = den.find_all {|i| i==item}.size
372
+ r = ((x/n)*(n-1)).to_int
373
+ r.times {|x| den.delete_at(den.index(item))}
374
+ end
375
+ q = @scalar**(1/n)
376
+ Unit.new([q,num,den])
377
+ end
378
+
273
379
  # returns inverse of Unit (1/unit)
274
380
  def inverse
275
- (Unit.new("1") / self)
381
+ Unit("1") / self
276
382
  end
277
383
 
278
384
  # convert to a specified unit string or to the same units as another Unit
@@ -290,76 +396,78 @@ class Unit < Numeric
290
396
  #
291
397
  # Note that if temperature is part of a compound unit, the temperature will be treated as a differential
292
398
  # and the units will be scaled appropriately.
293
- def >>(other)
294
- case other.class.to_s
295
- when 'Unit': target = other
296
- when 'String': target = Unit.new(other)
297
- else
298
- raise ArgumentError, "Unknown target units"
299
- end
300
- raise ArgumentError, "Incompatible Units" unless self =~ target
301
- if target.signature == 400 then # special handling for temperature conversions
302
- q=case self.numerator[0]
303
- when '<celcius>':
304
- case target.numerator[0]
305
- when '<celcius>' : @quantity
306
- when '<kelvin>' : @quantity + 273.15
307
- when '<farenheit>': @quantity * (9.0/5.0) + 32.0
308
- when '<rankine>' : @quantity * (9.0/5.0) + 491.67
309
- else
310
- raise ArgumentError, "Unknown temperature conversion requested"
311
- end
312
- when '<kelvin>':
313
- case target.numerator[0]
314
- when '<celcius>' : @quantity - 273.15
315
- when '<kelvin>' : @quantity
316
- when '<farenheit>': @quantity * (9.0/5.0) - 459.67
317
- when '<rankine>' : @quantity * (9.0/5.0)
318
- else
319
- raise ArgumentError, "Unknown temperature conversion requested"
320
- end
321
- when '<farenheit>':
322
- case target.numerator[0]
323
- when '<celcius>' : (@quantity-32)*(5.0/9.0)
324
- when '<kelvin>' : (@quantity+459.67)*(5.0/9.0)
325
- when '<farenheit>': @quantity
326
- when '<rankine>' : @quantity + 459.67
327
- else
328
- raise ArgumentError, "Unknown temperature conversion requested"
329
- end
330
- when '<rankine>':
331
- case target.numerator[0]
332
- when '<celcius>' : @quantity*(5.0/9.0) -273.15
333
- when '<kelvin>' : @quantity*(5.0/9.0)
334
- when '<farenheit>': @quantity - 459.67
335
- when '<rankine>' : @quantity
399
+ def to(other)
400
+ return self if other.nil?
401
+ return self if TrueClass === other
402
+ return self if FalseClass === other
403
+ if String === other && other =~ /temp(K|C|R|F)/
404
+ raise ArgumentError, "Receiver is not a temperature unit" unless self.signature==400
405
+ #return self.to_base.to(other) unless self.is_base?
406
+ return self.to_base.to("tempF") if @numerator.size > 1 || @denominator != ['<1>']
407
+ q=case
408
+ when @numerator.include?('<celcius>'):
409
+ case other
410
+ when 'tempC' : @scalar
411
+ when 'tempK' : @scalar + 273.15
412
+ when 'tempF' : @scalar * (9.0/5.0) + 32.0
413
+ when 'tempR' : @scalar * (9.0/5.0) + 491.67
414
+ end
415
+ when @numerator.include?( '<kelvin>'):
416
+ case other
417
+ when 'tempC' : @scalar - 273.15
418
+ when 'tempK' : @scalar
419
+ when 'tempF' : @scalar * (9.0/5.0) - 459.67
420
+ when 'tempR' : @scalar * (9.0/5.0)
421
+ end
422
+ when @numerator.include?( '<farenheit>'):
423
+ case other
424
+ when 'tempC' : (@scalar-32)*(5.0/9.0)
425
+ when 'tempK' : (@scalar+459.67)*(5.0/9.0)
426
+ when 'tempF' : @scalar
427
+ when 'tempR' : @scalar + 459.67
428
+ end
429
+ when @numerator.include?( '<rankine>'):
430
+ case other
431
+ when 'tempC' : @scalar*(5.0/9.0) -273.15
432
+ when 'tempK' : @scalar*(5.0/9.0)
433
+ when 'tempF' : @scalar - 459.67
434
+ when 'tempR' : @scalar
435
+ end
336
436
  else
337
- raise ArgumentError, "Unknown temperature conversion requested"
338
- end
339
- else
340
- raise ArgumentError, "Unknown temperature conversion requested"
437
+ raise ArgumentError, "Unknown temperature conversion requested #{self.numerator}"
341
438
  end
342
- Unit.new(:quantity=>q, :numerator=>target.numerator, :denominator=>target.denominator)
439
+ Unit.new("#{q} deg#{$1}")
343
440
  else
344
- 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
345
- 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
441
+ case other
442
+ when Unit:
443
+ return self if other.units == self.units
444
+ target = other
445
+ when String: target = Unit.new(other)
446
+ else
447
+ raise ArgumentError, "Unknown target units"
448
+ end
449
+ raise ArgumentError, "Incompatible Units" unless self =~ target
450
+ 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)][:scalar] }.compact
451
+ 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)][:scalar] }.compact
346
452
  v = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n}
347
- 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
348
- 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
453
+ 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)][:scalar] }.compact
454
+ 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)][:scalar] }.compact
349
455
  y = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n}
350
- q = @quantity * v/y
351
- Unit.new(:quantity=>q, :numerator=>target.numerator, :denominator=>target.denominator)
456
+ q = @scalar * v/y
457
+ Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator)
352
458
  end
353
459
  end
354
460
 
461
+ alias :>> :to
355
462
  # calculates the unit signature vector used by unit_signature
356
463
  def unit_signature_vector
357
- result = self.to_base
464
+ return self.to_base.unit_signature_vector unless self.is_base?
465
+ result = self
358
466
  y = [:length, :time, :temperature, :mass, :current, :substance, :luminosity, :currency, :memory, :angle]
359
467
  vector = Array.new(y.size,0)
360
468
  y.each_with_index do |units,index|
361
- vector[index] = result.numerator.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size
362
- vector[index] -= result.denominator.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size
469
+ vector[index] = result.numerator.compact.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size
470
+ vector[index] -= result.denominator.compact.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size
363
471
  end
364
472
  vector
365
473
  end
@@ -379,135 +487,159 @@ class Unit < Numeric
379
487
  end
380
488
 
381
489
  # Eliminates terms in the passed numerator and denominator. Expands out prefixes and applies them to the
382
- # quantity. Returns a hash that can be used to initialize a new Unit object.
490
+ # scalar. Returns a hash that can be used to initialize a new Unit object.
383
491
  def self.eliminate_terms(q, n, d)
384
492
  num = n.clone
385
493
  den = d.clone
386
494
 
387
- # cancel terms in both numerator and denominator
388
- num.each_with_index do |item,index|
389
- if i=den.index(item)
390
- num.delete_at(index)
391
- den.delete_at(i)
495
+ num.delete_if {|v| v == '<1>'}
496
+ den.delete_if {|v| v == '<1>'}
497
+ combined = Hash.new(0)
498
+
499
+ i = 0
500
+ loop do
501
+ break if i > num.size
502
+ if @@PREFIX_VALUES.has_key? num[i]
503
+ k = [num[i],num[i+1]]
504
+ i += 2
505
+ else
506
+ k = num[i]
507
+ i += 1
392
508
  end
509
+ combined[k] += 1 unless k.nil? || k == '<1>'
393
510
  end
394
511
 
395
- num = num.flatten.compact
396
- den = den.flatten.compact
397
-
398
- # substitute in SI prefix multipliers and numerical constants
399
- num.each_with_index do |item, index|
400
- if item =~ /<([\dEe+-.]+)>/
401
- q *= $1.to_f
402
- num.delete_at(index)
403
- elsif multiplier=@@PREFIX_VALUES[Regexp.escape(item)]
404
- q *= multiplier
405
- num.delete_at(index)
406
- end
512
+ j = 0
513
+ loop do
514
+ break if j > den.size
515
+ if @@PREFIX_VALUES.has_key? den[j]
516
+ k = [den[j],den[j+1]]
517
+ j += 2
518
+ else
519
+ k = den[j]
520
+ j += 1
521
+ end
522
+ combined[k] -= 1 unless k.nil? || k == '<1>'
407
523
  end
408
-
409
- den.each_with_index do |item, index|
410
- if item =~ /<([\dEe+-.]+)>/
411
- q /= $1.to_f
412
- den.delete_at(index)
413
- elsif multiplier=@@PREFIX_VALUES[Regexp.escape(item)]
414
- q /= multiplier
415
- den.delete_at(index)
524
+
525
+ num = []
526
+ den = []
527
+ combined.each do |key,value|
528
+ case
529
+ when value > 0 : value.times {num << key}
530
+ when value < 0 : value.abs.times {den << key}
416
531
  end
417
532
  end
418
533
  num = ["<1>"] if num.empty?
419
- den = ["<1>"] if den.empty?
420
- {:quantity=>q, :numerator=>num, :denominator=>den}
534
+ den = ["<1>"] if den.empty?
535
+ {:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact}
421
536
  end
422
537
 
423
- # returns the quantity part of the Unit
538
+ # returns the scalar part of the Unit
424
539
  def to_f
425
- @quantity
540
+ return @scalar.to_f if self.unitless?
541
+ raise RuntimeError, "Can't convert to float unless unitless. Use Unit#scalar"
426
542
  end
427
543
 
428
- # returns the 'unit' part of the Unit object without the quantity
429
- def to_unit
544
+ # returns the 'unit' part of the Unit object without the scalar
545
+ def units
430
546
  return "" if @numerator == ["<1>"] && @denominator == ["<1>"]
431
- output_n = []
432
- num = @numerator.clone
433
- den = @denominator.clone
547
+ output_n = []
548
+ output_d =[]
549
+ num = @numerator.clone.compact
550
+ den = @denominator.clone.compact
434
551
  if @numerator == ["<1>"]
435
552
  output_n << "1"
436
553
  else
437
554
  num.each_with_index do |token,index|
438
555
  if token && @@PREFIX_VALUES[Regexp.escape(token)] then
439
- output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(@numerator[index+1])]}"
556
+ output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(num[index+1])]}"
440
557
  num[index+1]=nil
441
558
  else
442
559
  output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}" if token
443
560
  end
444
561
  end
445
562
  end
446
- output_d = den.map do |token|
447
- @@PREFIX_MAP[Regexp.escape(token)] ? @@OUTPUT_MAP[Regexp.escape(token)] : "#{@@OUTPUT_MAP[Regexp.escape(token)]} "
563
+ if @denominator == ['<1>']
564
+ output_d = ['1']
565
+ else
566
+ den.each_with_index do |token,index|
567
+ if token && @@PREFIX_VALUES[Regexp.escape(token)] then
568
+ output_d << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(den[index+1])]}"
569
+ den[index+1]=nil
570
+ else
571
+ output_d << "#{@@OUTPUT_MAP[Regexp.escape(token)]}" if token
572
+ end
573
+ end
448
574
  end
449
575
  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]}" : ''))}
450
576
  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]}" : ''))}
451
577
  "#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip
452
578
  end
453
579
 
454
- # negates the quantity of the Unit
580
+ # negates the scalar of the Unit
455
581
  def -@
456
- q = -self.quantity
457
- Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
582
+ Unit.new([-@scalar,@numerator,@denominator])
458
583
  end
459
584
 
460
- # returns abs of quantity, without the units
585
+ # returns abs of scalar, without the units
461
586
  def abs
462
- return @quantity.abs
587
+ return @scalar.abs
463
588
  end
464
589
 
465
590
  def ceil
466
- q = self.quantity.ceil
467
- Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
591
+ Unit.new([@scalar.ceil, @numerator, @denominator])
468
592
  end
469
593
 
470
594
  def floor
471
- q = self.quantity.floor
472
- Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
595
+ Unit.new([@scalar.floor, @numerator, @denominator])
473
596
  end
474
-
597
+
598
+ # changes internal scalar to an integer, but retains the units
599
+ # if unitless, returns an int
475
600
  def to_int
476
- q = self.quantity.to_int
477
- Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
601
+ return @scalar.to_int if unitless?
602
+ Unit.new([@scalar.to_int, @numerator, @denominator])
478
603
  end
479
604
 
605
+ def to_time
606
+ Time.at(self)
607
+ end
608
+ alias :time :to_time
480
609
  alias :to_i :to_int
481
610
  alias :truncate :to_int
482
611
 
483
612
  def round
484
- q = self.quantity.round
485
- Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
613
+ Unit.new([@scalar.round, @numerator, @denominator])
486
614
  end
487
615
 
488
- # true if quantity is zero
616
+ # true if scalar is zero
489
617
  def zero?
490
- return @quantity.zero?
618
+ return @scalar.zero?
491
619
  end
492
-
493
- # returns self if zero? is false, nil otherwise
494
- #def nonzero?
495
- # self.zero? ? nil : self
496
- #end
497
-
498
- def update_base_quantity
499
- @base_quantity = self.is_base? ? @quantity : self.to_base.quantity
500
- self
620
+
621
+ def succ
622
+ raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
623
+ q = @scalar.to_i.succ
624
+ Unit.new([q, @numerator, @denominator])
625
+ end
626
+
627
+ def update_base_scalar
628
+ if self.is_base?
629
+ @base_scalar = @scalar
630
+ @signature = unit_signature
631
+ else
632
+ base = self.to_base
633
+ @base_scalar = base.scalar
634
+ @signature = base.signature
635
+ end
501
636
  end
502
637
 
503
638
  def coerce(other)
504
639
  case other
505
640
  when Unit : [other, self]
506
- when String : [Unit.new(other), self]
507
- when Array: [Unit.new(other.join('*')), self]
508
- when Numeric : [Unit.new(other.to_s), self]
509
641
  else
510
- raise ArgumentError, "Invalid Unit Definition"
642
+ [Unit.new(other), self]
511
643
  end
512
644
  end
513
645
 
@@ -521,59 +653,58 @@ class Unit < Numeric
521
653
  # "2.2 kPa"
522
654
  # "37 degC"
523
655
  # "1" -- creates a unitless constant with value 1
524
- # "GPa" -- creates a unit with quantity 1 with units 'GPa'
656
+ # "GPa" -- creates a unit with scalar 1 with units 'GPa'
525
657
  # 6'4" -- recognized as 6 feet + 4 inches
526
658
  # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
527
659
  def parse(unit_string="0")
528
660
  @numerator = ['<1>']
529
661
  @denominator = ['<1>']
662
+ unit_string.gsub!(/[<>]/,"")
530
663
 
531
664
  # Special processing for unusual unit strings
532
665
  # feet -- 6'5"
533
- feet, inches = unit_string.scan(/(\d+)[\s*|'|ft|feet\s*](\d+)[\s*|"|in|inches]/)[0]
666
+ feet, inches = unit_string.scan(/(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/)[0]
534
667
  if (feet && inches)
535
668
  result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches")
536
- @quantity = result.quantity
669
+ @scalar = result.scalar
537
670
  @numerator = result.numerator
538
671
  @denominator = result.denominator
539
- @base_quantity = result.base_quantity
672
+ @base_scalar = result.base_scalar
540
673
  return self
541
674
  end
542
675
 
543
676
  # weight -- 8 lbs 12 oz
544
- pounds, oz = unit_string.scan(/(\d+)[\s|#|lbs|pounds|,]+(\d+)[\s*|oz|ounces]/)[0]
677
+ pounds, oz = unit_string.scan(/(\d+)\s*(?:#|lbs|pounds)+[\s,]*(\d+)\s*(?:oz|ounces)/)[0]
545
678
  if (pounds && oz)
546
679
  result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz")
547
- @quantity = result.quantity
680
+ @scalar = result.scalar
548
681
  @numerator = result.numerator
549
682
  @denominator = result.denominator
550
- @base_quantity = result.base_quantity
683
+ @base_scalar = result.base_scalar
551
684
  return self
552
685
  end
553
-
554
- @quantity, top, bottom = unit_string.scan(/([\dEe+.-]*)\s*([^\/]*)\/*(.+)*/)[0] #parse the string into parts
555
-
556
- top.scan(/([^ \*]+)\^([\d-]+)/).each do |item|
686
+ @scalar, top, bottom = unit_string.scan(/([\dEe+.-]*)\s*([^\/]*)\/*(.+)*/)[0] #parse the string into parts
687
+
688
+ top.scan(/([^ \*]+)(?:\^|\*\*)([\d-]+)/).each do |item|
557
689
  n = item[1].to_i
558
690
  x = "#{item[0]} "
559
691
  case
560
- when n>=0 : top.gsub!(/([^ \*]+)\^(\d+)/) {|s| x * n}
561
- when n<0 : bottom = "#{bottom} #{x * -n}"; top.gsub!("#{item[0]}^#{item[1]}","")
692
+ when n>=0 : top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) {|s| x * n}
693
+ when n<0 : bottom = "#{bottom} #{x * -n}"; top.gsub!(/#{item[0]}(\^|\*\*)#{n}/,"")
562
694
  end
563
695
  end
564
-
565
- bottom.gsub!(/([^* ]+)\^(\d+)/) {|s| "#{$1} " * $2.to_i} if bottom
566
-
567
- if @quantity.empty?
696
+
697
+ bottom.gsub!(/([^* ]+)(?:\^|\*\*)(\d+)/) {|s| "#{$1} " * $2.to_i} if bottom
698
+ if @scalar.empty?
568
699
  if top =~ /[\dEe+.-]+/
569
- @quantity = top.to_f # need this for 'number only' initialization
700
+ @scalar = top.to_f # need this for 'number only' initialization
570
701
  else
571
- @quantity = 1 # need this for 'unit only' intialization
702
+ @scalar = 1 # need this for 'unit only' intialization
572
703
  end
573
704
  else
574
- @quantity = @quantity.to_f
705
+ @scalar = @scalar.to_f
575
706
  end
576
-
707
+
577
708
  @numerator = top.scan(/((#{@@PREFIX_REGEX})*(#{@@UNIT_REGEX}))/).delete_if {|x| x.empty?}.compact if top
578
709
  @denominator = bottom.scan(/((#{@@PREFIX_REGEX})*(#{@@UNIT_REGEX}))/).delete_if {|x| x.empty?}.compact if bottom
579
710
 
@@ -590,10 +721,18 @@ class Unit < Numeric
590
721
  @numerator = ['<1>'] if @numerator.empty?
591
722
  @denominator = ['<1>'] if @denominator.empty?
592
723
  self
724
+ end
725
+ end
726
+
727
+ if defined? Uncertain
728
+ class Uncertain
729
+ def to_unit(other=nil)
730
+ other ? Unit.new(self).to(other) : Unit.new(self)
731
+ end
593
732
  end
594
-
595
733
  end
596
734
 
735
+
597
736
  class Object
598
737
  def Unit(other)
599
738
  other.to_unit
@@ -602,14 +741,15 @@ end
602
741
 
603
742
  class Numeric
604
743
  def to_unit(other = nil)
605
- other ? Unit.new(self.to_s) >> other : Unit.new(self.to_s)
744
+ other ? Unit.new(self) * Unit.new(other) : Unit.new(self)
606
745
  end
607
746
  alias :unit :to_unit
608
747
  end
609
748
 
749
+
610
750
  class Array
611
751
  def to_unit(other = nil)
612
- other ? Unit.new("#{self[0]} #{self[1]}/#{self[2]}") >> other : Unit.new("#{self[0]} #{self[1]}/#{self[2]}")
752
+ other ? Unit.new(self).to(other) : Unit.new(self)
613
753
  end
614
754
  alias :unit :to_unit
615
755
  end
@@ -619,4 +759,51 @@ class String
619
759
  other ? Unit.new(self) >> other : Unit.new(self)
620
760
  end
621
761
  alias :unit :to_unit
762
+ alias :unit_format :%
763
+
764
+ def %(*args)
765
+ if Unit === args[0]
766
+ args[0].to_s(self)
767
+ else
768
+ unit_format(*args)
769
+ end
770
+ end
771
+ end
772
+
773
+ class Time
774
+
775
+ class << self
776
+ alias unit_time_at at
777
+ end
778
+
779
+ def self.at(*args)
780
+ if Unit === args[0]
781
+ unit_time_at(args[0].to("s").scalar)
782
+ else
783
+ unit_time_at(*args)
784
+ end
785
+ end
786
+
787
+ def to_unit(other = "s")
788
+ other ? Unit.new(self.to_f) * Unit.new(other) : Unit.new(self.to_f)
789
+ end
790
+ alias :unit :to_unit
791
+
792
+ alias :unit_add :+
793
+ def +(other)
794
+ if Unit === other
795
+ self.unit + other
796
+ else
797
+ unit_add(other)
798
+ end
799
+ end
800
+
801
+ alias :unit_sub :-
802
+ def -(other)
803
+ if Unit === other
804
+ self.unit - other
805
+ else
806
+ unit_sub(other)
807
+ end
808
+ end
622
809
  end