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