ruby-units 0.1.1 → 0.2.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.
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