ruby-units 1.0.1 → 1.0.2
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.txt +146 -128
- data/Manifest.txt +11 -1
- data/Rakefile +2 -1
- data/lib/ruby-units.rb +10 -1413
- data/lib/ruby_units/array.rb +9 -0
- data/lib/ruby_units/complex.rb +10 -0
- data/lib/ruby_units/date.rb +51 -0
- data/lib/ruby_units/math.rb +87 -0
- data/lib/ruby_units/numeric.rb +8 -0
- data/lib/ruby_units/object.rb +8 -0
- data/lib/ruby_units/ruby-units.rb +1104 -0
- data/lib/ruby_units/string.rb +94 -0
- data/lib/ruby_units/time.rb +73 -0
- data/lib/{units.rb → ruby_units/units.rb} +12 -2
- data/lib/ruby_units.rb +11 -1
- data/test/test_ruby-units.rb +28 -23
- metadata +12 -3
data/lib/ruby-units.rb
CHANGED
@@ -1,1414 +1,11 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
# http://www.sciwerks.org
|
12
|
-
#
|
13
|
-
# mailto://kevin.olbrich+ruby-units@gmail.com
|
14
|
-
#
|
15
|
-
# See README for detailed usage instructions and examples
|
16
|
-
#
|
17
|
-
# ==Unit Definition Format
|
18
|
-
#
|
19
|
-
# '<name>' => [%w{prefered_name synonyms}, conversion_to_base, :classification, %w{<base> <units> <in> <numerator>} , %w{<base> <units> <in> <denominator>} ],
|
20
|
-
#
|
21
|
-
# Prefixes (e.g., a :prefix classification) get special handling
|
22
|
-
# Note: The accuracy of unit conversions depends on the precision of the conversion factor.
|
23
|
-
# If you have more accurate estimates for particular conversion factors, please send them
|
24
|
-
# to me and I will incorporate them into the next release. It is also incumbent on the end-user
|
25
|
-
# to ensure that the accuracy of any conversions is sufficient for their intended application.
|
26
|
-
#
|
27
|
-
# While there are a large number of unit specified in the base package,
|
28
|
-
# there are also a large number of units that are not included.
|
29
|
-
# This package covers nearly all SI, Imperial, and units commonly used
|
30
|
-
# in the United States. If your favorite units are not listed here, send me an email
|
31
|
-
#
|
32
|
-
# To add / override a unit definition, add a code block like this..
|
33
|
-
#
|
34
|
-
# class Unit < Numeric
|
35
|
-
# UNIT_DEFINITIONS = {
|
36
|
-
# <name>' => [%w{prefered_name synonyms}, conversion_to_base, :classification, %w{<base> <units> <in> <numerator>} , %w{<base> <units> <in> <denominator>} ]
|
37
|
-
# }
|
38
|
-
# end
|
39
|
-
# Unit.setup
|
40
|
-
class Unit < Numeric
|
41
|
-
require 'units'
|
42
|
-
# pre-generate hashes from unit definitions for performance.
|
43
|
-
VERSION = '1.0.1'
|
44
|
-
@@USER_DEFINITIONS = {}
|
45
|
-
@@PREFIX_VALUES = {}
|
46
|
-
@@PREFIX_MAP = {}
|
47
|
-
@@UNIT_MAP = {}
|
48
|
-
@@UNIT_VALUES = {}
|
49
|
-
@@OUTPUT_MAP = {}
|
50
|
-
@@BASE_UNITS = ['<meter>','<kilogram>','<second>','<mole>', '<farad>', '<ampere>','<radian>','<kelvin>','<byte>','<dollar>','<candela>','<each>','<steradian>','<decibel>']
|
51
|
-
UNITY = '<1>'
|
52
|
-
UNITY_ARRAY= [UNITY]
|
53
|
-
FEET_INCH_REGEX = /(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/
|
54
|
-
TIME_REGEX = /(\d+)*:(\d+)*:*(\d+)*[:,]*(\d+)*/
|
55
|
-
LBS_OZ_REGEX = /(\d+)\s*(?:#|lbs|pounds)+[\s,]*(\d+)\s*(?:oz|ounces)/
|
56
|
-
SCI_NUMBER = %r{([+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*)}
|
57
|
-
RATIONAL_NUMBER = /(\d+)\/(\d+)/
|
58
|
-
COMPLEX_NUMBER = /#{SCI_NUMBER}?#{SCI_NUMBER}i\b/
|
59
|
-
NUMBER_REGEX = /#{SCI_NUMBER}*\s*(.+)?/
|
60
|
-
UNIT_STRING_REGEX = /#{SCI_NUMBER}*\s*([^\/]*)\/*(.+)*/
|
61
|
-
TOP_REGEX = /([^ \*]+)(?:\^|\*\*)([\d-]+)/
|
62
|
-
BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/
|
63
|
-
UNCERTAIN_REGEX = /#{SCI_NUMBER}\s*\+\/-\s*#{SCI_NUMBER}\s(.+)/
|
64
|
-
COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(.+)?/
|
65
|
-
RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(.+)?/
|
66
|
-
KELVIN = ['<kelvin>']
|
67
|
-
FARENHEIT = ['<farenheit>']
|
68
|
-
RANKINE = ['<rankine>']
|
69
|
-
CELCIUS = ['<celcius>']
|
1
|
+
require 'ruby_units/array'
|
2
|
+
require 'ruby_units/date'
|
3
|
+
require 'ruby_units/time'
|
4
|
+
require 'ruby_units/math'
|
5
|
+
require 'ruby_units/numeric'
|
6
|
+
require 'ruby_units/object'
|
7
|
+
require 'ruby_units/string'
|
8
|
+
require 'ruby_units/complex'
|
9
|
+
require 'ruby_units/units'
|
10
|
+
require 'ruby_units/ruby-units'
|
70
11
|
|
71
|
-
SIGNATURE_VECTOR = [:length, :time, :temperature, :mass, :current, :substance, :luminosity, :currency, :memory, :angle, :capacitance]
|
72
|
-
@@KINDS = {
|
73
|
-
-312058=>:resistance,
|
74
|
-
-312038=>:inductance,
|
75
|
-
-152040=>:magnetism,
|
76
|
-
-152038=>:magnetism,
|
77
|
-
-152058=>:potential,
|
78
|
-
-39=>:acceleration,
|
79
|
-
-38=>:radiation,
|
80
|
-
-20=>:frequency,
|
81
|
-
-19=>:speed,
|
82
|
-
-18=>:viscosity,
|
83
|
-
0=>:unitless,
|
84
|
-
1=>:length,
|
85
|
-
2=>:area,
|
86
|
-
3=>:volume,
|
87
|
-
20=>:time,
|
88
|
-
400=>:temperature,
|
89
|
-
7942=>:power,
|
90
|
-
7959=>:pressure,
|
91
|
-
7962=>:energy,
|
92
|
-
7979=>:viscosity,
|
93
|
-
7981=>:force,
|
94
|
-
7997=>:mass_concentration,
|
95
|
-
8000=>:mass,
|
96
|
-
159999=>:magnetism,
|
97
|
-
160000=>:current,
|
98
|
-
160020=>:charge,
|
99
|
-
312058=>:resistance,
|
100
|
-
3199980=>:activity,
|
101
|
-
3199997=>:molar_concentration,
|
102
|
-
3200000=>:substance,
|
103
|
-
63999998=>:illuminance,
|
104
|
-
64000000=>:luminous_power,
|
105
|
-
1280000000=>:currency,
|
106
|
-
25600000000=>:memory,
|
107
|
-
511999999980=>:angular_velocity,
|
108
|
-
512000000000=>:angle,
|
109
|
-
10240000000000=>:capacitance,
|
110
|
-
}
|
111
|
-
|
112
|
-
@@cached_units = {}
|
113
|
-
@@base_unit_cache = {}
|
114
|
-
|
115
|
-
def self.setup
|
116
|
-
@@ALL_UNIT_DEFINITIONS = UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS)
|
117
|
-
for unit in (@@ALL_UNIT_DEFINITIONS) do
|
118
|
-
key, value = unit
|
119
|
-
if value[2] == :prefix then
|
120
|
-
@@PREFIX_VALUES[key]=value[1]
|
121
|
-
for name in value[0] do
|
122
|
-
@@PREFIX_MAP[name]=key
|
123
|
-
end
|
124
|
-
else
|
125
|
-
@@UNIT_VALUES[key]={}
|
126
|
-
@@UNIT_VALUES[key][:scalar]=value[1]
|
127
|
-
@@UNIT_VALUES[key][:numerator]=value[3] if value[3]
|
128
|
-
@@UNIT_VALUES[key][:denominator]=value[4] if value[4]
|
129
|
-
for name in value[0] do
|
130
|
-
@@UNIT_MAP[name]=key
|
131
|
-
end
|
132
|
-
end
|
133
|
-
@@OUTPUT_MAP[key]=value[0][0]
|
134
|
-
end
|
135
|
-
@@PREFIX_REGEX = @@PREFIX_MAP.keys.sort_by {|prefix| prefix.length}.reverse.join('|')
|
136
|
-
@@UNIT_REGEX = @@UNIT_MAP.keys.sort_by {|unit| unit.length}.reverse.join('|')
|
137
|
-
@@UNIT_MATCH_REGEX = /(#{@@PREFIX_REGEX})*?(#{@@UNIT_REGEX})\b/
|
138
|
-
Unit.new(1)
|
139
|
-
end
|
140
|
-
|
141
|
-
|
142
|
-
include Comparable
|
143
|
-
attr_accessor :scalar, :numerator, :denominator, :signature, :base_scalar, :base_numerator, :base_denominator, :output, :unit_name
|
144
|
-
|
145
|
-
def to_yaml_properties
|
146
|
-
%w{@scalar @numerator @denominator @signature @base_scalar}
|
147
|
-
end
|
148
|
-
|
149
|
-
def copy(from)
|
150
|
-
@scalar = from.scalar
|
151
|
-
@numerator = from.numerator
|
152
|
-
@denominator = from.denominator
|
153
|
-
@is_base = from.is_base?
|
154
|
-
@signature = from.signature
|
155
|
-
@base_scalar = from.base_scalar
|
156
|
-
@output = from.output rescue nil
|
157
|
-
@unit_name = from.unit_name rescue nil
|
158
|
-
end
|
159
|
-
|
160
|
-
# basically a copy of the basic to_yaml. Needed because otherwise it ends up coercing the object to a string
|
161
|
-
# before YAML'izing it.
|
162
|
-
def to_yaml( opts = {} )
|
163
|
-
YAML::quick_emit( object_id, opts ) do |out|
|
164
|
-
out.map( taguri, to_yaml_style ) do |map|
|
165
|
-
for m in to_yaml_properties do
|
166
|
-
map.add( m[1..-1], instance_variable_get( m ) )
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
# Create a new Unit object. Can be initialized using a string, or a hash
|
173
|
-
# Valid formats include:
|
174
|
-
# "5.6 kg*m/s^2"
|
175
|
-
# "5.6 kg*m*s^-2"
|
176
|
-
# "5.6 kilogram*meter*second^-2"
|
177
|
-
# "2.2 kPa"
|
178
|
-
# "37 degC"
|
179
|
-
# "1" -- creates a unitless constant with value 1
|
180
|
-
# "GPa" -- creates a unit with scalar 1 with units 'GPa'
|
181
|
-
# 6'4" -- recognized as 6 feet + 4 inches
|
182
|
-
# 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
|
183
|
-
#
|
184
|
-
def initialize(*options)
|
185
|
-
@scalar = nil
|
186
|
-
@base_scalar = nil
|
187
|
-
@unit_name = nil
|
188
|
-
@signature = nil
|
189
|
-
@output = nil
|
190
|
-
if options.size == 2
|
191
|
-
begin
|
192
|
-
cached = @@cached_units[options[1]] * options[0]
|
193
|
-
copy(cached)
|
194
|
-
rescue
|
195
|
-
initialize("#{options[0]} #{(options[1].units rescue options[1])}")
|
196
|
-
end
|
197
|
-
return
|
198
|
-
end
|
199
|
-
if options.size == 3
|
200
|
-
begin
|
201
|
-
cached = @@cached_units["#{options[1]}/#{options[2]}"] * options[0]
|
202
|
-
copy(cached)
|
203
|
-
rescue
|
204
|
-
initialize("#{options[0]} #{options[1]}/#{options[2]}")
|
205
|
-
end
|
206
|
-
return
|
207
|
-
end
|
208
|
-
|
209
|
-
|
210
|
-
case options[0]
|
211
|
-
when Hash:
|
212
|
-
@scalar = options[0][:scalar] || 1
|
213
|
-
@numerator = options[0][:numerator] || UNITY_ARRAY
|
214
|
-
@denominator = options[0][:denominator] || UNITY_ARRAY
|
215
|
-
@signature = options[0][:signature]
|
216
|
-
when Array:
|
217
|
-
initialize(*options[0])
|
218
|
-
return
|
219
|
-
when Numeric:
|
220
|
-
@scalar = options[0]
|
221
|
-
@numerator = @denominator = UNITY_ARRAY
|
222
|
-
when Time:
|
223
|
-
@scalar = options[0].to_f
|
224
|
-
@numerator = ['<second>']
|
225
|
-
@denominator = UNITY_ARRAY
|
226
|
-
when DateTime:
|
227
|
-
@scalar = options[0].ajd
|
228
|
-
@numerator = ['<day>']
|
229
|
-
@denominator = UNITY_ARRAY
|
230
|
-
when "": raise ArgumentError, "No Unit Specified"
|
231
|
-
when String: parse(options[0])
|
232
|
-
else
|
233
|
-
raise ArgumentError, "Invalid Unit Format"
|
234
|
-
end
|
235
|
-
self.update_base_scalar
|
236
|
-
self.replace_temperature
|
237
|
-
|
238
|
-
unary_unit = self.units || ""
|
239
|
-
opt_units = options[0].scan(NUMBER_REGEX)[0][1] if String === options[0]
|
240
|
-
unless @@cached_units.keys.include?(opt_units) || (opt_units =~ /(temp|deg(C|K|R|F))|(pounds|lbs[ ,]\d+ ounces|oz)|('\d+")|(ft|feet[ ,]\d+ in|inch|inches)|%|(#{TIME_REGEX})|i\s?(.+)?/)
|
241
|
-
@@cached_units[opt_units] = (self.scalar == 1 ? self : opt_units.unit) if opt_units && !opt_units.empty?
|
242
|
-
end
|
243
|
-
unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /(temp|deg)(C|K|R|F)/) then
|
244
|
-
@@cached_units[unary_unit] = (self.scalar == 1 ? self : unary_unit.unit)
|
245
|
-
end
|
246
|
-
[@scalar, @numerator, @denominator, @base_scalar, @signature, @is_base].each {|x| x.freeze}
|
247
|
-
self
|
248
|
-
end
|
249
|
-
|
250
|
-
def kind
|
251
|
-
return @@KINDS[self.signature]
|
252
|
-
end
|
253
|
-
|
254
|
-
def self.cached
|
255
|
-
return @@cached_units
|
256
|
-
end
|
257
|
-
|
258
|
-
def self.clear_cache
|
259
|
-
@@cached_units = {}
|
260
|
-
@@base_unit_cache = {}
|
261
|
-
end
|
262
|
-
|
263
|
-
def self.base_unit_cache
|
264
|
-
return @@base_unit_cache
|
265
|
-
end
|
266
|
-
|
267
|
-
def to_unit
|
268
|
-
self
|
269
|
-
end
|
270
|
-
alias :unit :to_unit
|
271
|
-
|
272
|
-
# Returns 'true' if the Unit is represented in base units
|
273
|
-
def is_base?
|
274
|
-
return @is_base if defined? @is_base
|
275
|
-
return @is_base=true if @signature == 400 && @numerator.size == 1 && @numerator[0] =~ /(celcius|kelvin|farenheit|rankine)/
|
276
|
-
n = @numerator + @denominator
|
277
|
-
for x in n.compact do
|
278
|
-
return @is_base=false unless x == UNITY || (@@BASE_UNITS.include?((x)))
|
279
|
-
end
|
280
|
-
return @is_base = true
|
281
|
-
end
|
282
|
-
|
283
|
-
# convert to base SI units
|
284
|
-
# results of the conversion are cached so subsequent calls to this will be fast
|
285
|
-
def to_base
|
286
|
-
return self if self.is_base?
|
287
|
-
cached = @@base_unit_cache[self.units] * self.scalar rescue nil
|
288
|
-
return cached if cached
|
289
|
-
|
290
|
-
num = []
|
291
|
-
den = []
|
292
|
-
q = 1
|
293
|
-
for unit in @numerator.compact do
|
294
|
-
if @@PREFIX_VALUES[unit]
|
295
|
-
q *= @@PREFIX_VALUES[unit]
|
296
|
-
else
|
297
|
-
q *= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit]
|
298
|
-
num << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator]
|
299
|
-
den << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator]
|
300
|
-
end
|
301
|
-
end
|
302
|
-
for unit in @denominator.compact do
|
303
|
-
if @@PREFIX_VALUES[unit]
|
304
|
-
q /= @@PREFIX_VALUES[unit]
|
305
|
-
else
|
306
|
-
q /= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit]
|
307
|
-
den << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator]
|
308
|
-
num << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator]
|
309
|
-
end
|
310
|
-
end
|
311
|
-
|
312
|
-
num = num.flatten.compact
|
313
|
-
den = den.flatten.compact
|
314
|
-
num = UNITY_ARRAY if num.empty?
|
315
|
-
base= Unit.new(Unit.eliminate_terms(q,num,den))
|
316
|
-
@@base_unit_cache[self.units]=base
|
317
|
-
return base * @scalar
|
318
|
-
end
|
319
|
-
|
320
|
-
# Generate human readable output.
|
321
|
-
# If the name of a unit is passed, the unit will first be converted to the target unit before output.
|
322
|
-
# some named conversions are available
|
323
|
-
#
|
324
|
-
# :ft - outputs in feet and inches (e.g., 6'4")
|
325
|
-
# :lbs - outputs in pounds and ounces (e.g, 8 lbs, 8 oz)
|
326
|
-
#
|
327
|
-
# You can also pass a standard format string (i.e., '%0.2f')
|
328
|
-
# or a strftime format string.
|
329
|
-
#
|
330
|
-
# output is cached so subsequent calls for the same format will be fast
|
331
|
-
#
|
332
|
-
def to_s(target_units=nil)
|
333
|
-
out = @output[target_units] rescue nil
|
334
|
-
if out
|
335
|
-
return out
|
336
|
-
else
|
337
|
-
case target_units
|
338
|
-
when :ft:
|
339
|
-
inches = self.to("in").scalar.to_int
|
340
|
-
out = "#{(inches / 12).truncate}\'#{(inches % 12).round}\""
|
341
|
-
when :lbs:
|
342
|
-
ounces = self.to("oz").scalar.to_int
|
343
|
-
out = "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz"
|
344
|
-
when String
|
345
|
-
begin #first try a standard format string
|
346
|
-
target_units =~ /(%[\w\d#+-.]*)*\s*(.+)*/
|
347
|
-
out = $2 ? self.to($2).to_s($1) : "#{($1 || '%g') % @scalar || 0} #{self.units}".strip
|
348
|
-
rescue #if that is malformed, try a time string
|
349
|
-
out = (Time.gm(0) + self).strftime(target_units)
|
350
|
-
end
|
351
|
-
else
|
352
|
-
out = case @scalar
|
353
|
-
when Rational :
|
354
|
-
"#{@scalar} #{self.units}"
|
355
|
-
else
|
356
|
-
"#{'%g' % @scalar} #{self.units}"
|
357
|
-
end.strip
|
358
|
-
end
|
359
|
-
@output = {target_units => out}
|
360
|
-
return out
|
361
|
-
end
|
362
|
-
end
|
363
|
-
|
364
|
-
# Normally pretty prints the unit, but if you really want to see the guts of it, pass ':dump'
|
365
|
-
def inspect(option=nil)
|
366
|
-
return super() if option == :dump
|
367
|
-
self.to_s
|
368
|
-
end
|
369
|
-
|
370
|
-
# returns true if no associated units
|
371
|
-
# false, even if the units are "unitless" like 'radians, each, etc'
|
372
|
-
def unitless?
|
373
|
-
(@numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY)
|
374
|
-
end
|
375
|
-
|
376
|
-
# Compare two Unit objects. Throws an exception if they are not of compatible types.
|
377
|
-
# Comparisons are done based on the value of the unit in base SI units.
|
378
|
-
def <=>(other)
|
379
|
-
case other
|
380
|
-
when Unit:
|
381
|
-
raise ArgumentError, "Incompatible Units" unless self =~ other
|
382
|
-
self.base_scalar <=> other.base_scalar
|
383
|
-
else
|
384
|
-
x,y = coerce(other)
|
385
|
-
x <=> y
|
386
|
-
end
|
387
|
-
end
|
388
|
-
|
389
|
-
# check to see if units are compatible, but not the scalar part
|
390
|
-
# this check is done by comparing signatures for performance reasons
|
391
|
-
# if passed a string, it will create a unit object with the string and then do the comparison
|
392
|
-
# this permits a syntax like:
|
393
|
-
# unit =~ "mm"
|
394
|
-
# if you want to do a regexp on the unit string do this ...
|
395
|
-
# unit.units =~ /regexp/
|
396
|
-
def =~(other)
|
397
|
-
case other
|
398
|
-
when Unit : self.signature == other.signature
|
399
|
-
else
|
400
|
-
x,y = coerce(other)
|
401
|
-
x =~ y
|
402
|
-
end
|
403
|
-
end
|
404
|
-
|
405
|
-
alias :compatible? :=~
|
406
|
-
alias :compatible_with? :=~
|
407
|
-
|
408
|
-
# Compare two units. Returns true if quantities and units match
|
409
|
-
#
|
410
|
-
# Unit("100 cm") === Unit("100 cm") # => true
|
411
|
-
# Unit("100 cm") === Unit("1 m") # => false
|
412
|
-
def ===(other)
|
413
|
-
case other
|
414
|
-
when Unit: (self.scalar == other.scalar) && (self.units == other.units)
|
415
|
-
else
|
416
|
-
x,y = coerce(other)
|
417
|
-
x === y
|
418
|
-
end
|
419
|
-
end
|
420
|
-
|
421
|
-
alias :same? :===
|
422
|
-
alias :same_as? :===
|
423
|
-
|
424
|
-
# Add two units together. Result is same units as receiver and scalar and base_scalar are updated appropriately
|
425
|
-
# throws an exception if the units are not compatible.
|
426
|
-
# It is possible to add Time objects to units of time
|
427
|
-
def +(other)
|
428
|
-
if Unit === other
|
429
|
-
if self =~ other then
|
430
|
-
@q ||= @@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar
|
431
|
-
Unit.new(:scalar=>(self.base_scalar + other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
|
432
|
-
else
|
433
|
-
raise ArgumentError, "Incompatible Units"
|
434
|
-
end
|
435
|
-
elsif Time === other
|
436
|
-
other + self
|
437
|
-
else
|
438
|
-
x,y = coerce(other)
|
439
|
-
y + x
|
440
|
-
end
|
441
|
-
end
|
442
|
-
|
443
|
-
# Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately
|
444
|
-
# throws an exception if the units are not compatible.
|
445
|
-
def -(other)
|
446
|
-
if Unit === other
|
447
|
-
if self =~ other then
|
448
|
-
@q ||= @@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar
|
449
|
-
Unit.new(:scalar=>(self.base_scalar - other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature=>@signature)
|
450
|
-
else
|
451
|
-
raise ArgumentError, "Incompatible Units"
|
452
|
-
end
|
453
|
-
elsif Time === other
|
454
|
-
other - self
|
455
|
-
else
|
456
|
-
x,y = coerce(other)
|
457
|
-
y-x
|
458
|
-
end
|
459
|
-
end
|
460
|
-
|
461
|
-
# Multiply two units.
|
462
|
-
def *(other)
|
463
|
-
case other
|
464
|
-
when Unit
|
465
|
-
opts = Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator)
|
466
|
-
opts.merge!(:signature => @signature + other.signature)
|
467
|
-
Unit.new(opts)
|
468
|
-
when Numeric
|
469
|
-
Unit.new(:scalar=>@scalar*other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
|
470
|
-
else
|
471
|
-
x,y = coerce(other)
|
472
|
-
x * y
|
473
|
-
end
|
474
|
-
end
|
475
|
-
|
476
|
-
# Divide two units.
|
477
|
-
# Throws an exception if divisor is 0
|
478
|
-
def /(other)
|
479
|
-
case other
|
480
|
-
when Unit
|
481
|
-
raise ZeroDivisionError if other.zero?
|
482
|
-
opts = Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator)
|
483
|
-
opts.merge!(:signature=> @signature - other.signature)
|
484
|
-
Unit.new(opts)
|
485
|
-
when Numeric
|
486
|
-
raise ZeroDivisionError if other.zero?
|
487
|
-
Unit.new(:scalar=>@scalar/other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
|
488
|
-
else
|
489
|
-
x,y = coerce(other)
|
490
|
-
y / x
|
491
|
-
end
|
492
|
-
end
|
493
|
-
|
494
|
-
# Exponentiate. Only takes integer powers.
|
495
|
-
# Note that anything raised to the power of 0 results in a Unit object with a scalar of 1, and no units.
|
496
|
-
# Throws an exception if exponent is not an integer.
|
497
|
-
# Ideally this routine should accept a float for the exponent
|
498
|
-
# It should then convert the float to a rational and raise the unit by the numerator and root it by the denominator
|
499
|
-
# but, sadly, floats can't be converted to rationals.
|
500
|
-
#
|
501
|
-
# For now, if a rational is passed in, it will be used, otherwise we are stuck with integers and certain floats < 1
|
502
|
-
def **(other)
|
503
|
-
if Numeric === other
|
504
|
-
return Unit("1") if other.zero?
|
505
|
-
return self if other == 1
|
506
|
-
return self.inverse if other == -1
|
507
|
-
end
|
508
|
-
case other
|
509
|
-
when Rational:
|
510
|
-
self.power(other.numerator).root(other.denominator)
|
511
|
-
when Integer:
|
512
|
-
self.power(other)
|
513
|
-
when Float:
|
514
|
-
return self**(other.to_i) if other == other.to_i
|
515
|
-
valid = (1..9).map {|x| 1/x}
|
516
|
-
raise ArgumentError, "Not a n-th root (1..9), use 1/n" unless valid.include? other.abs
|
517
|
-
self.root((1/other).to_int)
|
518
|
-
else
|
519
|
-
raise ArgumentError, "Invalid Exponent"
|
520
|
-
end
|
521
|
-
end
|
522
|
-
|
523
|
-
def real
|
524
|
-
return Unit.new(self.scalar.real,self.units)
|
525
|
-
end
|
526
|
-
|
527
|
-
def imag
|
528
|
-
return Unit.new(self.scalar.imag, self.units)
|
529
|
-
end
|
530
|
-
|
531
|
-
# returns the unit raised to the n-th power. Integers only
|
532
|
-
def power(n)
|
533
|
-
raise ArgumentError, "Can only use Integer exponenents" unless Integer === n
|
534
|
-
return self if n == 1
|
535
|
-
return Unit("1") if n == 0
|
536
|
-
return self.inverse if n == -1
|
537
|
-
if n > 0 then
|
538
|
-
(1..n.to_i).inject(Unit.new("1")) {|product, x| product * self}
|
539
|
-
else
|
540
|
-
(1..-n.to_i).inject(Unit.new("1")) {|product, x| product / self}
|
541
|
-
end
|
542
|
-
end
|
543
|
-
|
544
|
-
# Calculates the n-th root of a unit, where n = (1..9)
|
545
|
-
# if n < 0, returns 1/unit^(1/n)
|
546
|
-
def root(n)
|
547
|
-
raise ArgumentError, "Exponent must an Integer" unless Integer === n
|
548
|
-
raise ArgumentError, "0th root undefined" if n == 0
|
549
|
-
return self if n == 1
|
550
|
-
return self.root(n.abs).inverse if n < 0
|
551
|
-
|
552
|
-
vec = self.unit_signature_vector
|
553
|
-
vec=vec.map {|x| x % n}
|
554
|
-
raise ArgumentError, "Illegal root" unless vec.max == 0
|
555
|
-
num = @numerator.dup
|
556
|
-
den = @denominator.dup
|
557
|
-
|
558
|
-
for item in @numerator.uniq do
|
559
|
-
x = num.find_all {|i| i==item}.size
|
560
|
-
r = ((x/n)*(n-1)).to_int
|
561
|
-
r.times {|x| num.delete_at(num.index(item))}
|
562
|
-
end
|
563
|
-
|
564
|
-
for item in @denominator.uniq do
|
565
|
-
x = den.find_all {|i| i==item}.size
|
566
|
-
r = ((x/n)*(n-1)).to_int
|
567
|
-
r.times {|x| den.delete_at(den.index(item))}
|
568
|
-
end
|
569
|
-
q = @scalar < 0 ? (-1)**Rational(1,n) * (@scalar.abs)**Rational(1,n) : @scalar**Rational(1,n)
|
570
|
-
Unit.new(:scalar=>q,:numerator=>num,:denominator=>den)
|
571
|
-
end
|
572
|
-
|
573
|
-
# returns inverse of Unit (1/unit)
|
574
|
-
def inverse
|
575
|
-
Unit("1") / self
|
576
|
-
end
|
577
|
-
|
578
|
-
# convert to a specified unit string or to the same units as another Unit
|
579
|
-
#
|
580
|
-
# unit >> "kg" will covert to kilograms
|
581
|
-
# unit1 >> unit2 converts to same units as unit2 object
|
582
|
-
#
|
583
|
-
# To convert a Unit object to match another Unit object, use:
|
584
|
-
# unit1 >>= unit2
|
585
|
-
# Throws an exception if the requested target units are incompatible with current Unit.
|
586
|
-
#
|
587
|
-
# Special handling for temperature conversions is supported. If the Unit object is converted
|
588
|
-
# from one temperature unit to another, the proper temperature offsets will be used.
|
589
|
-
# Supports Kelvin, Celcius, Farenheit, and Rankine scales.
|
590
|
-
#
|
591
|
-
# Note that if temperature is part of a compound unit, the temperature will be treated as a differential
|
592
|
-
# and the units will be scaled appropriately.
|
593
|
-
def to(other)
|
594
|
-
return self if other.nil?
|
595
|
-
return self if TrueClass === other
|
596
|
-
return self if FalseClass === other
|
597
|
-
if (Unit === other && other.units =~ /temp(K|C|R|F)/) || (String === other && other =~ /temp(K|C|R|F)/)
|
598
|
-
raise ArgumentError, "Receiver is not a temperature unit" unless self.signature==400
|
599
|
-
start_unit = self.units
|
600
|
-
target_unit = other.units rescue other
|
601
|
-
q=case start_unit
|
602
|
-
when 'degC':
|
603
|
-
case target_unit
|
604
|
-
when 'tempC' : @scalar
|
605
|
-
when 'tempK' : @scalar + 273.15
|
606
|
-
when 'tempF' : @scalar * (9.0/5.0) + 32.0
|
607
|
-
when 'tempR' : @scalar * (9.0/5.0) + 491.67
|
608
|
-
end
|
609
|
-
when 'degK':
|
610
|
-
case target_unit
|
611
|
-
when 'tempC' : @scalar - 273.15
|
612
|
-
when 'tempK' : @scalar
|
613
|
-
when 'tempF' : @scalar * (9.0/5.0) - 459.67
|
614
|
-
when 'tempR' : @scalar * (9.0/5.0)
|
615
|
-
end
|
616
|
-
when 'degF':
|
617
|
-
case target_unit
|
618
|
-
when 'tempC' : (@scalar-32)*(5.0/9.0)
|
619
|
-
when 'tempK' : (@scalar+459.67)*(5.0/9.0)
|
620
|
-
when 'tempF' : @scalar
|
621
|
-
when 'tempR' : @scalar + 459.67
|
622
|
-
end
|
623
|
-
when 'degR':
|
624
|
-
case target_unit
|
625
|
-
when 'tempC' : @scalar*(5.0/9.0) -273.15
|
626
|
-
when 'tempK' : @scalar*(5.0/9.0)
|
627
|
-
when 'tempF' : @scalar - 459.67
|
628
|
-
when 'tempR' : @scalar
|
629
|
-
end
|
630
|
-
else
|
631
|
-
return self.to_base.to(other) unless self.is_base?
|
632
|
-
#raise ArgumentError, "Unknown temperature conversion requested #{self.numerator}"
|
633
|
-
end
|
634
|
-
target_unit =~ /temp(C|K|F|R)/
|
635
|
-
Unit.new("#{q} deg#{$1}")
|
636
|
-
else
|
637
|
-
case other
|
638
|
-
when Unit:
|
639
|
-
return self if other.units == self.units
|
640
|
-
target = other
|
641
|
-
when String: target = Unit.new(other)
|
642
|
-
else
|
643
|
-
raise ArgumentError, "Unknown target units"
|
644
|
-
end
|
645
|
-
raise ArgumentError, "Incompatible Units" unless self =~ target
|
646
|
-
one = @numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact
|
647
|
-
two = @denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact
|
648
|
-
v = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n}
|
649
|
-
one = target.numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact
|
650
|
-
two = target.denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact
|
651
|
-
y = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n}
|
652
|
-
q = @scalar * v/y
|
653
|
-
Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator, :signature => target.signature)
|
654
|
-
end
|
655
|
-
end
|
656
|
-
alias :>> :to
|
657
|
-
alias :convert_to :to
|
658
|
-
|
659
|
-
# converts the unit back to a float if it is unitless. Otherwise raises an exception
|
660
|
-
def to_f
|
661
|
-
return @scalar.to_f if self.unitless?
|
662
|
-
raise RuntimeError, "Can't convert to Float unless unitless. Use Unit#scalar"
|
663
|
-
end
|
664
|
-
|
665
|
-
# converts the unit back to a complex if it is unitless. Otherwise raises an exception
|
666
|
-
|
667
|
-
def to_c
|
668
|
-
return Complex(@scalar) if self.unitless?
|
669
|
-
raise RuntimeError, "Can't convert to Complex unless unitless. Use Unit#scalar"
|
670
|
-
end
|
671
|
-
|
672
|
-
# returns the 'unit' part of the Unit object without the scalar
|
673
|
-
def units
|
674
|
-
return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
|
675
|
-
return @unit_name unless @unit_name.nil?
|
676
|
-
output_n = []
|
677
|
-
output_d =[]
|
678
|
-
num = @numerator.clone.compact
|
679
|
-
den = @denominator.clone.compact
|
680
|
-
if @numerator == UNITY_ARRAY
|
681
|
-
output_n << "1"
|
682
|
-
else
|
683
|
-
num.each_with_index do |token,index|
|
684
|
-
if token && @@PREFIX_VALUES[token] then
|
685
|
-
output_n << "#{@@OUTPUT_MAP[token]}#{@@OUTPUT_MAP[num[index+1]]}"
|
686
|
-
num[index+1]=nil
|
687
|
-
else
|
688
|
-
output_n << "#{@@OUTPUT_MAP[token]}" if token
|
689
|
-
end
|
690
|
-
end
|
691
|
-
end
|
692
|
-
if @denominator == UNITY_ARRAY
|
693
|
-
output_d = ['1']
|
694
|
-
else
|
695
|
-
den.each_with_index do |token,index|
|
696
|
-
if token && @@PREFIX_VALUES[token] then
|
697
|
-
output_d << "#{@@OUTPUT_MAP[token]}#{@@OUTPUT_MAP[den[index+1]]}"
|
698
|
-
den[index+1]=nil
|
699
|
-
else
|
700
|
-
output_d << "#{@@OUTPUT_MAP[token]}" if token
|
701
|
-
end
|
702
|
-
end
|
703
|
-
end
|
704
|
-
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]}" : ''))}
|
705
|
-
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]}" : ''))}
|
706
|
-
out = "#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip
|
707
|
-
@unit_name = out unless self.kind == :temperature
|
708
|
-
return out
|
709
|
-
end
|
710
|
-
|
711
|
-
# negates the scalar of the Unit
|
712
|
-
def -@
|
713
|
-
return -@scalar if self.unitless?
|
714
|
-
#Unit.new(-@scalar,@numerator,@denominator)
|
715
|
-
-1 * self.dup
|
716
|
-
end
|
717
|
-
|
718
|
-
# returns abs of scalar, without the units
|
719
|
-
def abs
|
720
|
-
return @scalar.abs
|
721
|
-
end
|
722
|
-
|
723
|
-
def ceil
|
724
|
-
return @scalar.ceil if self.unitless?
|
725
|
-
Unit.new(@scalar.ceil, @numerator, @denominator)
|
726
|
-
end
|
727
|
-
|
728
|
-
def floor
|
729
|
-
return @scalar.floor if self.unitless?
|
730
|
-
Unit.new(@scalar.floor, @numerator, @denominator)
|
731
|
-
end
|
732
|
-
|
733
|
-
# if unitless, returns an int, otherwise raises an error
|
734
|
-
def to_i
|
735
|
-
return @scalar.to_int if self.unitless?
|
736
|
-
raise RuntimeError, 'Cannot convert to Integer unless unitless'
|
737
|
-
end
|
738
|
-
alias :to_int :to_i
|
739
|
-
|
740
|
-
# Tries to make a Time object from current unit. Assumes the current unit hold the duration in seconds from the epoch.
|
741
|
-
def to_time
|
742
|
-
Time.at(self)
|
743
|
-
end
|
744
|
-
alias :time :to_time
|
745
|
-
|
746
|
-
def truncate
|
747
|
-
return @scalar.truncate if self.unitless?
|
748
|
-
Unit.new(@scalar.truncate, @numerator, @denominator)
|
749
|
-
end
|
750
|
-
|
751
|
-
# convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date
|
752
|
-
# defined by DateTime
|
753
|
-
def to_datetime
|
754
|
-
DateTime.new(self.to('d').scalar)
|
755
|
-
end
|
756
|
-
|
757
|
-
def round
|
758
|
-
return @scalar.round if self.unitless?
|
759
|
-
Unit.new(@scalar.round, @numerator, @denominator)
|
760
|
-
end
|
761
|
-
|
762
|
-
# true if scalar is zero
|
763
|
-
def zero?
|
764
|
-
return @scalar.zero?
|
765
|
-
end
|
766
|
-
|
767
|
-
# '5 min'.unit.ago
|
768
|
-
def ago
|
769
|
-
self.before
|
770
|
-
end
|
771
|
-
|
772
|
-
# '5 min'.before(time)
|
773
|
-
def before(time_point = ::Time.now)
|
774
|
-
raise ArgumentError, "Must specify a Time" unless time_point
|
775
|
-
if String === time_point
|
776
|
-
time_point.time - self rescue time_point.datetime - self
|
777
|
-
else
|
778
|
-
time_point - self rescue time_point.to_datetime - self
|
779
|
-
end
|
780
|
-
end
|
781
|
-
alias :before_now :before
|
782
|
-
|
783
|
-
# 'min'.since(time)
|
784
|
-
def since(time_point = ::Time.now)
|
785
|
-
case time_point
|
786
|
-
when Time: (Time.now - time_point).unit('s').to(self)
|
787
|
-
when DateTime, Date: (DateTime.now - time_point).unit('d').to(self)
|
788
|
-
when String:
|
789
|
-
(DateTime.now - time_point.time(:context=>:past)).unit('d').to(self)
|
790
|
-
else
|
791
|
-
raise ArgumentError, "Must specify a Time, DateTime, or String"
|
792
|
-
end
|
793
|
-
end
|
794
|
-
|
795
|
-
# 'min'.until(time)
|
796
|
-
def until(time_point = ::Time.now)
|
797
|
-
case time_point
|
798
|
-
when Time: (time_point - Time.now).unit('s').to(self)
|
799
|
-
when DateTime, Date: (time_point - DateTime.now).unit('d').to(self)
|
800
|
-
when String:
|
801
|
-
r = (time_point.time(:context=>:future) - DateTime.now)
|
802
|
-
Time === time_point.time ? r.unit('s').to(self) : r.unit('d').to(self)
|
803
|
-
else
|
804
|
-
raise ArgumentError, "Must specify a Time, DateTime, or String"
|
805
|
-
end
|
806
|
-
end
|
807
|
-
|
808
|
-
# '5 min'.from(time)
|
809
|
-
def from(time_point = ::Time.now)
|
810
|
-
raise ArgumentError, "Must specify a Time" unless time_point
|
811
|
-
if String === time_point
|
812
|
-
time_point.time + self rescue time_point.datetime + self
|
813
|
-
else
|
814
|
-
time_point + self rescue time_point.to_datetime + self
|
815
|
-
end
|
816
|
-
end
|
817
|
-
alias :after :from
|
818
|
-
alias :from_now :from
|
819
|
-
|
820
|
-
# returns next unit in a range. '1 mm'.unit.succ #=> '2 mm'.unit
|
821
|
-
# only works when the scalar is an integer
|
822
|
-
def succ
|
823
|
-
raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
|
824
|
-
q = @scalar.to_i.succ
|
825
|
-
Unit.new(q, @numerator, @denominator)
|
826
|
-
end
|
827
|
-
|
828
|
-
# automatically coerce objects to units when possible
|
829
|
-
# if an object defines a 'to_unit' method, it will be coerced using that method
|
830
|
-
def coerce(other)
|
831
|
-
if other.respond_to? :to_unit
|
832
|
-
return [other.to_unit, self]
|
833
|
-
end
|
834
|
-
case other
|
835
|
-
when Unit : [other, self]
|
836
|
-
else
|
837
|
-
[Unit.new(other), self]
|
838
|
-
end
|
839
|
-
end
|
840
|
-
|
841
|
-
# Protected and Private Functions that should only be called from this class
|
842
|
-
protected
|
843
|
-
|
844
|
-
|
845
|
-
def update_base_scalar
|
846
|
-
return @base_scalar unless @base_scalar.nil?
|
847
|
-
if self.is_base?
|
848
|
-
@base_scalar = @scalar
|
849
|
-
@signature = unit_signature
|
850
|
-
else
|
851
|
-
base = self.to_base
|
852
|
-
@base_scalar = base.scalar
|
853
|
-
@signature = base.signature
|
854
|
-
end
|
855
|
-
end
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
# calculates the unit signature vector used by unit_signature
|
861
|
-
def unit_signature_vector
|
862
|
-
return self.to_base.unit_signature_vector unless self.is_base?
|
863
|
-
result = self
|
864
|
-
vector = Array.new(SIGNATURE_VECTOR.size,0)
|
865
|
-
for element in @numerator
|
866
|
-
if r=@@ALL_UNIT_DEFINITIONS[element]
|
867
|
-
n = SIGNATURE_VECTOR.index(r[2])
|
868
|
-
vector[n] = vector[n] + 1 if n
|
869
|
-
end
|
870
|
-
end
|
871
|
-
for element in @denominator
|
872
|
-
if r=@@ALL_UNIT_DEFINITIONS[element]
|
873
|
-
n = SIGNATURE_VECTOR.index(r[2])
|
874
|
-
vector[n] = vector[n] - 1 if n
|
875
|
-
end
|
876
|
-
end
|
877
|
-
vector
|
878
|
-
end
|
879
|
-
|
880
|
-
def replace_temperature
|
881
|
-
return self unless self.kind == :temperature && self.units =~ /temp(R|K|F|C)/
|
882
|
-
un = $1
|
883
|
-
@numerator = case un
|
884
|
-
when 'R' : RANKINE
|
885
|
-
when 'C' : CELCIUS
|
886
|
-
when 'F' : FARENHEIT
|
887
|
-
when 'K' : KELVIN
|
888
|
-
end
|
889
|
-
@unit_name = nil
|
890
|
-
r= self.to("tempK")
|
891
|
-
copy(r)
|
892
|
-
end
|
893
|
-
|
894
|
-
private
|
895
|
-
|
896
|
-
def initialize_copy(other)
|
897
|
-
@numerator = other.numerator.dup
|
898
|
-
@denominator = other.denominator.dup
|
899
|
-
|
900
|
-
end
|
901
|
-
|
902
|
-
# calculates the unit signature id for use in comparing compatible units and simplification
|
903
|
-
# the signature is based on a simple classification of units and is based on the following publication
|
904
|
-
#
|
905
|
-
# Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering,
|
906
|
-
# 21(8), Aug 1995, pp.651-661
|
907
|
-
# doi://10.1109/32.403789
|
908
|
-
# http://ieeexplore.ieee.org/Xplore/login.jsp?url=/iel1/32/9079/00403789.pdf?isnumber=9079&prod=JNL&arnumber=403789&arSt=651&ared=661&arAuthor=Novak%2C+G.S.%2C+Jr.
|
909
|
-
#
|
910
|
-
def unit_signature
|
911
|
-
return @signature unless @signature.nil?
|
912
|
-
vector = unit_signature_vector
|
913
|
-
vector.each_with_index {|item,index| vector[index] = item * 20**index}
|
914
|
-
@signature=vector.inject(0) {|sum,n| sum+n}
|
915
|
-
end
|
916
|
-
|
917
|
-
def self.eliminate_terms(q, n, d)
|
918
|
-
num = n.dup
|
919
|
-
den = d.dup
|
920
|
-
|
921
|
-
num.delete_if {|v| v == UNITY}
|
922
|
-
den.delete_if {|v| v == UNITY}
|
923
|
-
combined = Hash.new(0)
|
924
|
-
|
925
|
-
i = 0
|
926
|
-
loop do
|
927
|
-
break if i > num.size
|
928
|
-
if @@PREFIX_VALUES.has_key? num[i]
|
929
|
-
k = [num[i],num[i+1]]
|
930
|
-
i += 2
|
931
|
-
else
|
932
|
-
k = num[i]
|
933
|
-
i += 1
|
934
|
-
end
|
935
|
-
combined[k] += 1 unless k.nil? || k == UNITY
|
936
|
-
end
|
937
|
-
|
938
|
-
j = 0
|
939
|
-
loop do
|
940
|
-
break if j > den.size
|
941
|
-
if @@PREFIX_VALUES.has_key? den[j]
|
942
|
-
k = [den[j],den[j+1]]
|
943
|
-
j += 2
|
944
|
-
else
|
945
|
-
k = den[j]
|
946
|
-
j += 1
|
947
|
-
end
|
948
|
-
combined[k] -= 1 unless k.nil? || k == UNITY
|
949
|
-
end
|
950
|
-
|
951
|
-
num = []
|
952
|
-
den = []
|
953
|
-
for key, value in combined do
|
954
|
-
case
|
955
|
-
when value > 0 : value.times {num << key}
|
956
|
-
when value < 0 : value.abs.times {den << key}
|
957
|
-
end
|
958
|
-
end
|
959
|
-
num = UNITY_ARRAY if num.empty?
|
960
|
-
den = UNITY_ARRAY if den.empty?
|
961
|
-
{:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact}
|
962
|
-
end
|
963
|
-
|
964
|
-
|
965
|
-
# parse a string into a unit object.
|
966
|
-
# Typical formats like :
|
967
|
-
# "5.6 kg*m/s^2"
|
968
|
-
# "5.6 kg*m*s^-2"
|
969
|
-
# "5.6 kilogram*meter*second^-2"
|
970
|
-
# "2.2 kPa"
|
971
|
-
# "37 degC"
|
972
|
-
# "1" -- creates a unitless constant with value 1
|
973
|
-
# "GPa" -- creates a unit with scalar 1 with units 'GPa'
|
974
|
-
# 6'4" -- recognized as 6 feet + 4 inches
|
975
|
-
# 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
|
976
|
-
def parse(passed_unit_string="0")
|
977
|
-
unit_string = passed_unit_string.dup
|
978
|
-
if unit_string =~ /\$\s*(#{NUMBER_REGEX})/
|
979
|
-
unit_string = "#{$1} USD"
|
980
|
-
end
|
981
|
-
|
982
|
-
|
983
|
-
unit_string.gsub!(/%/,'percent')
|
984
|
-
unit_string.gsub!(/'/,'feet')
|
985
|
-
unit_string.gsub!(/"/,'inch')
|
986
|
-
unit_string.gsub!(/#/,'pound')
|
987
|
-
if defined?(Uncertain) && unit_string =~ /(\+\/-|±)/
|
988
|
-
value, uncertainty, unit_s = unit_string.scan(UNCERTAIN_REGEX)[0]
|
989
|
-
result = unit_s.unit * Uncertain(value.to_f,uncertainty.to_f)
|
990
|
-
copy(result)
|
991
|
-
return
|
992
|
-
end
|
993
|
-
|
994
|
-
if defined?(Complex) && unit_string =~ COMPLEX_NUMBER
|
995
|
-
real, imaginary, unit_s = unit_string.scan(COMPLEX_REGEX)[0]
|
996
|
-
result = Unit(unit_s || '1') * Complex(real.to_f,imaginary.to_f)
|
997
|
-
copy(result)
|
998
|
-
return
|
999
|
-
end
|
1000
|
-
|
1001
|
-
if defined?(Rational) && unit_string =~ RATIONAL_NUMBER
|
1002
|
-
numerator, denominator, unit_s = unit_string.scan(RATIONAL_REGEX)[0]
|
1003
|
-
result = Unit(unit_s || '1') * Rational(numerator.to_i,denominator.to_i)
|
1004
|
-
copy(result)
|
1005
|
-
return
|
1006
|
-
end
|
1007
|
-
|
1008
|
-
unit_string =~ NUMBER_REGEX
|
1009
|
-
unit = @@cached_units[$2]
|
1010
|
-
mult = ($1.empty? ? 1.0 : $1.to_f) rescue 1.0
|
1011
|
-
if unit
|
1012
|
-
copy(unit)
|
1013
|
-
@scalar *= mult
|
1014
|
-
@base_scalar *= mult
|
1015
|
-
return self
|
1016
|
-
end
|
1017
|
-
|
1018
|
-
unit_string.gsub!(/[<>]/,"")
|
1019
|
-
|
1020
|
-
if unit_string =~ /:/
|
1021
|
-
hours, minutes, seconds, microseconds = unit_string.scan(TIME_REGEX)[0]
|
1022
|
-
raise ArgumentError, "Invalid Duration" if [hours, minutes, seconds, microseconds].all? {|x| x.nil?}
|
1023
|
-
result = "#{hours || 0} h".unit +
|
1024
|
-
"#{minutes || 0} minutes".unit +
|
1025
|
-
"#{seconds || 0} seconds".unit +
|
1026
|
-
"#{microseconds || 0} usec".unit
|
1027
|
-
copy(result)
|
1028
|
-
return
|
1029
|
-
end
|
1030
|
-
|
1031
|
-
|
1032
|
-
# Special processing for unusual unit strings
|
1033
|
-
# feet -- 6'5"
|
1034
|
-
feet, inches = unit_string.scan(FEET_INCH_REGEX)[0]
|
1035
|
-
if (feet && inches)
|
1036
|
-
result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches")
|
1037
|
-
copy(result)
|
1038
|
-
return
|
1039
|
-
end
|
1040
|
-
|
1041
|
-
# weight -- 8 lbs 12 oz
|
1042
|
-
pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0]
|
1043
|
-
if (pounds && oz)
|
1044
|
-
result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz")
|
1045
|
-
copy(result)
|
1046
|
-
return
|
1047
|
-
end
|
1048
|
-
|
1049
|
-
raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1
|
1050
|
-
raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.scan(/\s\d+\S*/).size > 0
|
1051
|
-
|
1052
|
-
@scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] #parse the string into parts
|
1053
|
-
|
1054
|
-
top.scan(TOP_REGEX).each do |item|
|
1055
|
-
n = item[1].to_i
|
1056
|
-
x = "#{item[0]} "
|
1057
|
-
case
|
1058
|
-
when n>=0 : top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) {|s| x * n}
|
1059
|
-
when n<0 : bottom = "#{bottom} #{x * -n}"; top.gsub!(/#{item[0]}(\^|\*\*)#{n}/,"")
|
1060
|
-
end
|
1061
|
-
end
|
1062
|
-
bottom.gsub!(BOTTOM_REGEX) {|s| "#{$1} " * $2.to_i} if bottom
|
1063
|
-
@scalar = @scalar.to_f unless @scalar.nil? || @scalar.empty?
|
1064
|
-
@scalar = 1 unless @scalar.kind_of? Numeric
|
1065
|
-
|
1066
|
-
@numerator ||= UNITY_ARRAY
|
1067
|
-
@denominator ||= UNITY_ARRAY
|
1068
|
-
@numerator = top.scan(@@UNIT_MATCH_REGEX).delete_if {|x| x.empty?}.compact if top
|
1069
|
-
@denominator = bottom.scan(@@UNIT_MATCH_REGEX).delete_if {|x| x.empty?}.compact if bottom
|
1070
|
-
us = "#{(top || '' + bottom || '')}".to_s.gsub(@@UNIT_MATCH_REGEX,'').gsub(/[\d\*, "'_^\/\$]/,'')
|
1071
|
-
|
1072
|
-
raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless us.empty?
|
1073
|
-
|
1074
|
-
@numerator = @numerator.map do |item|
|
1075
|
-
@@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]]
|
1076
|
-
end.flatten.compact.delete_if {|x| x.empty?}
|
1077
|
-
|
1078
|
-
@denominator = @denominator.map do |item|
|
1079
|
-
@@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]]
|
1080
|
-
end.flatten.compact.delete_if {|x| x.empty?}
|
1081
|
-
|
1082
|
-
@numerator = UNITY_ARRAY if @numerator.empty?
|
1083
|
-
@denominator = UNITY_ARRAY if @denominator.empty?
|
1084
|
-
self
|
1085
|
-
end
|
1086
|
-
end
|
1087
|
-
|
1088
|
-
|
1089
|
-
# Allow date objects to do offsets by a time unit
|
1090
|
-
# Date.today + U"1 week" => gives today+1 week
|
1091
|
-
class Date
|
1092
|
-
alias :unit_date_add :+
|
1093
|
-
def +(unit)
|
1094
|
-
case unit
|
1095
|
-
when Unit:
|
1096
|
-
unit = unit.to('d').round if ['y', 'decade', 'century'].include? unit.units
|
1097
|
-
unit_date_add(unit.to('day').scalar)
|
1098
|
-
when Time: unit_date_add(unit.to_datetime)
|
1099
|
-
else
|
1100
|
-
unit_date_add(unit)
|
1101
|
-
end
|
1102
|
-
end
|
1103
|
-
|
1104
|
-
alias :unit_date_sub :-
|
1105
|
-
def -(unit)
|
1106
|
-
case unit
|
1107
|
-
when Unit:
|
1108
|
-
unit = unit.to('d').round if ['y', 'decade', 'century'].include? unit.units
|
1109
|
-
unit_date_sub(unit.to('day').scalar)
|
1110
|
-
when Time: unit_date_sub(unit.to_datetime)
|
1111
|
-
else
|
1112
|
-
unit_date_sub(unit)
|
1113
|
-
end
|
1114
|
-
end
|
1115
|
-
|
1116
|
-
def to_unit(other = nil)
|
1117
|
-
other ? Unit.new(self).to(other) : Unit.new(self)
|
1118
|
-
end
|
1119
|
-
alias :unit :to_unit
|
1120
|
-
|
1121
|
-
def to_time
|
1122
|
-
Time.local(*ParseDate.parsedate(self.to_s))
|
1123
|
-
end
|
1124
|
-
|
1125
|
-
alias :units_datetime_inspect :inspect
|
1126
|
-
def inspect(raw = false)
|
1127
|
-
return self.units_datetime_inspect if raw
|
1128
|
-
self.to_s
|
1129
|
-
end
|
1130
|
-
|
1131
|
-
def to_date
|
1132
|
-
Date.civil(self.year, self.month, self.day)
|
1133
|
-
end
|
1134
|
-
|
1135
|
-
end
|
1136
|
-
|
1137
|
-
class Object
|
1138
|
-
def Unit(*other)
|
1139
|
-
other.to_unit
|
1140
|
-
end
|
1141
|
-
|
1142
|
-
alias :U :Unit
|
1143
|
-
alias :u :Unit
|
1144
|
-
end
|
1145
|
-
|
1146
|
-
# make a unitless unit with a given scalar
|
1147
|
-
class Numeric
|
1148
|
-
def to_unit(other = nil)
|
1149
|
-
other ? Unit.new(self, other) : Unit.new(self)
|
1150
|
-
end
|
1151
|
-
alias :unit :to_unit
|
1152
|
-
alias :u :to_unit
|
1153
|
-
end
|
1154
|
-
|
1155
|
-
# make a unit from an array
|
1156
|
-
# [1, 'mm'].unit => 1 mm
|
1157
|
-
class Array
|
1158
|
-
def to_unit(other = nil)
|
1159
|
-
other ? Unit.new(self).to(other) : Unit.new(self)
|
1160
|
-
end
|
1161
|
-
alias :unit :to_unit
|
1162
|
-
alias :u :to_unit
|
1163
|
-
end
|
1164
|
-
|
1165
|
-
# make a string into a unit
|
1166
|
-
class String
|
1167
|
-
def to_unit(other = nil)
|
1168
|
-
other ? Unit.new(self).to(other) : Unit.new(self)
|
1169
|
-
end
|
1170
|
-
alias :unit :to_unit
|
1171
|
-
alias :u :to_unit
|
1172
|
-
alias :unit_format :%
|
1173
|
-
|
1174
|
-
# format unit output using formating codes '%0.2f' % '1 mm'.unit => '1.00 mm'
|
1175
|
-
def %(*args)
|
1176
|
-
case args[0]
|
1177
|
-
when Unit: args[0].to_s(self)
|
1178
|
-
when Complex: args[0].to_s
|
1179
|
-
else
|
1180
|
-
unit_format(*args)
|
1181
|
-
end
|
1182
|
-
end
|
1183
|
-
|
1184
|
-
#needed for compatibility with Rails, which defines a String.from method
|
1185
|
-
if self.public_instance_methods.include? 'from'
|
1186
|
-
alias :old_from :from
|
1187
|
-
end
|
1188
|
-
|
1189
|
-
def from(time_point = ::Time.now)
|
1190
|
-
return old_from(time_point) if Integer === time_point
|
1191
|
-
self.unit.from(time_point)
|
1192
|
-
end
|
1193
|
-
|
1194
|
-
alias :after :from
|
1195
|
-
alias :from_now :from
|
1196
|
-
|
1197
|
-
def ago
|
1198
|
-
self.unit.ago
|
1199
|
-
end
|
1200
|
-
|
1201
|
-
def before(time_point = ::Time.now)
|
1202
|
-
self.unit.before(time_point)
|
1203
|
-
end
|
1204
|
-
alias :before_now :before
|
1205
|
-
|
1206
|
-
def since(time_point = ::Time.now)
|
1207
|
-
self.unit.since(time_point)
|
1208
|
-
end
|
1209
|
-
|
1210
|
-
def until(time_point = ::Time.now)
|
1211
|
-
self.unit.until(time_point)
|
1212
|
-
end
|
1213
|
-
|
1214
|
-
def to(other)
|
1215
|
-
self.unit.to(other)
|
1216
|
-
end
|
1217
|
-
|
1218
|
-
def time(options = {})
|
1219
|
-
self.to_time(options) rescue self.to_datetime(options)
|
1220
|
-
end
|
1221
|
-
|
1222
|
-
def to_time(options = {})
|
1223
|
-
begin
|
1224
|
-
#raises exception when Chronic not defined or when it returns a nil (i.e., can't parse the input)
|
1225
|
-
r = Chronic.parse(self,options)
|
1226
|
-
raise(ArgumentError, 'Invalid Time String') unless r
|
1227
|
-
return r
|
1228
|
-
rescue
|
1229
|
-
Time.local(*ParseDate.parsedate(self))
|
1230
|
-
end
|
1231
|
-
end
|
1232
|
-
|
1233
|
-
def to_datetime(options = {})
|
1234
|
-
begin
|
1235
|
-
# raises an exception if Chronic.parse = nil or if Chronic not defined
|
1236
|
-
r = Chronic.parse(self,options).to_datetime
|
1237
|
-
rescue
|
1238
|
-
r=DateTime.civil(*ParseDate.parsedate(self)[0..5].compact)
|
1239
|
-
end
|
1240
|
-
raise RuntimeError, "Invalid Time String" if r == DateTime.new
|
1241
|
-
return r
|
1242
|
-
end
|
1243
|
-
|
1244
|
-
def to_date(options={})
|
1245
|
-
begin
|
1246
|
-
r = Chronic.parse(self,options).to_date
|
1247
|
-
rescue
|
1248
|
-
r = Date.civil(*ParseDate.parsedate(self)[0..5].compact)
|
1249
|
-
end
|
1250
|
-
raise RuntimeError, 'Invalid Date String' if r == Date.new
|
1251
|
-
return r
|
1252
|
-
end
|
1253
|
-
|
1254
|
-
def datetime(options = {})
|
1255
|
-
self.to_datetime(options) rescue self.to_time(options)
|
1256
|
-
end
|
1257
|
-
end
|
1258
|
-
|
1259
|
-
|
1260
|
-
#
|
1261
|
-
# Time math is handled slightly differently. The difference is considered to be an exact duration if
|
1262
|
-
# the subtracted value is in hours, minutes, or seconds. It is rounded to the nearest day if the offset
|
1263
|
-
# is in years, decades, or centuries. This leads to less precise values, but ones that match the
|
1264
|
-
# calendar better.
|
1265
|
-
class Time
|
1266
|
-
|
1267
|
-
class << self
|
1268
|
-
alias unit_time_at at
|
1269
|
-
end
|
1270
|
-
|
1271
|
-
def self.at(*args)
|
1272
|
-
if Unit === args[0]
|
1273
|
-
unit_time_at(args[0].to("s").scalar)
|
1274
|
-
else
|
1275
|
-
unit_time_at(*args)
|
1276
|
-
end
|
1277
|
-
end
|
1278
|
-
|
1279
|
-
def to_unit(other = nil)
|
1280
|
-
other ? Unit.new(self).to(other) : Unit.new(self)
|
1281
|
-
end
|
1282
|
-
alias :unit :to_unit
|
1283
|
-
alias :u :to_unit
|
1284
|
-
alias :unit_add :+
|
1285
|
-
|
1286
|
-
def to_datetime
|
1287
|
-
DateTime.civil(1970,1,1)+(self.to_f+self.gmt_offset)/86400
|
1288
|
-
end
|
1289
|
-
|
1290
|
-
def to_date
|
1291
|
-
Date.civil(1970,1,1)+(self.to_f+self.gmt_offset)/86400
|
1292
|
-
end
|
1293
|
-
|
1294
|
-
def +(other)
|
1295
|
-
case other
|
1296
|
-
when Unit:
|
1297
|
-
other = other.to('d').round.to('s') if ['y', 'decade', 'century'].include? other.units
|
1298
|
-
begin
|
1299
|
-
unit_add(other.to('s').scalar)
|
1300
|
-
rescue RangeError
|
1301
|
-
self.to_datetime + other
|
1302
|
-
end
|
1303
|
-
when DateTime: unit_add(other.to_time)
|
1304
|
-
else
|
1305
|
-
unit_add(other)
|
1306
|
-
end
|
1307
|
-
end
|
1308
|
-
|
1309
|
-
# usage: Time.in '5 min'
|
1310
|
-
def self.in(duration)
|
1311
|
-
Time.now + duration.to_unit
|
1312
|
-
end
|
1313
|
-
|
1314
|
-
alias :unit_sub :-
|
1315
|
-
|
1316
|
-
def -(other)
|
1317
|
-
case other
|
1318
|
-
when Unit:
|
1319
|
-
other = other.to('d').round.to('s') if ['y', 'decade', 'century'].include? other.units
|
1320
|
-
begin
|
1321
|
-
unit_sub(other.to('s').scalar)
|
1322
|
-
rescue RangeError
|
1323
|
-
self.to_datetime - other
|
1324
|
-
end
|
1325
|
-
|
1326
|
-
when DateTime: unit_sub(other.to_time)
|
1327
|
-
else
|
1328
|
-
unit_sub(other)
|
1329
|
-
end
|
1330
|
-
end
|
1331
|
-
end
|
1332
|
-
|
1333
|
-
# Math will convert unit objects to radians and then attempt to use the value for
|
1334
|
-
# trigonometric functions.
|
1335
|
-
module Math
|
1336
|
-
alias unit_sqrt sqrt
|
1337
|
-
def sqrt(n)
|
1338
|
-
Unit === n ? n**(1/2) : unit_sqrt(n)
|
1339
|
-
end
|
1340
|
-
|
1341
|
-
alias unit_sin sin
|
1342
|
-
def sin(n)
|
1343
|
-
Unit === n ? unit_sin(n.to('radian').scalar) : unit_sin(n)
|
1344
|
-
end
|
1345
|
-
|
1346
|
-
alias unit_cos cos
|
1347
|
-
def cos(n)
|
1348
|
-
Unit === n ? unit_cos(n.to('radian').scalar) : unit_cos(n)
|
1349
|
-
end
|
1350
|
-
|
1351
|
-
alias unit_sinh sinh
|
1352
|
-
def sinh(n)
|
1353
|
-
Unit === n ? unit_sinh(n.to('radian').scalar) : unit_sinh(n)
|
1354
|
-
end
|
1355
|
-
|
1356
|
-
alias unit_cosh cosh
|
1357
|
-
def cosh(n)
|
1358
|
-
Unit === n ? unit_cosh(n.to('radian').scalar) : unit_cosh(n)
|
1359
|
-
end
|
1360
|
-
|
1361
|
-
alias unit_tan tan
|
1362
|
-
def tan(n)
|
1363
|
-
Unit === n ? unit_tan(n.to('radian').scalar) : unit_tan(n)
|
1364
|
-
end
|
1365
|
-
|
1366
|
-
alias unit_tanh tanh
|
1367
|
-
def tanh(n)
|
1368
|
-
Unit === n ? unit_tanh(n.to('radian').scalar) : unit_tanh(n)
|
1369
|
-
end
|
1370
|
-
|
1371
|
-
alias unit_hypot hypot
|
1372
|
-
# Convert parameters to consistent units and perform the function
|
1373
|
-
def hypot(x,y)
|
1374
|
-
if Unit === x && Unit === y
|
1375
|
-
(x**2 + y**2)**(1/2)
|
1376
|
-
else
|
1377
|
-
unit_hypot(x,y)
|
1378
|
-
end
|
1379
|
-
end
|
1380
|
-
|
1381
|
-
alias unit_atan2 atan2
|
1382
|
-
def atan2(x,y)
|
1383
|
-
case
|
1384
|
-
when (Unit === x && Unit === y) && (x !~ y)
|
1385
|
-
raise ArgumentError, "Incompatible Units"
|
1386
|
-
when (Unit === x && Unit === y) && (x =~ y)
|
1387
|
-
unit_atan2(x.base_scalar, y.base_scalar)
|
1388
|
-
else
|
1389
|
-
unit_atan2(x,y)
|
1390
|
-
end
|
1391
|
-
end
|
1392
|
-
|
1393
|
-
module_function :unit_hypot
|
1394
|
-
module_function :hypot
|
1395
|
-
module_function :unit_sqrt
|
1396
|
-
module_function :sqrt
|
1397
|
-
module_function :unit_sin
|
1398
|
-
module_function :sin
|
1399
|
-
module_function :unit_cos
|
1400
|
-
module_function :cos
|
1401
|
-
module_function :unit_sinh
|
1402
|
-
module_function :sinh
|
1403
|
-
module_function :unit_cosh
|
1404
|
-
module_function :cosh
|
1405
|
-
module_function :unit_tan
|
1406
|
-
module_function :tan
|
1407
|
-
module_function :unit_tanh
|
1408
|
-
module_function :tanh
|
1409
|
-
module_function :unit_atan2
|
1410
|
-
module_function :atan2
|
1411
|
-
|
1412
|
-
end
|
1413
|
-
|
1414
|
-
Unit.setup
|