ruby-units 0.1.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/LICENSE +20 -0
- data/README +85 -0
- data/lib/ruby_units.rb +596 -0
- data/lib/units.rb +226 -0
- data/test/test_ruby_units.rb +384 -0
- metadata +50 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2006 Kevin C. Olbrich
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
=Ruby Units
|
2
|
+
|
3
|
+
Version: 0.1.0
|
4
|
+
|
5
|
+
Kevin C. Olbrich, Ph.D.
|
6
|
+
|
7
|
+
http://www.sciwerks.com
|
8
|
+
|
9
|
+
Project page: http://rubyforge.org/projects/ruby-units
|
10
|
+
|
11
|
+
==Introduction
|
12
|
+
Many technical applications make use of specialized calculations at some point. Frequently, these calculations require unit conversions to ensure accurate results. Needless to say, this is a pain to properly keep track of, and is prone to numerous errors.
|
13
|
+
|
14
|
+
==Solution
|
15
|
+
The 'Ruby units' gem is designed so simplify the handling of units for scientific calculations. The units of each quantity are specified when a Unit object is created and the Unit class will handle all subsequent conversions and manipulations to ensure an accurate result.
|
16
|
+
|
17
|
+
==Installation:
|
18
|
+
This package may be installed using:
|
19
|
+
gem install ruby-units
|
20
|
+
|
21
|
+
==Usage:
|
22
|
+
unit = Unit.new("1") # constant only
|
23
|
+
unit = Unit.new("mm") # unit only (defaults to a value of 1)
|
24
|
+
unit = Unit.new("1 mm") # create a simple unit
|
25
|
+
unit = Unit.new("1 mm/s") # a compound unit
|
26
|
+
unit = Unit.new("1 mm s^-1") # in exponent notation
|
27
|
+
unit = Unit.new("1 kg*m^2/s^2") # complex unit
|
28
|
+
unit = Unit.new("1 kg m^2 s^-2") # complex unit
|
29
|
+
unit = Unit("1 mm") # shorthand
|
30
|
+
unit = "1 mm".to_unit # convert string object
|
31
|
+
unit = object.to_unit # convert any object using object.to_s
|
32
|
+
|
33
|
+
==Rules:
|
34
|
+
1. only 1 quantity per unit (with 2 exceptions... 6'5" and '8 lbs 8 oz')
|
35
|
+
2. use SI notation when possible
|
36
|
+
3. avoid using spaces in unit names
|
37
|
+
|
38
|
+
==Unit compatability:
|
39
|
+
Many methods require that the units of two operands are compatible. Compatible units are those that can be easily converted into each other, such as 'meters' and 'feet'.
|
40
|
+
|
41
|
+
unit1 =~ unit2 #=> true if units are compatible
|
42
|
+
|
43
|
+
==Unit Math:
|
44
|
+
|
45
|
+
<b>Method</b>:: <b>Comment</b>
|
46
|
+
Unit#+():: Add. only works if units are compatible
|
47
|
+
Unit#-():: Subtract. only works if units are compatible
|
48
|
+
Unit#*():: Multiply.
|
49
|
+
Unit#/():: Divide.
|
50
|
+
Unit#**():: Exponentiate. Exponent must be an integer, can be positive, negative, or zero
|
51
|
+
Unit#inverse:: Returns 1/unit
|
52
|
+
Unit#abs:: Returns absolute value of the unit quantity. Strips off the units
|
53
|
+
Unit#ceil:: rounds quantity to next highest integer
|
54
|
+
Unit#floor:: rounds quantity down to next lower integer
|
55
|
+
Unit#round:: rounds quantity to nearest integer
|
56
|
+
Unit#to_int:: returns the quantity as an integer
|
57
|
+
|
58
|
+
Unit will coerce other objects into a Unit if used in a formula. This means that ..
|
59
|
+
|
60
|
+
Unit("1 mm") + "2 mm" == Unit("3 mm")
|
61
|
+
|
62
|
+
This will work as expected so long as you start the formula with a Unit object.
|
63
|
+
|
64
|
+
==Conversions & comparisons
|
65
|
+
|
66
|
+
Units can be converted to other units in a couple of ways.
|
67
|
+
|
68
|
+
unit1 = unit >> "ft" # => convert to 'feet'
|
69
|
+
unit >>= "ft" # => convert and overwrite original object
|
70
|
+
unit3 = unit1 + unit2 # => resulting object will have the units of unit1
|
71
|
+
unit3 = unit1 - unit2 # => resulting object will have the units of unit1
|
72
|
+
unit1 <=> unit2 # => does comparison on quantities in base units, throws an exception if not compatible
|
73
|
+
unit1 === unit2 # => true if units and quantity are the same, even if 'equivalent' by <=>
|
74
|
+
|
75
|
+
==Text Output
|
76
|
+
Units will display themselves nicely based on the preferred abbreviation for the units and prefixes.
|
77
|
+
Since Unit implements a Unit#to_s, all that is needed in most cases is:
|
78
|
+
"#{Unit.new('1 mm')}" #=> "1 mm"
|
79
|
+
|
80
|
+
The to_s also accepts some options.
|
81
|
+
Unit.new('1.5 mm').to_s("%0.2f") # => "1.50 mm". Enter any valid format string
|
82
|
+
Unit.new('1.5 mm').to_s("in") # => converts to inches before printing
|
83
|
+
Unit.new("2 m").to_s(:ft) #=> returns 6'7"
|
84
|
+
Unit.new("100 kg").to_s(:lbs) #=> returns 220 lbs, 7 oz
|
85
|
+
|
data/lib/ruby_units.rb
ADDED
@@ -0,0 +1,596 @@
|
|
1
|
+
|
2
|
+
# = Ruby Units
|
3
|
+
#
|
4
|
+
# Copyright 2006 by Kevin C. Olbrich, Ph.D.
|
5
|
+
#
|
6
|
+
# See http://rubyforge.org/ruby-units/
|
7
|
+
#
|
8
|
+
# http://www.sciwerks.org
|
9
|
+
#
|
10
|
+
# mailto://kevin.olbrich+ruby-units@gmail.com
|
11
|
+
#
|
12
|
+
# See README for detailed usage instructions and examples
|
13
|
+
#
|
14
|
+
# =Unit Definition Format
|
15
|
+
#
|
16
|
+
# '<name>' => [%w{prefered_name synonyms}, conversion_to_base, :classification, %w{<base> <units> <in> <numerator>} , %w{<base> <units> <in> <denominator>} ],
|
17
|
+
#
|
18
|
+
# Prefixes (e.g., a :prefix classification) get special handling
|
19
|
+
# Note: The accuracy of unit conversions depends on the precision of the conversion factor.
|
20
|
+
# If you have more accurate estimates for particular conversion factors, please send them
|
21
|
+
# to me and I will incorporate them into the next release. It is also incumbent on the end-user
|
22
|
+
# to ensure that the accuracy of any conversions is sufficient for their intended application.
|
23
|
+
#
|
24
|
+
# While there are a large number of unit specified in the base package,
|
25
|
+
# there are also a large number of units that are not included.
|
26
|
+
# This package covers nearly all SI, Imperial, and units commonly used
|
27
|
+
# in the United States. If your favorite units are not listed here, send me an email
|
28
|
+
#
|
29
|
+
# Future versions will provide a simple interface for the end-user to specify
|
30
|
+
# custom units.
|
31
|
+
class Unit < Numeric
|
32
|
+
require 'units'
|
33
|
+
# pre-generate hashes from unit definitions for performance.
|
34
|
+
@@USER_DEFINITIONS = {}
|
35
|
+
@@PREFIX_VALUES = {}
|
36
|
+
@@PREFIX_MAP = {}
|
37
|
+
@@UNIT_MAP = {}
|
38
|
+
@@UNIT_VALUES = {}
|
39
|
+
@@OUTPUT_MAP = {}
|
40
|
+
@@UNIT_VECTORS = {}
|
41
|
+
|
42
|
+
def self.setup
|
43
|
+
(UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS)).each do |key, value|
|
44
|
+
if value[2] == :prefix then
|
45
|
+
@@PREFIX_VALUES[Regexp.escape(key)]=value[1]
|
46
|
+
value[0].each {|x| @@PREFIX_MAP[Regexp.escape(x)]=key}
|
47
|
+
else
|
48
|
+
@@UNIT_VALUES[Regexp.escape(key)]={}
|
49
|
+
@@UNIT_VALUES[Regexp.escape(key)][:quantity]=value[1]
|
50
|
+
@@UNIT_VALUES[Regexp.escape(key)][:numerator]=value[3] if value[3]
|
51
|
+
@@UNIT_VALUES[Regexp.escape(key)][:denominator]=value[4] if value[4]
|
52
|
+
value[0].each {|x| @@UNIT_MAP[Regexp.escape(x)]=key}
|
53
|
+
@@UNIT_VECTORS[value[2]] = [] unless @@UNIT_VECTORS[value[2]]
|
54
|
+
@@UNIT_VECTORS[value[2]] = @@UNIT_VECTORS[value[2]]+[Regexp.escape(key)]
|
55
|
+
end
|
56
|
+
@@OUTPUT_MAP[Regexp.escape(key)]=value[0][0]
|
57
|
+
end
|
58
|
+
@@PREFIX_REGEX = @@PREFIX_MAP.keys.sort_by {|prefix| prefix.length}.reverse.join('|')
|
59
|
+
@@UNIT_REGEX = @@UNIT_MAP.keys.sort_by {|unit| unit.length}.reverse.join('|')
|
60
|
+
end
|
61
|
+
|
62
|
+
self.setup
|
63
|
+
|
64
|
+
include Comparable
|
65
|
+
attr_reader :quantity, :numerator, :denominator, :signature, :base_quantity
|
66
|
+
|
67
|
+
# Create a new Unit object. Can be initialized using a string, or a hash
|
68
|
+
# Valid formats include:
|
69
|
+
# "5.6 kg*m/s^2"
|
70
|
+
# "5.6 kg*m*s^-2"
|
71
|
+
# "5.6 kilogram*meter*second^-2"
|
72
|
+
# "2.2 kPa"
|
73
|
+
# "37 degC"
|
74
|
+
# "1" -- creates a unitless constant with value 1
|
75
|
+
# "GPa" -- creates a unit with quantity 1 with units 'GPa'
|
76
|
+
# 6'4" -- recognized as 6 feet + 4 inches
|
77
|
+
# 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
|
78
|
+
#
|
79
|
+
def initialize(options)
|
80
|
+
if options.kind_of? String
|
81
|
+
parse(options)
|
82
|
+
elsif options.kind_of? Hash
|
83
|
+
@quantity = options[:quantity] || 1
|
84
|
+
@numerator = options[:numerator] || ["<1>"]
|
85
|
+
@denominator = options[:denominator] || []
|
86
|
+
else
|
87
|
+
raise ArgumentError, "Unknown Unit Specification"
|
88
|
+
end
|
89
|
+
self.update_base_quantity
|
90
|
+
self.unit_signature
|
91
|
+
self.freeze
|
92
|
+
end
|
93
|
+
|
94
|
+
def initialize_copy(other)
|
95
|
+
@numerator = other.numerator.clone
|
96
|
+
@denominator = other.denominator.clone
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns 'true' if the Unit is represented in base units
|
100
|
+
def is_base?
|
101
|
+
n = @numerator + @denominator
|
102
|
+
n.each do |x|
|
103
|
+
return false unless x == '<1>' || (@@UNIT_VALUES[Regexp.escape(x)] && @@UNIT_VALUES[Regexp.escape(x)][:numerator].include?(Regexp.escape(x)))
|
104
|
+
end
|
105
|
+
return true
|
106
|
+
end
|
107
|
+
|
108
|
+
#convert to base SI units
|
109
|
+
def to_base
|
110
|
+
return self if self.is_base?
|
111
|
+
num = []
|
112
|
+
den = []
|
113
|
+
q = @quantity.to_f
|
114
|
+
@numerator.each do |unit|
|
115
|
+
if @@PREFIX_VALUES[Regexp.escape(unit)]
|
116
|
+
q *= @@PREFIX_VALUES[Regexp.escape(unit)]
|
117
|
+
else
|
118
|
+
q *= @@UNIT_VALUES[Regexp.escape(unit)][:quantity] if @@UNIT_VALUES[Regexp.escape(unit)]
|
119
|
+
num << @@UNIT_VALUES[Regexp.escape(unit)][:numerator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:numerator]
|
120
|
+
den << @@UNIT_VALUES[Regexp.escape(unit)][:denominator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:denominator]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
@denominator.each do |unit|
|
125
|
+
if @@PREFIX_MAP[Regexp.escape(unit)]
|
126
|
+
q /= @@PREFIX_MAP[Regexp.escape(unit)]
|
127
|
+
else
|
128
|
+
q /= @@UNIT_VALUES[Regexp.escape(unit)][:quantity] if @@UNIT_VALUES[Regexp.escape(unit)]
|
129
|
+
den << @@UNIT_VALUES[Regexp.escape(unit)][:numerator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:numerator]
|
130
|
+
num << @@UNIT_VALUES[Regexp.escape(unit)][:denominator] if @@UNIT_VALUES[Regexp.escape(unit)] && @@UNIT_VALUES[Regexp.escape(unit)][:denominator]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
num = num.flatten.compact
|
134
|
+
den = den.flatten.compact
|
135
|
+
num = ['<1>'] if num.empty?
|
136
|
+
|
137
|
+
Unit.new(Unit.eliminate_terms(q,num,den))
|
138
|
+
end
|
139
|
+
|
140
|
+
# Generate human readable output.
|
141
|
+
# If the name of a unit is passed, the quantity will first be converted to the target unit before output.
|
142
|
+
# some named conversions are available
|
143
|
+
#
|
144
|
+
# :ft - outputs in feet and inches (e.g., 6'4")
|
145
|
+
# :lbs - outputs in pounds and ounces (e.g, 8 lbs, 8 oz)
|
146
|
+
#
|
147
|
+
def to_s(target_units=nil)
|
148
|
+
case target_units
|
149
|
+
when :ft:
|
150
|
+
inches = (self >> "in").to_f
|
151
|
+
"#{(inches / 12).truncate}\'#{(inches % 12).round}\""
|
152
|
+
when :lbs:
|
153
|
+
ounces = (self >> "oz").to_f
|
154
|
+
"#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz"
|
155
|
+
else
|
156
|
+
target_units =~ /(%[\w\d#+-.]*)*\s*(.+)*/
|
157
|
+
format_string = "#{$1}" if $1
|
158
|
+
units = $2
|
159
|
+
return (self >> units).to_s(format_string) if units
|
160
|
+
"#{(format_string || '%g') % @quantity} #{self.to_unit}".strip
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Compare two Unit objects. Throws an exception if they are not of compatible types.
|
165
|
+
# Comparisons are done based on the value of the unit in base SI units.
|
166
|
+
def <=>(other)
|
167
|
+
raise ArgumentError, "Incompatible Units" unless self =~ other
|
168
|
+
return self.base_quantity <=> other.base_quantity
|
169
|
+
end
|
170
|
+
|
171
|
+
# check to see if units are compatible, but not the quantity part
|
172
|
+
# this check is done by comparing signatures for performance reasons
|
173
|
+
# if passed a string, it will create a unit object with the string and then do the comparison
|
174
|
+
# this permits a syntax like:
|
175
|
+
# unit =~ "mm"
|
176
|
+
# if you want to do a regexp on the unit string do this ...
|
177
|
+
# unit.to_unit =~ /regexp/
|
178
|
+
def =~(other)
|
179
|
+
case other
|
180
|
+
when Unit : self.signature == other.signature
|
181
|
+
else
|
182
|
+
x,y = coerce(other)
|
183
|
+
x =~ y
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Compare two units. Returns true if quantities and units match
|
188
|
+
#
|
189
|
+
# Unit("100 cm") === Unit("100 cm") # => true
|
190
|
+
# Unit("100 cm") === Unit("1 m") # => false
|
191
|
+
def ===(other)
|
192
|
+
case other
|
193
|
+
when Unit: (self.quantity == other.quantity) && (self.to_unit == other.to_unit)
|
194
|
+
else
|
195
|
+
x,y = coerce(other)
|
196
|
+
x === y
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Add two units together. Result is same units as receiver and quantity and base_quantity are updated appropriately
|
201
|
+
# throws an exception if the units are not compatible.
|
202
|
+
def +(other)
|
203
|
+
if Unit === other
|
204
|
+
if self =~ other then
|
205
|
+
q = @quantity + (other >> self).quantity
|
206
|
+
Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
|
207
|
+
else
|
208
|
+
raise ArgumentError, "Incompatible Units"
|
209
|
+
end
|
210
|
+
else
|
211
|
+
x,y = coerce(other)
|
212
|
+
x + y
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Subtract two units. Result is same units as receiver and quantity and base_quantity are updated appropriately
|
217
|
+
# throws an exception if the units are not compatible.
|
218
|
+
def -(other)
|
219
|
+
if Unit === other
|
220
|
+
if self =~ other then
|
221
|
+
q = @quantity - (other >> self).quantity
|
222
|
+
Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
|
223
|
+
else
|
224
|
+
raise ArgumentError, "Incompatible Units"
|
225
|
+
end
|
226
|
+
else
|
227
|
+
x,y = coerce(other)
|
228
|
+
x - y
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Multiply two units.
|
233
|
+
# Throws an exception if multiplier is not a Unit or Numeric
|
234
|
+
def *(other)
|
235
|
+
if Unit === other
|
236
|
+
Unit.new(Unit.eliminate_terms(@quantity*other.quantity, @numerator + other.numerator ,@denominator + other.denominator))
|
237
|
+
else
|
238
|
+
x,y = coerce(other)
|
239
|
+
x * y
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Divide two units.
|
244
|
+
# Throws an exception if divisor is not a Unit or Numeric
|
245
|
+
def /(other)
|
246
|
+
if Unit === other
|
247
|
+
Unit.new(Unit.eliminate_terms(@quantity/other.quantity, @numerator + other.denominator ,@denominator + other.numerator))
|
248
|
+
else
|
249
|
+
x,y = coerce(other)
|
250
|
+
y / x
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Exponentiate. Only takes integer powers.
|
255
|
+
# Note that anything raised to the power of 0 results in a Unit object with a quantity of 1, and no units.
|
256
|
+
# Throws an exception if exponent is not an integer.
|
257
|
+
def **(other)
|
258
|
+
raise ArgumentError, "Exponent must be Integer" unless Integer === other
|
259
|
+
case
|
260
|
+
when other.to_i > 0 : (1..other.to_i).inject(Unit.new("1")) {|product, n| product * self}
|
261
|
+
when other.to_i == 0 : Unit.new("1")
|
262
|
+
when other.to_i < 0 : (1..-other.to_i).inject(Unit.new("1")) {|product, n| product / self}
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# returns inverse of Unit (1/unit)
|
267
|
+
def inverse
|
268
|
+
(Unit.new("1") / self)
|
269
|
+
end
|
270
|
+
|
271
|
+
# convert to a specified unit string or to the same units as another Unit
|
272
|
+
#
|
273
|
+
# unit >> "kg" will covert to kilograms
|
274
|
+
# unit1 >> unit2 converts to same units as unit2 object
|
275
|
+
#
|
276
|
+
# To convert a Unit object to match another Unit object, use:
|
277
|
+
# unit1 >>= unit2
|
278
|
+
# Throws an exception if the requested target units are incompatible with current Unit.
|
279
|
+
#
|
280
|
+
# Special handling for temperature conversions is supported. If the Unit object is converted
|
281
|
+
# from one temperature unit to another, the proper temperature offsets will be used.
|
282
|
+
# Supports Kelvin, Celcius, Farenheit, and Rankine scales.
|
283
|
+
#
|
284
|
+
# Note that if temperature is part of a compound unit, the temperature will be treated as a differential
|
285
|
+
# and the units will be scaled appropriately.
|
286
|
+
def >>(other)
|
287
|
+
case other.class.to_s
|
288
|
+
when 'Unit': target = other
|
289
|
+
when 'String': target = Unit.new(other)
|
290
|
+
else
|
291
|
+
raise ArgumentError, "Unknown target units"
|
292
|
+
end
|
293
|
+
raise ArgumentError, "Incompatible Units" unless self =~ target
|
294
|
+
if target.signature == 400 then # special handling for temperature conversions
|
295
|
+
q=case self.numerator[0]
|
296
|
+
when '<celcius>':
|
297
|
+
case target.numerator[0]
|
298
|
+
when '<celcius>' : @quantity
|
299
|
+
when '<kelvin>' : @quantity + 273.15
|
300
|
+
when '<farenheit>': @quantity * (9.0/5.0) + 32.0
|
301
|
+
when '<rankine>' : @quantity * (9.0/5.0) + 491.67
|
302
|
+
else
|
303
|
+
raise ArgumentError, "Unknown temperature conversion requested"
|
304
|
+
end
|
305
|
+
when '<kelvin>':
|
306
|
+
case target.numerator[0]
|
307
|
+
when '<celcius>' : @quantity - 273.15
|
308
|
+
when '<kelvin>' : @quantity
|
309
|
+
when '<farenheit>': @quantity * (9.0/5.0) - 459.67
|
310
|
+
when '<rankine>' : @quantity * (9.0/5.0)
|
311
|
+
else
|
312
|
+
raise ArgumentError, "Unknown temperature conversion requested"
|
313
|
+
end
|
314
|
+
when '<farenheit>':
|
315
|
+
case target.numerator[0]
|
316
|
+
when '<celcius>' : (@quantity-32)*(5.0/9.0)
|
317
|
+
when '<kelvin>' : (@quantity+459.67)*(5.0/9.0)
|
318
|
+
when '<farenheit>': @quantity
|
319
|
+
when '<rankine>' : @quantity + 459.67
|
320
|
+
else
|
321
|
+
raise ArgumentError, "Unknown temperature conversion requested"
|
322
|
+
end
|
323
|
+
when '<rankine>':
|
324
|
+
case target.numerator[0]
|
325
|
+
when '<celcius>' : @quantity*(5.0/9.0) -273.15
|
326
|
+
when '<kelvin>' : @quantity*(5.0/9.0)
|
327
|
+
when '<farenheit>': @quantity - 459.67
|
328
|
+
when '<rankine>' : @quantity
|
329
|
+
else
|
330
|
+
raise ArgumentError, "Unknown temperature conversion requested"
|
331
|
+
end
|
332
|
+
else
|
333
|
+
raise ArgumentError, "Unknown temperature conversion requested"
|
334
|
+
end
|
335
|
+
Unit.new(:quantity=>q, :numerator=>target.numerator, :denominator=>target.denominator)
|
336
|
+
else
|
337
|
+
one = @numerator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:quantity] }.compact
|
338
|
+
two = @denominator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[Regexp.escape(i)][:quantity] }.compact
|
339
|
+
v = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n}
|
340
|
+
one = target.numerator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:quantity] }.compact
|
341
|
+
two = target.denominator.map {|x| @@PREFIX_VALUES[Regexp.escape(x)] ? @@PREFIX_VALUES[Regexp.escape(x)] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[Regexp.escape(x)][:quantity] }.compact
|
342
|
+
y = one.inject(1) {|product,n| product*n} / two.inject(1) {|product,n| product*n}
|
343
|
+
q = @quantity * v/y
|
344
|
+
Unit.new(:quantity=>q, :numerator=>target.numerator, :denominator=>target.denominator)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# calculates the unit signature vector used by unit_signature
|
349
|
+
def unit_signature_vector
|
350
|
+
result = self.to_base
|
351
|
+
y = [:length, :time, :temperature, :mass, :current, :substance, :luminosity, :currency, :memory, :angle]
|
352
|
+
vector = Array.new(y.size,0)
|
353
|
+
y.each_with_index do |units,index|
|
354
|
+
vector[index] = result.numerator.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size
|
355
|
+
vector[index] -= result.denominator.find_all {|x| @@UNIT_VECTORS[units].include? Regexp.escape(x)}.size
|
356
|
+
end
|
357
|
+
vector
|
358
|
+
end
|
359
|
+
|
360
|
+
# calculates the unit signature id for use in comparing compatible units and simplification
|
361
|
+
# the signature is based on a simple classification of units and is based on the following publication
|
362
|
+
#
|
363
|
+
# Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering,
|
364
|
+
# 21(8), Aug 1995, pp.651-661
|
365
|
+
# doi://10.1109/32.403789
|
366
|
+
# 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.
|
367
|
+
#
|
368
|
+
def unit_signature
|
369
|
+
vector = unit_signature_vector
|
370
|
+
vector.each_with_index {|item,index| vector[index] = item * 20**index}
|
371
|
+
@signature=vector.inject(0) {|sum,n| sum+n}
|
372
|
+
end
|
373
|
+
|
374
|
+
# Eliminates terms in the passed numerator and denominator. Expands out prefixes and applies them to the
|
375
|
+
# quantity. Returns a hash that can be used to initialize a new Unit object.
|
376
|
+
def self.eliminate_terms(q, n, d)
|
377
|
+
num = n.clone
|
378
|
+
den = d.clone
|
379
|
+
|
380
|
+
# cancel terms in both numerator and denominator
|
381
|
+
num.each_with_index do |item,index|
|
382
|
+
if i=den.index(item)
|
383
|
+
num.delete_at(index)
|
384
|
+
den.delete_at(i)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
num = num.flatten.compact
|
389
|
+
den = den.flatten.compact
|
390
|
+
|
391
|
+
# substitute in SI prefix multipliers and numerical constants
|
392
|
+
num.each_with_index do |item, index|
|
393
|
+
if item =~ /<([\dEe+-.]+)>/
|
394
|
+
q *= $1.to_f
|
395
|
+
num.delete_at(index)
|
396
|
+
elsif multiplier=@@PREFIX_VALUES[Regexp.escape(item)]
|
397
|
+
q *= multiplier
|
398
|
+
num.delete_at(index)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
den.each_with_index do |item, index|
|
403
|
+
if item =~ /<([\dEe+-.]+)>/
|
404
|
+
q /= $1.to_f
|
405
|
+
den.delete_at(index)
|
406
|
+
elsif multiplier=@@PREFIX_VALUES[Regexp.escape(item)]
|
407
|
+
q /= multiplier
|
408
|
+
den.delete_at(index)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
num = ["<1>"] if num.empty?
|
412
|
+
den = ["<1>"] if den.empty?
|
413
|
+
{:quantity=>q, :numerator=>num, :denominator=>den}
|
414
|
+
end
|
415
|
+
|
416
|
+
# returns the quantity part of the Unit
|
417
|
+
def to_f
|
418
|
+
@quantity
|
419
|
+
end
|
420
|
+
|
421
|
+
# returns the 'unit' part of the Unit object without the quantity
|
422
|
+
def to_unit
|
423
|
+
return "" if @numerator == ["<1>"] && @denominator == ["<1>"]
|
424
|
+
output_n = []
|
425
|
+
num = @numerator.clone
|
426
|
+
den = @denominator.clone
|
427
|
+
if @numerator == ["<1>"]
|
428
|
+
output_n << "1"
|
429
|
+
else
|
430
|
+
num.each_with_index do |token,index|
|
431
|
+
if token && @@PREFIX_VALUES[Regexp.escape(token)] then
|
432
|
+
output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}#{@@OUTPUT_MAP[Regexp.escape(@numerator[index+1])]}"
|
433
|
+
num[index+1]=nil
|
434
|
+
else
|
435
|
+
output_n << "#{@@OUTPUT_MAP[Regexp.escape(token)]}" if token
|
436
|
+
end
|
437
|
+
end
|
438
|
+
end
|
439
|
+
output_d = den.map do |token|
|
440
|
+
@@PREFIX_MAP[Regexp.escape(token)] ? @@OUTPUT_MAP[Regexp.escape(token)] : "#{@@OUTPUT_MAP[Regexp.escape(token)]} "
|
441
|
+
end
|
442
|
+
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]}" : ''))}
|
443
|
+
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]}" : ''))}
|
444
|
+
"#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip
|
445
|
+
end
|
446
|
+
|
447
|
+
# negates the quantity of the Unit
|
448
|
+
def -@
|
449
|
+
q = -self.quantity
|
450
|
+
Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
|
451
|
+
end
|
452
|
+
|
453
|
+
# returns abs of quantity, without the units
|
454
|
+
def abs
|
455
|
+
return @quantity.abs
|
456
|
+
end
|
457
|
+
|
458
|
+
def ceil
|
459
|
+
q = self.quantity.ceil
|
460
|
+
Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
|
461
|
+
end
|
462
|
+
|
463
|
+
def floor
|
464
|
+
q = self.quantity.floor
|
465
|
+
Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
|
466
|
+
end
|
467
|
+
|
468
|
+
def to_int
|
469
|
+
q = self.quantity.to_int
|
470
|
+
Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
|
471
|
+
end
|
472
|
+
|
473
|
+
alias :to_i :to_int
|
474
|
+
alias :truncate :to_int
|
475
|
+
|
476
|
+
def round
|
477
|
+
q = self.quantity.round
|
478
|
+
Unit.new(:quantity=>q, :numerator=>self.numerator, :denominator=>self.denominator)
|
479
|
+
end
|
480
|
+
|
481
|
+
# true if quantity is zero
|
482
|
+
def zero?
|
483
|
+
return @quantity.zero?
|
484
|
+
end
|
485
|
+
|
486
|
+
# returns self if zero? is false, nil otherwise
|
487
|
+
#def nonzero?
|
488
|
+
# self.zero? ? nil : self
|
489
|
+
#end
|
490
|
+
|
491
|
+
def update_base_quantity
|
492
|
+
@base_quantity = self.is_base? ? @quantity : self.to_base.quantity
|
493
|
+
self
|
494
|
+
end
|
495
|
+
|
496
|
+
def coerce(other)
|
497
|
+
case other
|
498
|
+
when Unit : [other, self]
|
499
|
+
when String : [Unit.new(other), self]
|
500
|
+
else
|
501
|
+
[Unit.new(other.to_s), self]
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
private
|
506
|
+
|
507
|
+
# parse a string into a unit object.
|
508
|
+
# Typical formats like :
|
509
|
+
# "5.6 kg*m/s^2"
|
510
|
+
# "5.6 kg*m*s^-2"
|
511
|
+
# "5.6 kilogram*meter*second^-2"
|
512
|
+
# "2.2 kPa"
|
513
|
+
# "37 degC"
|
514
|
+
# "1" -- creates a unitless constant with value 1
|
515
|
+
# "GPa" -- creates a unit with quantity 1 with units 'GPa'
|
516
|
+
# 6'4" -- recognized as 6 feet + 4 inches
|
517
|
+
# 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
|
518
|
+
def parse(unit_string="0")
|
519
|
+
@numerator = ['<1>']
|
520
|
+
@denominator = ['<1>']
|
521
|
+
|
522
|
+
# Special processing for unusual unit strings
|
523
|
+
# feet -- 6'5"
|
524
|
+
feet, inches = unit_string.scan(/(\d+)[\s*|'|ft|feet\s*](\d+)[\s*|"|in|inches]/)[0]
|
525
|
+
if (feet && inches)
|
526
|
+
result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches")
|
527
|
+
@quantity = result.quantity
|
528
|
+
@numerator = result.numerator
|
529
|
+
@denominator = result.denominator
|
530
|
+
@base_quantity = result.base_quantity
|
531
|
+
return self
|
532
|
+
end
|
533
|
+
|
534
|
+
# weight -- 8 lbs 12 oz
|
535
|
+
pounds, oz = unit_string.scan(/(\d+)[\s|#|lbs|pounds|,]+(\d+)[\s*|oz|ounces]/)[0]
|
536
|
+
if (pounds && oz)
|
537
|
+
result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz")
|
538
|
+
@quantity = result.quantity
|
539
|
+
@numerator = result.numerator
|
540
|
+
@denominator = result.denominator
|
541
|
+
@base_quantity = result.base_quantity
|
542
|
+
return self
|
543
|
+
end
|
544
|
+
|
545
|
+
@quantity, top, bottom = unit_string.scan(/([\dEe+.-]*)\s*([^\/]*)\/*(.+)*/)[0] #parse the string into parts
|
546
|
+
|
547
|
+
top.scan(/([^ \*]+)\^([\d-]+)/).each do |item|
|
548
|
+
n = item[1].to_i
|
549
|
+
x = "#{item[0]} "
|
550
|
+
case
|
551
|
+
when n>=0 : top.gsub!(/([^ \*]+)\^(\d+)/) {|s| x * n}
|
552
|
+
when n<0 : bottom = "#{bottom} #{x * -n}"; top.gsub!("#{item[0]}^#{item[1]}","")
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
bottom.gsub!(/([^* ]+)\^(\d+)/) {|s| "#{$1} " * $2.to_i} if bottom
|
557
|
+
|
558
|
+
if @quantity.empty?
|
559
|
+
if top =~ /[\dEe+.-]+/
|
560
|
+
@quantity = top.to_f # need this for 'number only' initialization
|
561
|
+
else
|
562
|
+
@quantity = 1 # need this for 'unit only' intialization
|
563
|
+
end
|
564
|
+
else
|
565
|
+
@quantity = @quantity.to_f
|
566
|
+
end
|
567
|
+
|
568
|
+
@numerator = top.scan(/((#{@@PREFIX_REGEX})*(#{@@UNIT_REGEX}))/).delete_if {|x| x.empty?}.compact if top
|
569
|
+
@denominator = bottom.scan(/((#{@@PREFIX_REGEX})*(#{@@UNIT_REGEX}))/).delete_if {|x| x.empty?}.compact if bottom
|
570
|
+
|
571
|
+
@numerator = @numerator.map do |item|
|
572
|
+
item.map {|x| Regexp.escape(x) if x}
|
573
|
+
@@UNIT_MAP[item[0]] ? [@@UNIT_MAP[item[0]]] : [@@PREFIX_MAP[item[1]], @@UNIT_MAP[item[2]]]
|
574
|
+
end.flatten.compact.delete_if {|x| x.empty?}
|
575
|
+
|
576
|
+
@denominator = @denominator.map do |item|
|
577
|
+
item.map {|x| Regexp.escape(x) if x}
|
578
|
+
@@UNIT_MAP[item[0]] ? [@@UNIT_MAP[item[0]]] : [@@PREFIX_MAP[item[1]], @@UNIT_MAP[item[2]]]
|
579
|
+
end.flatten.compact.delete_if {|x| x.empty?}
|
580
|
+
|
581
|
+
@numerator = ['<1>'] if @numerator.empty?
|
582
|
+
@denominator = ['<1>'] if @denominator.empty?
|
583
|
+
self
|
584
|
+
end
|
585
|
+
|
586
|
+
end
|
587
|
+
|
588
|
+
class Object
|
589
|
+
def Unit(other)
|
590
|
+
Unit.new(other.to_s)
|
591
|
+
end
|
592
|
+
|
593
|
+
def to_unit
|
594
|
+
Unit.new(self.to_s) unless Unit === self
|
595
|
+
end
|
596
|
+
end
|