ruby-units 1.3.0.a → 1.3.1

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