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/CHANGELOG +52 -6
- data/README +1 -1
- data/lib/ruby-units.rb +383 -196
- data/lib/ruby_units.rb +1 -0
- data/lib/units.rb +13 -12
- data/test/test_ruby-units.rb +148 -59
- metadata +4 -3
data/lib/ruby-units.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
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)][:
|
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
|
-
|
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
|
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
|
-
@
|
108
|
+
@scalar = options[:scalar] || 1
|
89
109
|
@numerator = options[:numerator] || ["<1>"]
|
90
110
|
@denominator = options[:denominator] || []
|
91
|
-
when Array:
|
92
|
-
|
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.
|
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 = @
|
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)][:
|
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)][:
|
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
|
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 = (
|
183
|
+
inches = self.to("in").scalar
|
158
184
|
"#{(inches / 12).truncate}\'#{(inches % 12).round}\""
|
159
185
|
when :lbs:
|
160
|
-
ounces = (
|
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
|
-
|
165
|
-
|
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
|
-
|
175
|
-
|
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
|
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.
|
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.
|
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
|
-
|
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 = @
|
213
|
-
Unit.new(:
|
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
|
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 = @
|
229
|
-
Unit.new(:
|
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(@
|
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
|
296
|
+
# Throws an exception if divisor is 0
|
252
297
|
def /(other)
|
253
298
|
if Unit === other
|
254
|
-
|
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
|
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
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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
|
-
|
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
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
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(
|
439
|
+
Unit.new("#{q} deg#{$1}")
|
343
440
|
else
|
344
|
-
|
345
|
-
|
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)][:
|
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)][:
|
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 = @
|
351
|
-
Unit.new(:
|
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
|
-
|
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
|
-
#
|
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
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
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
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
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
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
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
|
-
{:
|
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
|
538
|
+
# returns the scalar part of the Unit
|
424
539
|
def to_f
|
425
|
-
@
|
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
|
429
|
-
def
|
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
|
-
|
433
|
-
|
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(
|
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
|
-
|
447
|
-
|
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
|
580
|
+
# negates the scalar of the Unit
|
455
581
|
def -@
|
456
|
-
|
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
|
585
|
+
# returns abs of scalar, without the units
|
461
586
|
def abs
|
462
|
-
return @
|
587
|
+
return @scalar.abs
|
463
588
|
end
|
464
589
|
|
465
590
|
def ceil
|
466
|
-
|
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
|
-
|
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
|
-
|
477
|
-
Unit.new(
|
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
|
-
|
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
|
616
|
+
# true if scalar is zero
|
489
617
|
def zero?
|
490
|
-
return @
|
618
|
+
return @scalar.zero?
|
491
619
|
end
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
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
|
-
|
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
|
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+)
|
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
|
-
@
|
669
|
+
@scalar = result.scalar
|
537
670
|
@numerator = result.numerator
|
538
671
|
@denominator = result.denominator
|
539
|
-
@
|
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+)
|
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
|
-
@
|
680
|
+
@scalar = result.scalar
|
548
681
|
@numerator = result.numerator
|
549
682
|
@denominator = result.denominator
|
550
|
-
@
|
683
|
+
@base_scalar = result.base_scalar
|
551
684
|
return self
|
552
685
|
end
|
553
|
-
|
554
|
-
|
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!(
|
561
|
-
when n<0 : bottom = "#{bottom} #{x * -n}"; top.gsub!(
|
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!(/([^* ]+)
|
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
|
-
@
|
700
|
+
@scalar = top.to_f # need this for 'number only' initialization
|
570
701
|
else
|
571
|
-
@
|
702
|
+
@scalar = 1 # need this for 'unit only' intialization
|
572
703
|
end
|
573
704
|
else
|
574
|
-
@
|
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
|
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(
|
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
|