ruby-units 1.3.2 → 1.4.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.
Files changed (42) hide show
  1. data/CHANGELOG.txt +25 -7
  2. data/LICENSE.txt +1 -1
  3. data/README.md +68 -55
  4. data/RakeFile +27 -18
  5. data/TODO +2 -1
  6. data/VERSION +1 -1
  7. data/lib/ruby-units.rb +2 -2
  8. data/lib/ruby_units.rb +2 -2
  9. data/lib/ruby_units/array.rb +4 -2
  10. data/lib/ruby_units/date.rb +17 -4
  11. data/lib/ruby_units/definition.rb +100 -0
  12. data/lib/ruby_units/fixnum.rb +6 -4
  13. data/lib/ruby_units/math.rb +32 -2
  14. data/lib/ruby_units/numeric.rb +2 -1
  15. data/lib/ruby_units/object.rb +8 -1
  16. data/lib/ruby_units/string.rb +10 -109
  17. data/lib/ruby_units/string/extra.rb +45 -11
  18. data/lib/ruby_units/time.rb +11 -2
  19. data/lib/ruby_units/unit.rb +722 -434
  20. data/lib/ruby_units/unit_definitions.rb +3 -252
  21. data/lib/ruby_units/unit_definitions/base.rb +103 -0
  22. data/lib/ruby_units/unit_definitions/prefix.rb +40 -0
  23. data/lib/ruby_units/unit_definitions/standard.rb +705 -0
  24. data/lib/ruby_units/version.rb +1 -0
  25. data/ruby-units.gemspec +15 -20
  26. metadata +46 -35
  27. data/Gemfile +0 -12
  28. data/Manifest.txt +0 -19
  29. data/autotest/discover.rb +0 -1
  30. data/spec/ruby-units/array_spec.rb +0 -14
  31. data/spec/ruby-units/complex_spec.rb +0 -37
  32. data/spec/ruby-units/date_spec.rb +0 -38
  33. data/spec/ruby-units/math_spec.rb +0 -63
  34. data/spec/ruby-units/numeric_spec.rb +0 -12
  35. data/spec/ruby-units/object_spec.rb +0 -7
  36. data/spec/ruby-units/string/extra_spec.rb +0 -45
  37. data/spec/ruby-units/string_spec.rb +0 -20
  38. data/spec/ruby-units/time_spec.rb +0 -28
  39. data/spec/ruby-units/unit_spec.rb +0 -965
  40. data/spec/spec_helper.rb +0 -5
  41. data/test/test_cache.rb +0 -26
  42. data/test/test_ruby-units.rb +0 -976
@@ -11,6 +11,7 @@ class Time
11
11
 
12
12
  # Convert a duration to a Time value by considering the duration to be the number of seconds since the
13
13
  # epoch
14
+ # @return [Unit, Time]
14
15
  def self.at(arg)
15
16
  case arg
16
17
  when Unit
@@ -20,20 +21,25 @@ class Time
20
21
  end
21
22
  end
22
23
 
24
+ # @return (see Unit#initialize)
23
25
  def to_unit(other = nil)
24
26
  other ? Unit.new(self).convert_to(other) : Unit.new(self)
25
27
  end
26
28
  alias :unit :to_unit
27
29
  alias :u :to_unit
28
- alias :unit_add :+
29
30
 
30
31
  unless Time.instance_methods.include?(:to_date)
32
+ # :nocov_19:
33
+ # @return [Date]
31
34
  def to_date
32
35
  x=(Date.civil(1970,1,1)+((self.to_f+self.gmt_offset)/86400.0)-0.5)
33
36
  Date.civil(x.year, x.month, x.day)
34
37
  end
38
+ # :nocov_19:
35
39
  end
36
40
 
41
+ alias :unit_add :+
42
+ # @return [Unit, Time]
37
43
  def +(other)
38
44
  case other
39
45
  when Unit
@@ -48,13 +54,16 @@ class Time
48
54
  end
49
55
  end
50
56
 
51
- # usage: Time.in '5 min'
57
+ # @example
58
+ # Time.in '5 min'
59
+ # @return (see Time#+)
52
60
  def self.in(duration)
53
61
  Time.now + duration.to_unit
54
62
  end
55
63
 
56
64
  alias :unit_sub :-
57
65
 
66
+ # @return [Unit, Time]
58
67
  def -(other)
59
68
  case other
60
69
  when Unit
@@ -1,193 +1,296 @@
1
1
  require 'date'
2
2
  if RUBY_VERSION < "1.9"
3
+ # :nocov_19:
3
4
  require 'parsedate'
4
5
  require 'rational'
6
+ # :nocov_19:
5
7
  end
6
- # = Ruby Units
8
+ # Copyright 2006-2012
7
9
  #
8
- # Copyright 2006-2011 by Kevin C. Olbrich, Ph.D.
9
- #
10
- # See http://rubyforge.org/ruby-units/
10
+ # @author Kevin C. Olbrich, Ph.D.
11
+ # @see https://github.com/olbrich/ruby-units
11
12
  #
12
- # http://www.sciwerks.org
13
+ # @note The accuracy of unit conversions depends on the precision of the conversion factor.
14
+ # If you have more accurate estimates for particular conversion factors, please send them
15
+ # to me and I will incorporate them into the next release. It is also incumbent on the end-user
16
+ # to ensure that the accuracy of any conversions is sufficient for their intended application.
13
17
  #
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,
18
+ # While there are a large number of unit specified in the base package,
29
19
  # there are also a large number of units that are not included.
30
20
  # 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
21
+ # in the United States. If your favorite units are not listed here, file an issue on github.
32
22
  #
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
- # }
23
+ # To add or override a unit definition, add a code block like this..
24
+ # @example Define a new unit
25
+ # Unit.define("foobar") do |unit|
26
+ # unit.aliases = %w{foo fb foo-bar}
27
+ # unit.definition = Unit("1 baz")
39
28
  # end
40
- # Unit.setup
29
+ #
30
+ # @todo fix class variables so they conform to standard naming conventions and refactor away as many of them as possible
31
+ # @todo pull caching out into its own class
32
+ # @todo refactor internal representation of units
33
+ # @todo method to determine best natural prefix
41
34
  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>', '<ampere>','<radian>','<kelvin>','<tempK>','<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]
35
+ VERSION = Unit::Version::STRING
36
+ @@definitions = {}
37
+ @@PREFIX_VALUES = {}
38
+ @@PREFIX_MAP = {}
39
+ @@UNIT_MAP = {}
40
+ @@UNIT_VALUES = {}
41
+ @@UNIT_REGEX = nil
42
+ @@UNIT_MATCH_REGEX = nil
43
+ UNITY = '<1>'
44
+ UNITY_ARRAY = [UNITY]
45
+ FEET_INCH_REGEX = /(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/
46
+ TIME_REGEX = /(\d+)*:(\d+)*:*(\d+)*[:,]*(\d+)*/
47
+ LBS_OZ_REGEX = /(\d+)\s*(?:#|lbs|pounds|pound-mass)+[\s,]*(\d+)\s*(?:oz|ounces)/
48
+ SCI_NUMBER = %r{([+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*)}
49
+ RATIONAL_NUMBER = /\(?([+-]?\d+)\/(\d+)\)?/
50
+ COMPLEX_NUMBER = /#{SCI_NUMBER}?#{SCI_NUMBER}i\b/
51
+ NUMBER_REGEX = /#{SCI_NUMBER}*\s*(.+)?/
52
+ UNIT_STRING_REGEX = /#{SCI_NUMBER}*\s*([^\/]*)\/*(.+)*/
53
+ TOP_REGEX = /([^ \*]+)(?:\^|\*\*)([\d-]+)/
54
+ BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/
55
+ UNCERTAIN_REGEX = /#{SCI_NUMBER}\s*\+\/-\s*#{SCI_NUMBER}\s(.+)/
56
+ COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(.+)?/
57
+ RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(.+)?/
58
+ KELVIN = ['<kelvin>']
59
+ FAHRENHEIT = ['<fahrenheit>']
60
+ RANKINE = ['<rankine>']
61
+ CELSIUS = ['<celsius>']
62
+ TEMP_REGEX = /(?:temp|deg)[CFRK]/
63
+
64
+ SIGNATURE_VECTOR = [
65
+ :length,
66
+ :time,
67
+ :temperature,
68
+ :mass,
69
+ :current,
70
+ :substance,
71
+ :luminosity,
72
+ :currency,
73
+ :memory,
74
+ :angle
75
+ ]
73
76
  @@KINDS = {
74
- -312078 => :elastance,
75
- -312058=>:resistance,
76
- -312038=>:inductance,
77
- -152040=>:magnetism,
78
- -152038=>:magnetism,
79
- -152058=>:potential,
80
- -7997 => :specific_volume,
81
- -79 => :snap,
82
- -59 => :jolt,
83
- -39=>:acceleration,
84
- -38=>:radiation,
85
- -20=>:frequency,
86
- -19=>:speed,
87
- -18=>:viscosity,
88
- -17 => :volumetric_flow,
89
- -1 => :wavenumber,
90
- 0=>:unitless,
91
- 1=>:length,
92
- 2=>:area,
93
- 3=>:volume,
94
- 20=>:time,
95
- 400=>:temperature,
96
- 7941 => :yank,
97
- 7942=>:power,
98
- 7959=>:pressure,
99
- 7962=>:energy,
100
- 7979=>:viscosity,
101
- 7961=>:force,
102
- 7981 => :momentum,
103
- 7982 => :angular_momentum,
104
- 7997 => :density,
105
- 7998 => :area_density,
106
- 8000=>:mass,
107
- 159999=>:magnetism,
108
- 160000=>:current,
109
- 160020=>:charge,
110
- 312058=>:resistance,
111
- 312078=>:capacitance,
112
- 3199980=>:activity,
113
- 3199997=>:molar_concentration,
114
- 3200000=>:substance,
115
- 63999998=>:illuminance,
116
- 64000000=>:luminous_power,
117
- 1280000000=>:currency,
118
- 25600000000=>:memory,
119
- 511999999980=>:angular_velocity,
120
- 512000000000=>:angle
77
+ -312078 => :elastance,
78
+ -312058 => :resistance,
79
+ -312038 => :inductance,
80
+ -152040 => :magnetism,
81
+ -152038 => :magnetism,
82
+ -152058 => :potential,
83
+ -7997 => :specific_volume,
84
+ -79 => :snap,
85
+ -59 => :jolt,
86
+ -39 => :acceleration,
87
+ -38 => :radiation,
88
+ -20 => :frequency,
89
+ -19 => :speed,
90
+ -18 => :viscosity,
91
+ -17 => :volumetric_flow,
92
+ -1 => :wavenumber,
93
+ 0 => :unitless,
94
+ 1 => :length,
95
+ 2 => :area,
96
+ 3 => :volume,
97
+ 20 => :time,
98
+ 400 => :temperature,
99
+ 7941 => :yank,
100
+ 7942 => :power,
101
+ 7959 => :pressure,
102
+ 7962 => :energy,
103
+ 7979 => :viscosity,
104
+ 7961 => :force,
105
+ 7981 => :momentum,
106
+ 7982 => :angular_momentum,
107
+ 7997 => :density,
108
+ 7998 => :area_density,
109
+ 8000 => :mass,
110
+ 152020 => :radiation_exposure,
111
+ 159999 => :magnetism,
112
+ 160000 => :current,
113
+ 160020 => :charge,
114
+ 312058 => :resistance,
115
+ 312078 => :capacitance,
116
+ 3199980 => :activity,
117
+ 3199997 => :molar_concentration,
118
+ 3200000 => :substance,
119
+ 63999998 => :illuminance,
120
+ 64000000 => :luminous_power,
121
+ 1280000000 => :currency,
122
+ 25600000000 => :memory,
123
+ 511999999980 => :angular_velocity,
124
+ 512000000000 => :angle
121
125
  }
122
-
123
126
  @@cached_units = {}
124
127
  @@base_unit_cache = {}
125
-
128
+
129
+ # setup internal arrays and hashes
130
+ # @return [true]
126
131
  def self.setup
127
- @@ALL_UNIT_DEFINITIONS = UNIT_DEFINITIONS.merge!(@@USER_DEFINITIONS)
128
- for unit in (@@ALL_UNIT_DEFINITIONS) do
129
- key, value = unit
130
- if value[2] == :prefix then
131
- @@PREFIX_VALUES[key]=value[1]
132
- for name in value[0] do
133
- @@PREFIX_MAP[name]=key
134
- end
135
- else
136
- @@UNIT_VALUES[key]={}
137
- @@UNIT_VALUES[key][:scalar]=value[1]
138
- @@UNIT_VALUES[key][:numerator]=value[3] if value[3]
139
- @@UNIT_VALUES[key][:denominator]=value[4] if value[4]
140
- for name in value[0] do
141
- @@UNIT_MAP[name]=key
142
- end
143
- end
144
- @@OUTPUT_MAP[key]=value[0][0]
132
+ self.clear_cache
133
+ @@PREFIX_VALUES = {}
134
+ @@PREFIX_MAP = {}
135
+ @@UNIT_VALUES = {}
136
+ @@UNIT_MAP = {}
137
+ @@UNIT_REGEX = nil
138
+ @@UNIT_MATCH_REGEX = nil
139
+ @@PREFIX_REGEX = nil
140
+
141
+ @@definitions.each do |name, definition|
142
+ self.use_definition(definition)
145
143
  end
146
- @@PREFIX_REGEX = @@PREFIX_MAP.keys.sort_by {|prefix| [prefix.length, prefix]}.reverse.join('|')
147
- @@UNIT_REGEX = @@UNIT_MAP.keys.sort_by {|unit_name| [unit_name.length, unit]}.reverse.join('|')
148
- @@UNIT_MATCH_REGEX = /(#{@@PREFIX_REGEX})*?(#{@@UNIT_REGEX})\b/
144
+
149
145
  Unit.new(1)
146
+ return true
147
+ end
148
+
149
+
150
+ # determine if a unit is already defined
151
+ # @param [String] unit
152
+ # @return [Boolean]
153
+ def self.defined?(unit)
154
+ return @@UNIT_VALUES.keys.include?("<#{unit}>")
155
+ end
156
+
157
+ # return the unit definition for a unit
158
+ # @param [String] unit
159
+ # @return [Unit::Definition, nil]
160
+ def self.definition(_unit)
161
+ unit = (_unit =~ /^<.+>$/) ? _unit : "<#{_unit}>"
162
+ return @@definitions[unit]
163
+ end
164
+
165
+ # return a list of all defined units
166
+ # @return [Array]
167
+ def self.definitions
168
+ return @@definitions
169
+ end
170
+
171
+ # @param [Unit::Definition|String] unit_definition
172
+ # @param [Block] block
173
+ # @return [Unit::Definition]
174
+ # @raise [ArgumentError] when passed a non-string if using the block form
175
+ # Unpack a unit definition and add it to the array of defined units
176
+ #
177
+ # @example Block form
178
+ # Unit.define('foobar') do |foobar|
179
+ # foobar.definition = Unit("1 baz")
180
+ # end
181
+ #
182
+ # @example Unit::Definition form
183
+ # unit_definition = Unit::Definition.new("foobar") {|foobar| foobar.definition = Unit("1 baz")}
184
+ # Unit.define(unit_definition)
185
+ def self.define(unit_definition, &block)
186
+ if block_given?
187
+ raise ArgumentError, "When using the block form of Unit.define, pass the name of the unit" unless unit_definition.instance_of?(String)
188
+ unit_definition = Unit::Definition.new(unit_definition, &block)
189
+ end
190
+ Unit.definitions[unit_definition.name] = unit_definition
191
+ Unit.use_definition(unit_definition)
192
+ return unit_definition
193
+ end
194
+
195
+ # @param [String] name Name of unit to redefine
196
+ # @param [Block] block
197
+ # @raise [ArgumentError] if a block is not given
198
+ # @yield [Unit::Definition]
199
+ # @return (see Unit.define)
200
+ # Get the definition for a unit and allow it to be redefined
201
+ def self.redefine!(name, &block)
202
+ raise ArgumentError, "A block is required to redefine a unit" unless block_given?
203
+ unit_definition = self.definition(name)
204
+ yield unit_definition
205
+ self.define(unit_definition)
206
+ end
207
+
208
+ # @param [String] name of unit to undefine
209
+ # @return (see Unit.setup)
210
+ # Undefine a unit. Will not raise an exception for unknown units.
211
+ def self.undefine!(unit)
212
+ @@definitions.delete("<#{unit}>")
213
+ Unit.setup
150
214
  end
151
215
 
152
216
  include Comparable
153
- attr_accessor :scalar, :numerator, :denominator, :signature, :base_scalar, :base_numerator, :base_denominator, :output, :unit_name
154
217
 
155
- def to_yaml_properties
156
- %w{@scalar @numerator @denominator @signature @base_scalar}
157
- end
218
+ # @return [Numeric]
219
+ attr_accessor :scalar
158
220
 
221
+ # @return [Array]
222
+ attr_accessor :numerator
223
+
224
+ # @return [Array]
225
+ attr_accessor :denominator
226
+
227
+ # @return [Integer]
228
+ attr_accessor :signature
229
+
230
+ # @return [Numeric]
231
+ attr_accessor :base_scalar
232
+
233
+ # @return [Array]
234
+ attr_accessor :base_numerator
235
+
236
+ # @return [Array]
237
+ attr_accessor :base_denominator
238
+
239
+ # @return [String]
240
+ attr_accessor :output
241
+
242
+ # @return [String]
243
+ attr_accessor :unit_name
244
+
159
245
  # needed to make complex units play nice -- otherwise not detected as a complex_generic
160
-
246
+ # @param [Class]
247
+ # @return [Boolean]
161
248
  def kind_of?(klass)
162
249
  self.scalar.kind_of?(klass)
163
250
  end
164
-
251
+
252
+ # Used to copy one unit to another
253
+ # @param [Unit] from Unit to copy defintion from
254
+ # @return [Unit]
165
255
  def copy(from)
166
- @scalar = from.scalar
167
- @numerator = from.numerator
256
+ @scalar = from.scalar
257
+ @numerator = from.numerator
168
258
  @denominator = from.denominator
169
- @is_base = from.is_base?
170
- @signature = from.signature
259
+ @is_base = from.is_base?
260
+ @signature = from.signature
171
261
  @base_scalar = from.base_scalar
172
- @unit_name = from.unit_name rescue nil
262
+ @unit_name = from.unit_name rescue nil
263
+ return self
173
264
  end
174
-
175
- # basically a copy of the basic to_yaml. Needed because otherwise it ends up coercing the object to a string
176
- # before YAML'izing it.
265
+
177
266
  if RUBY_VERSION < "1.9"
267
+ # :nocov_19:
268
+
269
+ # a list of properties to emit to yaml
270
+ # @return [Array]
271
+ def to_yaml_properties
272
+ %w{@scalar @numerator @denominator @signature @base_scalar}
273
+ end
274
+
275
+ # basically a copy of the basic to_yaml. Needed because otherwise it ends up coercing the object to a string
276
+ # before YAML'izing it.
277
+ # @param [Hash] opts
278
+ # @return [String]
178
279
  def to_yaml( opts = {} )
179
280
  YAML::quick_emit( object_id, opts ) do |out|
180
281
  out.map( taguri, to_yaml_style ) do |map|
181
- for m in to_yaml_properties do
282
+ for m in to_yaml_properties do
182
283
  map.add( m[1..-1], instance_variable_get( m ) )
183
284
  end
184
285
  end
185
286
  end
186
287
  end
288
+ # :nocov_19:
187
289
  end
188
-
290
+
189
291
  # Create a new Unit object. Can be initialized using a String, a Hash, an Array, Time, DateTime
190
- # Valid formats include:
292
+ #
293
+ # @example Valid options include:
191
294
  # "5.6 kg*m/s^2"
192
295
  # "5.6 kg*m*s^-2"
193
296
  # "5.6 kilogram*meter*second^-2"
@@ -195,22 +298,29 @@ class Unit < Numeric
195
298
  # "37 degC"
196
299
  # "1" -- creates a unitless constant with value 1
197
300
  # "GPa" -- creates a unit with scalar 1 with units 'GPa'
198
- # 6'4" -- recognized as 6 feet + 4 inches
199
- # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
301
+ # "6'4\""" -- recognized as 6 feet + 4 inches
302
+ # "8 lbs 8 oz" -- recognized as 8 lbs + 8 ounces
303
+ # [1, 'kg']
304
+ # {:scalar => 1, :numerator=>'kg'}
200
305
  #
306
+ # @param [Unit,String,Hash,Array,Date,Time,DateTime] options
307
+ # @return [Unit]
308
+ # @raise [ArgumentError] if absolute value of a temperature is less than absolute zero
309
+ # @raise [ArgumentError] if no unit is specified
310
+ # @raise [ArgumentError] if an invalid unit is specified
201
311
  def initialize(*options)
202
- @scalar = nil
312
+ @scalar = nil
203
313
  @base_scalar = nil
204
- @unit_name = nil
205
- @signature = nil
206
- @output = {}
314
+ @unit_name = nil
315
+ @signature = nil
316
+ @output = {}
207
317
  if options.size == 2
208
318
  # options[0] is the scalar
209
319
  # options[1] is a unit string
210
320
  begin
211
321
  cached = @@cached_units[options[1]] * options[0]
212
322
  copy(cached)
213
- rescue
323
+ rescue
214
324
  initialize("#{options[0]} #{(options[1].units rescue options[1])}")
215
325
  end
216
326
  return
@@ -219,20 +329,20 @@ class Unit < Numeric
219
329
  options[1] = options[1].join if options[1].kind_of?(Array)
220
330
  options[2] = options[2].join if options[2].kind_of?(Array)
221
331
  begin
222
- cached = @@cached_units["#{options[1]}/#{options[2]}"] * options[0]
332
+ cached = @@cached_units["#{options[1]}/#{options[2]}"] * options[0]
223
333
  copy(cached)
224
- rescue
334
+ rescue
225
335
  initialize("#{options[0]} #{options[1]}/#{options[2]}")
226
336
  end
227
337
  return
228
338
  end
229
-
339
+
230
340
  case options[0]
231
341
  when Hash
232
- @scalar = options[0][:scalar] || 1
233
- @numerator = options[0][:numerator] || UNITY_ARRAY
342
+ @scalar = options[0][:scalar] || 1
343
+ @numerator = options[0][:numerator] || UNITY_ARRAY
234
344
  @denominator = options[0][:denominator] || UNITY_ARRAY
235
- @signature = options[0][:signature]
345
+ @signature = options[0][:signature]
236
346
  when Array
237
347
  initialize(*options[0])
238
348
  return
@@ -256,7 +366,6 @@ class Unit < Numeric
256
366
  end
257
367
  self.update_base_scalar
258
368
  raise ArgumentError, "Temperatures must not be less than absolute zero" if self.is_temperature? && self.base_scalar < 0
259
-
260
369
  unary_unit = self.units || ""
261
370
  if options.first.instance_of?(String)
262
371
  opt_scalar, opt_units = Unit.parse_into_numbers_and_units(options[0])
@@ -264,71 +373,88 @@ class Unit < Numeric
264
373
  @@cached_units[opt_units] = (self.scalar == 1 ? self : opt_units.unit) if opt_units && !opt_units.empty?
265
374
  end
266
375
  end
267
-
268
- unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /#{TEMP_REGEX}/) then
269
- @@cached_units[unary_unit] = (self.scalar == 1 ? self : unary_unit.unit)
270
- end
271
-
376
+ unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /#{TEMP_REGEX}/) then
377
+ @@cached_units[unary_unit] = (self.scalar == 1 ? self : unary_unit.unit)
378
+ end
272
379
  [@scalar, @numerator, @denominator, @base_scalar, @signature, @is_base].each {|x| x.freeze}
273
- self
380
+ return self
274
381
  end
275
382
 
383
+ # @todo: figure out how to handle :counting units. This method should probably return :counting instead of :unitless for 'each'
384
+ # return the kind of the unit (:mass, :length, etc...)
385
+ # @return [Symbol]
276
386
  def kind
277
387
  return @@KINDS[self.signature]
278
388
  end
279
-
389
+
390
+ # @private
391
+ # @return [Hash]
280
392
  def self.cached
281
393
  return @@cached_units
282
394
  end
283
-
395
+
396
+ # @private
397
+ # @return [true]
284
398
  def self.clear_cache
285
- @@cached_units = {}
399
+ @@cached_units = {}
286
400
  @@base_unit_cache = {}
287
401
  Unit.new(1)
402
+ return true
288
403
  end
289
-
404
+
405
+ # @private
406
+ # @return [Hash]
290
407
  def self.base_unit_cache
291
408
  return @@base_unit_cache
292
409
  end
293
-
294
- #
295
- # parse strings like "1 minute in seconds"
296
- #
410
+
411
+ # @example parse strings
412
+ # "1 minute in seconds"
413
+ # @param [String] input
414
+ # @return [Unit]
297
415
  def self.parse(input)
298
416
  first, second = input.scan(/(.+)\s(?:in|to|as)\s(.+)/i).first
299
- second.nil? ? first.unit : first.unit.convert_to(second)
417
+ return second.nil? ? first.unit : first.unit.convert_to(second)
300
418
  end
301
-
419
+
420
+ # @return [Unit]
302
421
  def to_unit
303
422
  self
304
423
  end
305
424
  alias :unit :to_unit
306
-
307
- # Returns 'true' if the Unit is represented in base units
425
+
426
+ # Is this unit in base form?
427
+ # @return [Boolean]
308
428
  def is_base?
309
429
  return @is_base if defined? @is_base
310
- return @is_base=true if self.degree? && self.numerator.size == 1 && self.denominator == UNITY_ARRAY && self.units =~ /(?:deg|temp)K/
311
- n = @numerator + @denominator
312
- for x in n.compact do
313
- return @is_base=false unless x == UNITY || (@@BASE_UNITS.include?((x)))
314
- end
315
- return @is_base = true
316
- end
430
+ # return @is_base = true if self.degree? &&
431
+ # self.numerator.size == 1 &&
432
+ # self.denominator == UNITY_ARRAY &&
433
+ # self.units =~ /(?:deg|temp)K/
434
+ @is_base = (@numerator + @denominator).compact.uniq.
435
+ map {|unit| Unit.definition(unit)}.
436
+ all? {|element| element.unity? || element.base? }
437
+ return @is_base
438
+ end
317
439
  alias :base? :is_base?
318
-
440
+
319
441
  # convert to base SI units
320
442
  # results of the conversion are cached so subsequent calls to this will be fast
443
+ # @return [Unit]
444
+ # @todo this is brittle as it depends on the display_name of a unit, which can be changed
321
445
  def to_base
322
446
  return self if self.is_base?
323
447
  if self.units =~ /\A(?:temp|deg)[CRF]\Z/
324
448
  if RUBY_VERSION < "1.9"
449
+ # :nocov_19:
325
450
  @signature = @@KINDS.index(:temperature)
451
+ # :nocov_19:
326
452
  else
327
453
  #:nocov:
328
454
  @signature = @@KINDS.key(:temperature)
329
455
  #:nocov:
330
456
  end
331
- base = case
457
+ base = case
332
458
  when self.is_temperature?
333
459
  self.convert_to('tempK')
334
460
  when self.is_degree?
@@ -370,19 +496,22 @@ class Unit < Numeric
370
496
  return base * @scalar
371
497
  end
372
498
  alias :base :to_base
373
-
499
+
374
500
  # Generate human readable output.
375
501
  # If the name of a unit is passed, the unit will first be converted to the target unit before output.
376
502
  # some named conversions are available
377
503
  #
378
- # :ft - outputs in feet and inches (e.g., 6'4")
379
- # :lbs - outputs in pounds and ounces (e.g, 8 lbs, 8 oz)
380
- #
504
+ # @example
505
+ # unit.to_s(:ft) - outputs in feet and inches (e.g., 6'4")
506
+ # unit.to_s(:lbs) - outputs in pounds and ounces (e.g, 8 lbs, 8 oz)
507
+ #
381
508
  # You can also pass a standard format string (i.e., '%0.2f')
382
509
  # or a strftime format string.
383
510
  #
384
511
  # output is cached so subsequent calls for the same format will be fast
385
512
  #
513
+ # @param [Symbol] target_units
514
+ # @return [String]
386
515
  def to_s(target_units=nil)
387
516
  out = @output[target_units]
388
517
  if out
@@ -401,14 +530,14 @@ class Unit < Numeric
401
530
  begin
402
531
  if $2 #unit specified, need to convert
403
532
  self.convert_to($2).to_s($1)
404
- else
533
+ else
405
534
  "#{$1 % @scalar} #{$2 || self.units}".strip
406
535
  end
407
- rescue
408
- (DateTime.new(0) + self).strftime(target_units)
536
+ rescue # parse it like a strftime format string
537
+ (DateTime.new(0) + self).strftime(target_units)
409
538
  end
410
539
  when /(\S+)/ #unit only 'mm' or '1/mm'
411
- "#{self.convert_to($1).to_s}"
540
+ self.convert_to($1).to_s
412
541
  else
413
542
  raise "unhandled case"
414
543
  end
@@ -422,47 +551,59 @@ class Unit < Numeric
422
551
  end
423
552
  @output[target_units] = out
424
553
  return out
425
- end
554
+ end
426
555
  end
427
-
556
+
428
557
  # Normally pretty prints the unit, but if you really want to see the guts of it, pass ':dump'
558
+ # @deprecated
559
+ # @return [String]
429
560
  def inspect(option=nil)
430
561
  return super() if option == :dump
431
- self.to_s
562
+ return self.to_s
432
563
  end
433
-
564
+
434
565
  # true if unit is a 'temperature', false if a 'degree' or anything else
566
+ # @return [Boolean]
567
+ # @todo use unit definition to determine if it's a temperature instead of a regex
435
568
  def is_temperature?
436
- self.is_degree? && (!(self.units =~ /temp[CFRK]/).nil?)
569
+ return self.is_degree? && (!(self.units =~ /temp[CFRK]/).nil?)
437
570
  end
438
571
  alias :temperature? :is_temperature?
439
-
572
+
440
573
  # true if a degree unit or equivalent.
574
+ # @return [Boolean]
441
575
  def is_degree?
442
- self.kind == :temperature
576
+ return self.kind == :temperature
443
577
  end
444
578
  alias :degree? :is_degree?
445
-
579
+
446
580
  # returns the 'degree' unit associated with a temperature unit
447
- # '100 tempC'.unit.temperature_scale #=> 'degC'
581
+ # @example '100 tempC'.unit.temperature_scale #=> 'degC'
582
+ # @return [String] possible values: degC, degF, degR, or degK
448
583
  def temperature_scale
449
584
  return nil unless self.is_temperature?
450
- self.units =~ /temp([CFRK])/
451
- "deg#{$1}"
585
+ return "deg#{self.units[/temp([CFRK])/,1]}"
452
586
  end
453
-
587
+
454
588
  # returns true if no associated units
455
589
  # false, even if the units are "unitless" like 'radians, each, etc'
590
+ # @return [Boolean]
456
591
  def unitless?
457
- (@numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY)
592
+ return(@numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY)
458
593
  end
459
-
594
+
460
595
  # Compare two Unit objects. Throws an exception if they are not of compatible types.
461
596
  # Comparisons are done based on the value of the unit in base SI units.
597
+ # @param [Object] other
598
+ # @return [-1|0|1|nil]
599
+ # @raise [NoMethodError] when other does not define <=>
600
+ # @raise [ArgumentError] when units are not compatible
462
601
  def <=>(other)
463
602
  case
464
603
  when !self.base_scalar.respond_to?(:<=>)
465
604
  raise NoMethodError, "undefined method `<=>' for #{self.base_scalar.inspect}"
605
+ when other.nil?
606
+ return self.base_scalar <=> nil
466
607
  when !self.is_temperature? && other.zero?
467
608
  return self.base_scalar <=> 0
468
609
  when other.instance_of?(Unit)
@@ -473,13 +614,15 @@ class Unit < Numeric
473
614
  return x <=> y
474
615
  end
475
616
  end
476
-
617
+
477
618
  # Compare Units for equality
478
619
  # this is necessary mostly for Complex units. Complex units do not have a <=> operator
479
620
  # so we define this one here so that we can properly check complex units for equality.
480
621
  # Units of incompatible types are not equal, except when they are both zero and neither is a temperature
481
622
  # Equality checks can be tricky since round off errors may make essentially equivalent units
482
623
  # appear to be different.
624
+ # @param [Object] other
625
+ # @return [Boolean]
483
626
  def ==(other)
484
627
  case
485
628
  when other.respond_to?(:zero?) && other.zero?
@@ -488,51 +631,72 @@ class Unit < Numeric
488
631
  return false unless self =~ other
489
632
  return self.base_scalar == other.base_scalar
490
633
  else
491
- x,y = coerce(other)
492
- return x == y
493
- end
634
+ begin
635
+ x,y = coerce(other)
636
+ return x == y
637
+ rescue ArgumentError # return false when object cannot be coerced
638
+ return false
639
+ end
640
+ end
494
641
  end
495
-
642
+
496
643
  # check to see if units are compatible, but not the scalar part
497
644
  # this check is done by comparing signatures for performance reasons
498
645
  # if passed a string, it will create a unit object with the string and then do the comparison
499
- # this permits a syntax like:
646
+ # @example this permits a syntax like:
500
647
  # unit =~ "mm"
501
- # if you want to do a regexp on the unit string do this ...
648
+ # @note if you want to do a regexp comparison of the unit string do this ...
502
649
  # unit.units =~ /regexp/
650
+ # @param [Object] other
651
+ # @return [Boolean]
503
652
  def =~(other)
504
653
  case other
505
654
  when Unit
506
655
  self.signature == other.signature
507
656
  else
508
- x,y = coerce(other)
509
- x =~ y
510
- end
657
+ begin
658
+ x,y = coerce(other)
659
+ return x =~ y
660
+ rescue ArgumentError
661
+ return false
662
+ end
663
+ end
511
664
  end
512
-
665
+
513
666
  alias :compatible? :=~
514
667
  alias :compatible_with? :=~
515
-
668
+
516
669
  # Compare two units. Returns true if quantities and units match
517
- #
518
- # Unit("100 cm") === Unit("100 cm") # => true
519
- # Unit("100 cm") === Unit("1 m") # => false
670
+ # @example
671
+ # Unit("100 cm") === Unit("100 cm") # => true
672
+ # Unit("100 cm") === Unit("1 m") # => false
673
+ # @param [Object] other
674
+ # @return [Boolean]
520
675
  def ===(other)
521
676
  case other
522
677
  when Unit
523
678
  (self.scalar == other.scalar) && (self.units == other.units)
524
679
  else
525
- x,y = coerce(other)
526
- x === y
680
+ begin
681
+ x,y = coerce(other)
682
+ return x === y
683
+ rescue ArgumentError
684
+ return false
685
+ end
527
686
  end
528
687
  end
529
-
688
+
530
689
  alias :same? :===
531
690
  alias :same_as? :===
532
-
691
+
533
692
  # Add two units together. Result is same units as receiver and scalar and base_scalar are updated appropriately
534
693
  # throws an exception if the units are not compatible.
535
694
  # It is possible to add Time objects to units of time
695
+ # @param [Object] other
696
+ # @return [Unit]
697
+ # @raise [ArgumentError] when two temperatures are added
698
+ # @raise [ArgumentError] when units are not compatible
699
+ # @raise [ArgumentError] when adding a fixed time or date to a time span
536
700
  def +(other)
537
701
  case other
538
702
  when Unit
@@ -561,9 +725,13 @@ class Unit < Numeric
561
725
  y + x
562
726
  end
563
727
  end
564
-
728
+
565
729
  # Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately
566
- # throws an exception if the units are not compatible.
730
+ # @param [Numeric] other
731
+ # @return [Unit]
732
+ # @raise [ArgumentError] when subtracting a temperature from a degree
733
+ # @raise [ArgumentError] when units are not compatible
734
+ # @raise [ArgumentError] when subtracting a fixed time from a time span
567
735
  def -(other)
568
736
  case other
569
737
  when Unit
@@ -573,14 +741,14 @@ class Unit < Numeric
573
741
  when self =~ other
574
742
  case
575
743
  when [self, other].all? {|x| x.is_temperature?}
576
- Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => KELVIN, :denominator => UNITY_ARRAY, :signature => @signature).convert_to(self.temperature_scale)
744
+ Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => KELVIN, :denominator => UNITY_ARRAY, :signature => @signature).convert_to(self.temperature_scale)
577
745
  when self.is_temperature?
578
- Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => ['<tempK>'], :denominator => UNITY_ARRAY, :signature => @signature).convert_to(self)
746
+ Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => ['<tempK>'], :denominator => UNITY_ARRAY, :signature => @signature).convert_to(self)
579
747
  when other.is_temperature?
580
748
  raise ArgumentError, "Cannot subtract a temperature from a differential degree unit"
581
749
  else
582
750
  @q ||= ((@@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar) rescue (self.units.unit.scalar/self.units.unit.to_base.scalar))
583
- Unit.new(:scalar=>(self.base_scalar - other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature=>@signature)
751
+ Unit.new(:scalar=>(self.base_scalar - other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature=>@signature)
584
752
  end
585
753
  else
586
754
  raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')"
@@ -589,48 +757,57 @@ class Unit < Numeric
589
757
  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"
590
758
  else
591
759
  x,y = coerce(other)
592
- y-x
760
+ return y-x
593
761
  end
594
762
  end
595
-
763
+
596
764
  # Multiply two units.
765
+ # @param [Numeric] other
766
+ # @return [Unit]
767
+ # @raise [ArgumentError] when attempting to multiply two temperatures
597
768
  def *(other)
598
769
  case other
599
770
  when Unit
600
771
  raise ArgumentError, "Cannot multiply by temperatures" if [other,self].any? {|x| x.is_temperature?}
601
772
  opts = Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator)
602
773
  opts.merge!(:signature => @signature + other.signature)
603
- Unit.new(opts)
774
+ return Unit.new(opts)
604
775
  when Numeric
605
- Unit.new(:scalar=>@scalar*other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
776
+ return Unit.new(:scalar=>@scalar*other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
606
777
  else
607
778
  x,y = coerce(other)
608
- x * y
779
+ return x * y
609
780
  end
610
781
  end
611
-
782
+
612
783
  # Divide two units.
613
784
  # Throws an exception if divisor is 0
785
+ # @param [Numeric] other
786
+ # @return [Unit]
787
+ # @raise [ZeroDivisionError] if divisor is zero
788
+ # @raise [ArgumentError] if attempting to divide a temperature by another temperature
614
789
  def /(other)
615
790
  case other
616
- when Unit
791
+ when Unit
617
792
  raise ZeroDivisionError if other.zero?
618
793
  raise ArgumentError, "Cannot divide with temperatures" if [other,self].any? {|x| x.is_temperature?}
619
794
  opts = Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator)
620
795
  opts.merge!(:signature=> @signature - other.signature)
621
- Unit.new(opts)
796
+ return Unit.new(opts)
622
797
  when Numeric
623
798
  raise ZeroDivisionError if other.zero?
624
- Unit.new(:scalar=>@scalar/other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
799
+ return Unit.new(:scalar=>@scalar/other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature)
625
800
  else
626
801
  x,y = coerce(other)
627
- y / x
802
+ return y / x
628
803
  end
629
804
  end
630
805
 
631
806
  # divide two units and return quotient and remainder
632
807
  # when both units are in the same units we just use divmod on the raw scalars
633
808
  # otherwise we use the scalar of the base unit which will be a float
809
+ # @param [Object] other
810
+ # @return [Array]
634
811
  def divmod(other)
635
812
  raise ArgumentError, "Incompatible Units" unless self =~ other
636
813
  if self.units == other.units
@@ -641,11 +818,13 @@ class Unit < Numeric
641
818
  end
642
819
 
643
820
  # perform a modulo on a unit, will raise an exception if the units are not compatible
821
+ # @param [Object] other
822
+ # @return [Integer]
644
823
  def %(other)
645
- self.divmod(other).last
824
+ return self.divmod(other).last
646
825
  end
647
-
648
- # Exponentiate. Only takes integer powers.
826
+
827
+ # Exponentiate. Only takes integer powers.
649
828
  # Note that anything raised to the power of 0 results in a Unit object with a scalar of 1, and no units.
650
829
  # Throws an exception if exponent is not an integer.
651
830
  # Ideally this routine should accept a float for the exponent
@@ -653,6 +832,12 @@ class Unit < Numeric
653
832
  # but, sadly, floats can't be converted to rationals.
654
833
  #
655
834
  # For now, if a rational is passed in, it will be used, otherwise we are stuck with integers and certain floats < 1
835
+ # @param [Numeric] other
836
+ # @return [Unit]
837
+ # @raise [ArgumentError] when raising a temperature to a power
838
+ # @raise [ArgumentError] when n not in the set integers from (1..9)
839
+ # @raise [ArgumentError] when attempting to raise to a complex number
840
+ # @raise [ArgumentError] when an invalid exponent is passed
656
841
  def **(other)
657
842
  raise ArgumentError, "Cannot raise a temperature to a power" if self.is_temperature?
658
843
  if other.kind_of?(Numeric)
@@ -662,14 +847,14 @@ class Unit < Numeric
662
847
  end
663
848
  case other
664
849
  when Rational
665
- self.power(other.numerator).root(other.denominator)
850
+ return self.power(other.numerator).root(other.denominator)
666
851
  when Integer
667
- self.power(other)
852
+ return self.power(other)
668
853
  when Float
669
854
  return self**(other.to_i) if other == other.to_i
670
855
  valid = (1..9).map {|x| 1/x}
671
856
  raise ArgumentError, "Not a n-th root (1..9), use 1/n" unless valid.include? other.abs
672
- self.root((1/other).to_int)
857
+ return self.root((1/other).to_int)
673
858
  when Complex
674
859
  raise ArgumentError, "exponentiation of complex numbers is not yet supported."
675
860
  else
@@ -677,75 +862,89 @@ class Unit < Numeric
677
862
  end
678
863
  end
679
864
 
680
- # returns the unit raised to the n-th power. Integers only
865
+ # returns the unit raised to the n-th power
866
+ # @param [Integer] n
867
+ # @return [Unit]
868
+ # @raise [ArgumentError] when attempting to raise a temperature to a power
869
+ # @raise [ArgumentError] when n is not an integer
681
870
  def power(n)
682
871
  raise ArgumentError, "Cannot raise a temperature to a power" if self.is_temperature?
683
872
  raise ArgumentError, "Exponent must an Integer" unless n.kind_of?(Integer)
684
873
  return self.inverse if n == -1
685
874
  return 1 if n.zero?
686
875
  return self if n == 1
687
- if n > 0 then
688
- (1..(n-1).to_i).inject(self) {|product, x| product * self}
876
+ if n > 0 then
877
+ return (1..(n-1).to_i).inject(self) {|product, x| product * self}
689
878
  else
690
- (1..-(n-1).to_i).inject(self) {|product, x| product / self}
879
+ return (1..-(n-1).to_i).inject(self) {|product, x| product / self}
691
880
  end
692
881
  end
693
-
694
- # Calculates the n-th root of a unit, where n = (1..9)
882
+
883
+ # Calculates the n-th root of a unit
695
884
  # if n < 0, returns 1/unit^(1/n)
885
+ # @param [Integer] n
886
+ # @return [Unit]
887
+ # @raise [ArgumentError] when attemptint to take the root of a temperature
888
+ # @raise [ArgumentError] when n is not an integer
889
+ # @raise [ArgumentError] when n is 0
696
890
  def root(n)
697
891
  raise ArgumentError, "Cannot take the root of a temperature" if self.is_temperature?
698
892
  raise ArgumentError, "Exponent must an Integer" unless n.kind_of?(Integer)
699
893
  raise ArgumentError, "0th root undefined" if n.zero?
700
894
  return self if n == 1
701
895
  return self.root(n.abs).inverse if n < 0
702
-
896
+
703
897
  vec = self.unit_signature_vector
704
- vec=vec.map {|x| x % n}
898
+ vec=vec.map {|x| x % n}
705
899
  raise ArgumentError, "Illegal root" unless vec.max == 0
706
900
  num = @numerator.dup
707
901
  den = @denominator.dup
708
-
902
+
709
903
  for item in @numerator.uniq do
710
904
  x = num.find_all {|i| i==item}.size
711
905
  r = ((x/n)*(n-1)).to_int
712
906
  r.times {|y| num.delete_at(num.index(item))}
713
907
  end
714
-
715
- for item in @denominator.uniq do
908
+
909
+ for item in @denominator.uniq do
716
910
  x = den.find_all {|i| i==item}.size
717
911
  r = ((x/n)*(n-1)).to_int
718
912
  r.times {|y| den.delete_at(den.index(item))}
719
913
  end
720
914
  q = @scalar < 0 ? (-1)**Rational(1,n) * (@scalar.abs)**Rational(1,n) : @scalar**Rational(1,n)
721
- Unit.new(:scalar=>q,:numerator=>num,:denominator=>den)
915
+ return Unit.new(:scalar=>q,:numerator=>num,:denominator=>den)
722
916
  end
723
-
917
+
724
918
  # returns inverse of Unit (1/unit)
919
+ # @return [Unit]
725
920
  def inverse
726
- Unit("1") / self
921
+ return Unit("1") / self
727
922
  end
728
-
923
+
729
924
  # convert to a specified unit string or to the same units as another Unit
730
- #
731
- # unit >> "kg" will covert to kilograms
732
- # unit1 >> unit2 converts to same units as unit2 object
733
- #
925
+ #
926
+ # unit.convert_to "kg" will covert to kilograms
927
+ # unit1.convert_to unit2 converts to same units as unit2 object
928
+ #
734
929
  # To convert a Unit object to match another Unit object, use:
735
930
  # unit1 >>= unit2
736
- # Throws an exception if the requested target units are incompatible with current Unit.
737
931
  #
738
932
  # Special handling for temperature conversions is supported. If the Unit object is converted
739
933
  # from one temperature unit to another, the proper temperature offsets will be used.
740
- # Supports Kelvin, Celsius, fahrenheit, and Rankine scales.
934
+ # Supports Kelvin, Celsius, Fahrenheit, and Rankine scales.
741
935
  #
742
- # Note that if temperature is part of a compound unit, the temperature will be treated as a differential
743
- # and the units will be scaled appropriately.
936
+ # @note If temperature is part of a compound unit, the temperature will be treated as a differential
937
+ # and the units will be scaled appropriately.
938
+ # @param [Object] other
939
+ # @return [Unit]
940
+ # @raise [ArgumentError] when attempting to convert a degree to a temperature
941
+ # @raise [ArgumentError] when target unit is unknown
942
+ # @raise [ArgumentError] when target unit is incompatible
744
943
  def convert_to(other)
745
- return self if other.nil?
944
+ return self if other.nil?
746
945
  return self if TrueClass === other
747
946
  return self if FalseClass === other
748
- if (Unit === other && other.is_temperature?) || (String === other && other =~ /temp[CFRK]/)
947
+ if (Unit === other && other.is_temperature?) || (String === other && other =~ /temp[CFRK]/)
749
948
  raise ArgumentError, "Receiver is not a temperature unit" unless self.degree?
750
949
  start_unit = self.units
751
950
  target_unit = other.units rescue other
@@ -765,13 +964,13 @@ class Unit < Numeric
765
964
  when 'tempC'
766
965
  @base_scalar - 273.15
767
966
  when 'tempK'
768
- @base_scalar
967
+ @base_scalar
769
968
  when 'tempF'
770
969
  @base_scalar * Rational(9,5) - 459.67
771
970
  when 'tempR'
772
971
  @base_scalar * Rational(9,5)
773
972
  end
774
- Unit.new("#{q} #{target_unit}")
973
+ return Unit.new("#{q} #{target_unit}")
775
974
  else
776
975
  case other
777
976
  when Unit
@@ -787,225 +986,260 @@ class Unit < Numeric
787
986
  _denominator1 = @denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact
788
987
  _numerator2 = target.numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact
789
988
  _denominator2 = target.denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact
790
-
791
- q = @scalar * ( (_numerator1 + _denominator2).inject(1) {|product,n| product*n} ) /
792
- ( (_numerator2 + _denominator1).inject(1) {|product,n| product*n} )
793
-
794
-
795
- Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator, :signature => target.signature)
989
+
990
+ q = @scalar * ( (_numerator1 + _denominator2).inject(1) {|product,n| product*n} ) /
991
+ ( (_numerator2 + _denominator1).inject(1) {|product,n| product*n} )
992
+ return Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator, :signature => target.signature)
796
993
  end
797
- end
994
+ end
798
995
  alias :>> :convert_to
799
996
  alias :to :convert_to
800
-
997
+
801
998
  # converts the unit back to a float if it is unitless. Otherwise raises an exception
999
+ # @return [Float]
1000
+ # @raise [RuntimeError] when not unitless
802
1001
  def to_f
803
1002
  return @scalar.to_f if self.unitless?
804
1003
  raise RuntimeError, "Cannot convert '#{self.to_s}' to Float unless unitless. Use Unit#scalar"
805
1004
  end
806
-
1005
+
807
1006
  # converts the unit back to a complex if it is unitless. Otherwise raises an exception
1007
+ # @return [Complex]
1008
+ # @raise [RuntimeError] when not unitless
808
1009
  def to_c
809
1010
  return Complex(@scalar) if self.unitless?
810
1011
  raise RuntimeError, "Cannot convert '#{self.to_s}' to Complex unless unitless. Use Unit#scalar"
811
1012
  end
812
1013
 
813
1014
  # if unitless, returns an int, otherwise raises an error
1015
+ # @return [Integer]
1016
+ # @raise [RuntimeError] when not unitless
814
1017
  def to_i
815
1018
  return @scalar.to_int if self.unitless?
816
1019
  raise RuntimeError, "Cannot convert '#{self.to_s}' to Integer unless unitless. Use Unit#scalar"
817
1020
  end
818
1021
  alias :to_int :to_i
819
-
1022
+
820
1023
  # if unitless, returns a Rational, otherwise raises an error
1024
+ # @return [Rational]
1025
+ # @raise [RuntimeError] when not unitless
821
1026
  def to_r
822
1027
  return @scalar.to_r if self.unitless?
823
1028
  raise RuntimeError, "Cannot convert '#{self.to_s}' to Rational unless unitless. Use Unit#scalar"
824
1029
  end
825
-
1030
+
826
1031
  # returns the 'unit' part of the Unit object without the scalar
1032
+ # @return [String]
827
1033
  def units
828
1034
  return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
829
1035
  return @unit_name unless @unit_name.nil?
830
- output_n = []
831
- output_d =[]
832
- num = @numerator.clone.compact
833
- den = @denominator.clone.compact
1036
+ output_numerator = []
1037
+ output_denominator = []
1038
+ num = @numerator.clone.compact
1039
+ den = @denominator.clone.compact
1040
+
834
1041
  if @numerator == UNITY_ARRAY
835
- output_n << "1"
1042
+ output_numerator << "1"
836
1043
  else
837
- num.each_with_index do |token,index|
838
- if token && @@PREFIX_VALUES[token] then
839
- output_n << "#{@@OUTPUT_MAP[token]}#{@@OUTPUT_MAP[num[index+1]]}"
840
- num[index+1]=nil
1044
+ while defn = Unit.definition(num.shift) do
1045
+ if defn && defn.prefix?
1046
+ output_numerator << defn.display_name + Unit.definition(num.shift).display_name
841
1047
  else
842
- output_n << "#{@@OUTPUT_MAP[token]}" if token
1048
+ output_numerator << defn.display_name
843
1049
  end
844
1050
  end
845
1051
  end
1052
+
846
1053
  if @denominator == UNITY_ARRAY
847
- output_d = ['1']
1054
+ output_denominator = []
848
1055
  else
849
- den.each_with_index do |token,index|
850
- if token && @@PREFIX_VALUES[token] then
851
- output_d << "#{@@OUTPUT_MAP[token]}#{@@OUTPUT_MAP[den[index+1]]}"
852
- den[index+1]=nil
1056
+ while defn = Unit.definition(den.shift) do
1057
+ if defn && defn.prefix?
1058
+ output_denominator << defn.display_name + Unit.definition(den.shift).display_name
853
1059
  else
854
- output_d << "#{@@OUTPUT_MAP[token]}" if token
1060
+ output_denominator << defn.display_name
855
1061
  end
856
1062
  end
857
1063
  end
858
- 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]}" : ''))}
859
- 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]}" : ''))}
860
- out = "#{on.join('*')}#{od == ['1'] ? '': '/'+od.join('*')}".strip
1064
+
1065
+ on = output_numerator.uniq.
1066
+ map {|x| [x, output_numerator.count(x)]}.
1067
+ map {|element, power| ("#{element}".strip + (power > 1 ? "^#{power}" : ''))}
1068
+ od = output_denominator.uniq.
1069
+ map {|x| [x, output_denominator.count(x)]}.
1070
+ map {|element, power| ("#{element}".strip + (power > 1 ? "^#{power}" : ''))}
1071
+ out = "#{on.join('*')}#{od.empty? ? '': '/' + od.join('*')}".strip
861
1072
  @unit_name = out unless self.kind == :temperature
862
1073
  return out
863
1074
  end
864
-
1075
+
865
1076
  # negates the scalar of the Unit
1077
+ # @return [Numeric,Unit]
866
1078
  def -@
867
1079
  return -@scalar if self.unitless?
868
- self.dup * -1
1080
+ return (self.dup * -1)
869
1081
  end
870
-
1082
+
1083
+ # absolute value of a unit
1084
+ # @return [Numeric,Unit]
871
1085
  def abs
872
1086
  return @scalar.abs if self.unitless?
873
- Unit.new(@scalar.abs, @numerator, @denominator)
1087
+ return Unit.new(@scalar.abs, @numerator, @denominator)
874
1088
  end
875
-
1089
+
1090
+ # ceil of a unit
1091
+ # @return [Numeric,Unit]
876
1092
  def ceil
877
1093
  return @scalar.ceil if self.unitless?
878
- Unit.new(@scalar.ceil, @numerator, @denominator)
1094
+ return Unit.new(@scalar.ceil, @numerator, @denominator)
879
1095
  end
880
-
1096
+
1097
+ # @return [Numeric,Unit]
881
1098
  def floor
882
1099
  return @scalar.floor if self.unitless?
883
- Unit.new(@scalar.floor, @numerator, @denominator)
1100
+ return Unit.new(@scalar.floor, @numerator, @denominator)
884
1101
  end
885
1102
 
1103
+ # @return [Numeric,Unit]
886
1104
  def round
887
1105
  return @scalar.round if self.unitless?
888
- Unit.new(@scalar.round, @numerator, @denominator)
1106
+ return Unit.new(@scalar.round, @numerator, @denominator)
889
1107
  end
890
1108
 
1109
+ # @return [Numeric, Unit]
891
1110
  def truncate
892
1111
  return @scalar.truncate if self.unitless?
893
- Unit.new(@scalar.truncate, @numerator, @denominator)
1112
+ return Unit.new(@scalar.truncate, @numerator, @denominator)
894
1113
  end
895
1114
 
896
1115
  # returns next unit in a range. '1 mm'.unit.succ #=> '2 mm'.unit
897
- # only works when the scalar is an integer
1116
+ # only works when the scalar is an integer
1117
+ # @return [Unit]
1118
+ # @raise [ArgumentError] when scalar is not equal to an integer
898
1119
  def succ
899
1120
  raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
900
- Unit.new(@scalar.to_i.succ, @numerator, @denominator)
1121
+ return Unit.new(@scalar.to_i.succ, @numerator, @denominator)
901
1122
  end
902
1123
  alias :next :succ
903
-
904
- # returns next unit in a range. '1 mm'.unit.succ #=> '2 mm'.unit
905
- # only works when the scalar is an integer
1124
+
1125
+ # returns previous unit in a range. '2 mm'.unit.pred #=> '1 mm'.unit
1126
+ # only works when the scalar is an integer
1127
+ # @return [Unit]
1128
+ # @raise [ArgumentError] when scalar is not equal to an integer
906
1129
  def pred
907
1130
  raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
908
- Unit.new(@scalar.to_i.pred, @numerator, @denominator)
1131
+ return Unit.new(@scalar.to_i.pred, @numerator, @denominator)
909
1132
  end
910
-
911
-
1133
+
912
1134
  # Tries to make a Time object from current unit. Assumes the current unit hold the duration in seconds from the epoch.
1135
+ # @return [Time]
913
1136
  def to_time
914
- Time.at(self)
1137
+ return Time.at(self)
915
1138
  end
916
1139
  alias :time :to_time
917
1140
 
918
-
919
1141
  # convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date
920
1142
  # defined by DateTime
1143
+ # @return [DateTime]
921
1144
  def to_datetime
922
- DateTime.new!(self.convert_to('d').scalar)
1145
+ return DateTime.new!(self.convert_to('d').scalar)
923
1146
  end
924
-
1147
+
1148
+ # @return [Date]
925
1149
  def to_date
926
- Date.new0(self.convert_to('d').scalar)
1150
+ return Date.new0(self.convert_to('d').scalar)
927
1151
  end
928
-
929
-
1152
+
930
1153
  # true if scalar is zero
1154
+ # @return [Boolean]
931
1155
  def zero?
932
1156
  return self.base_scalar.zero?
933
1157
  end
934
-
935
- # '5 min'.unit.ago
1158
+
1159
+ # @example '5 min'.unit.ago
1160
+ # @return [Unit]
936
1161
  def ago
937
- self.before
1162
+ return self.before
938
1163
  end
939
-
940
- # '5 min'.before(time)
1164
+
1165
+ # @example '5 min'.before(time)
1166
+ # @return [Unit]
941
1167
  def before(time_point = ::Time.now)
942
1168
  case time_point
943
1169
  when Time, Date, DateTime
944
- time_point - self rescue time_point.to_datetime - self
1170
+ return (time_point - self rescue time_point.to_datetime - self)
945
1171
  else
946
1172
  raise ArgumentError, "Must specify a Time, Date, or DateTime"
947
1173
  end
948
1174
  end
949
1175
  alias :before_now :before
950
-
951
- # 'min'.since(time)
1176
+
1177
+ # @example 'min'.since(time)
1178
+ # @param [Time, Date, DateTime] time_point
1179
+ # @return [Unit]
1180
+ # @raise [ArgumentError] when time point is not a Time, Date, or DateTime
952
1181
  def since(time_point)
953
1182
  case time_point
954
1183
  when Time
955
- (Time.now - time_point).unit('s').convert_to(self)
1184
+ return (Time.now - time_point).unit('s').convert_to(self)
956
1185
  when DateTime, Date
957
- (DateTime.now - time_point).unit('d').convert_to(self)
1186
+ return (DateTime.now - time_point).unit('d').convert_to(self)
958
1187
  else
959
- raise ArgumentError, "Must specify a Time, Date, or DateTime"
1188
+ raise ArgumentError, "Must specify a Time, Date, or DateTime"
960
1189
  end
961
1190
  end
962
-
963
- # 'min'.until(time)
1191
+
1192
+ # @example 'min'.until(time)
1193
+ # @param [Time, Date, DateTime] time_point
1194
+ # @return [Unit]
964
1195
  def until(time_point)
965
1196
  case time_point
966
1197
  when Time
967
- (time_point - Time.now).unit('s').convert_to(self)
1198
+ return (time_point - Time.now).unit('s').convert_to(self)
968
1199
  when DateTime, Date
969
- (time_point - DateTime.now).unit('d').convert_to(self)
1200
+ return (time_point - DateTime.now).unit('d').convert_to(self)
970
1201
  else
971
- raise ArgumentError, "Must specify a Time, Date, or DateTime"
1202
+ raise ArgumentError, "Must specify a Time, Date, or DateTime"
972
1203
  end
973
1204
  end
974
-
975
- # '5 min'.from(time)
1205
+
1206
+ # @example '5 min'.from(time)
1207
+ # @param [Time, Date, DateTime] time_point
1208
+ # @return [Time, Date, DateTime]
1209
+ # @raise [ArgumentError] when passed argument is not a Time, Date, or DateTime
976
1210
  def from(time_point)
977
1211
  case time_point
978
1212
  when Time, DateTime, Date
979
- time_point + self rescue time_point.to_datetime + self
1213
+ return (time_point + self rescue time_point.to_datetime + self)
980
1214
  else
981
- raise ArgumentError, "Must specify a Time, Date, or DateTime"
1215
+ raise ArgumentError, "Must specify a Time, Date, or DateTime"
982
1216
  end
983
1217
  end
984
1218
  alias :after :from
985
1219
  alias :from_now :from
986
-
987
-
988
1220
 
989
1221
  # automatically coerce objects to units when possible
990
1222
  # if an object defines a 'to_unit' method, it will be coerced using that method
1223
+ # @param [Object, #to_unit]
1224
+ # @return [Array]
991
1225
  def coerce(other)
992
1226
  if other.respond_to? :to_unit
993
1227
  return [other.to_unit, self]
994
1228
  end
995
1229
  case other
996
1230
  when Unit
997
- [other, self]
998
- else
999
- [Unit.new(other), self]
1231
+ return [other, self]
1232
+ else
1233
+ return [Unit.new(other), self]
1000
1234
  end
1001
1235
  end
1002
-
1236
+
1003
1237
  # Protected and Private Functions that should only be called from this class
1004
1238
  protected
1005
-
1006
-
1239
+
1240
+ # figure out what the scalar part of the base unit for this unit is
1241
+ # @return [nil]
1007
1242
  def update_base_scalar
1008
- return @base_scalar unless @base_scalar.nil?
1009
1243
  if self.is_base?
1010
1244
  @base_scalar = @scalar
1011
1245
  @signature = unit_signature
@@ -1015,59 +1249,65 @@ class Unit < Numeric
1015
1249
  @signature = base.signature
1016
1250
  end
1017
1251
  end
1018
-
1252
+
1019
1253
  # calculates the unit signature vector used by unit_signature
1254
+ # @return [Array]
1255
+ # @raise [ArgumentError] when exponent associated with a unit is > 20 or < -20
1020
1256
  def unit_signature_vector
1021
1257
  return self.to_base.unit_signature_vector unless self.is_base?
1022
1258
  vector = Array.new(SIGNATURE_VECTOR.size,0)
1023
- for element in @numerator
1024
- if r=@@ALL_UNIT_DEFINITIONS[element]
1025
- n = SIGNATURE_VECTOR.index(r[2])
1026
- vector[n] = vector[n] + 1 if n
1027
- end
1259
+ # it's possible to have a kind that misses the array... kinds like :counting
1260
+ # are more like prefixes, so don't use them to calculate the vector
1261
+ @numerator.map {|element| Unit.definition(element)}.each do |definition|
1262
+ index = SIGNATURE_VECTOR.index(definition.kind)
1263
+ vector[index] += 1 if index
1028
1264
  end
1029
- for element in @denominator
1030
- if r=@@ALL_UNIT_DEFINITIONS[element]
1031
- n = SIGNATURE_VECTOR.index(r[2])
1032
- vector[n] = vector[n] - 1 if n
1033
- end
1265
+ @denominator.map {|element| Unit.definition(element)}.each do |definition|
1266
+ index = SIGNATURE_VECTOR.index(definition.kind)
1267
+ vector[index] -= 1 if index
1034
1268
  end
1035
1269
  raise ArgumentError, "Power out of range (-20 < net power of a unit < 20)" if vector.any? {|x| x.abs >=20}
1036
- vector
1270
+ return vector
1037
1271
  end
1038
-
1272
+
1039
1273
  private
1040
-
1274
+
1275
+ # used by #dup to duplicate a Unit
1276
+ # @param [Unit] other
1277
+ # @private
1041
1278
  def initialize_copy(other)
1042
1279
  @numerator = other.numerator.dup
1043
- @denominator = other.denominator.dup
1280
+ @denominator = other.denominator.dup
1044
1281
  end
1045
-
1282
+
1046
1283
  # calculates the unit signature id for use in comparing compatible units and simplification
1047
1284
  # the signature is based on a simple classification of units and is based on the following publication
1048
- #
1049
- # Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering,
1050
- # 21(8), Aug 1995, pp.651-661
1051
- # doi://10.1109/32.403789
1052
- # 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.
1053
1285
  #
1286
+ # Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering, 21(8), Aug 1995, pp.651-661
1287
+ # @see http://doi.ieeecomputersociety.org/10.1109/32.403789
1288
+ # @return [Array]
1054
1289
  def unit_signature
1055
1290
  return @signature unless @signature.nil?
1056
1291
  vector = unit_signature_vector
1057
1292
  vector.each_with_index {|item,index| vector[index] = item * 20**index}
1058
1293
  @signature=vector.inject(0) {|sum,n| sum+n}
1294
+ return @signature
1059
1295
  end
1060
-
1296
+
1297
+ # @param [Numeric] q quantity
1298
+ # @param [Array] n numerator
1299
+ # @param [Array] d denominator
1300
+ # @return [Hash]
1061
1301
  def self.eliminate_terms(q, n, d)
1062
1302
  num = n.dup
1063
1303
  den = d.dup
1064
-
1304
+
1065
1305
  num.delete_if {|v| v == UNITY}
1066
1306
  den.delete_if {|v| v == UNITY}
1067
1307
  combined = Hash.new(0)
1068
-
1308
+
1069
1309
  i = 0
1070
- loop do
1310
+ loop do
1071
1311
  break if i > num.size
1072
1312
  if @@PREFIX_VALUES.has_key? num[i]
1073
1313
  k = [num[i],num[i+1]]
@@ -1078,7 +1318,7 @@ class Unit < Numeric
1078
1318
  end
1079
1319
  combined[k] += 1 unless k.nil? || k == UNITY
1080
1320
  end
1081
-
1321
+
1082
1322
  j = 0
1083
1323
  loop do
1084
1324
  break if j > den.size
@@ -1091,11 +1331,11 @@ class Unit < Numeric
1091
1331
  end
1092
1332
  combined[k] -= 1 unless k.nil? || k == UNITY
1093
1333
  end
1094
-
1334
+
1095
1335
  num = []
1096
1336
  den = []
1097
- for key, value in combined do
1098
- case
1337
+ for key, value in combined do
1338
+ case
1099
1339
  when value > 0
1100
1340
  value.times {num << key}
1101
1341
  when value < 0
@@ -1104,12 +1344,11 @@ class Unit < Numeric
1104
1344
  end
1105
1345
  num = UNITY_ARRAY if num.empty?
1106
1346
  den = UNITY_ARRAY if den.empty?
1107
- {:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact}
1347
+ return {:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact}
1108
1348
  end
1109
-
1110
-
1349
+
1111
1350
  # parse a string into a unit object.
1112
- # Typical formats like :
1351
+ # Typical formats like :
1113
1352
  # "5.6 kg*m/s^2"
1114
1353
  # "5.6 kg*m*s^-2"
1115
1354
  # "5.6 kilogram*meter*second^-2"
@@ -1118,7 +1357,9 @@ class Unit < Numeric
1118
1357
  # "1" -- creates a unitless constant with value 1
1119
1358
  # "GPa" -- creates a unit with scalar 1 with units 'GPa'
1120
1359
  # 6'4" -- recognized as 6 feet + 4 inches
1121
- # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
1360
+ # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
1361
+ # @return [nil | Unit]
1362
+ # @todo This should either be a separate class or at least a class method
1122
1363
  def parse(passed_unit_string="0")
1123
1364
  unit_string = passed_unit_string.dup
1124
1365
  if unit_string =~ /\$\s*(#{NUMBER_REGEX})/
@@ -1128,32 +1369,34 @@ class Unit < Numeric
1128
1369
  unit_string.gsub!(/'/,'feet')
1129
1370
  unit_string.gsub!(/"/,'inch')
1130
1371
  unit_string.gsub!(/#/,'pound')
1131
-
1372
+
1132
1373
  #:nocov:
1374
+ #:nocov_19:
1133
1375
  if defined?(Uncertain) && unit_string =~ /(\+\/-|&plusmn;)/
1134
1376
  value, uncertainty, unit_s = unit_string.scan(UNCERTAIN_REGEX)[0]
1135
1377
  result = unit_s.unit * Uncertain.new(value.to_f,uncertainty.to_f)
1136
1378
  copy(result)
1137
- return
1379
+ return
1138
1380
  end
1139
1381
  #:nocov:
1140
-
1382
+ #:nocov_19:
1383
+
1141
1384
  if defined?(Complex) && unit_string =~ COMPLEX_NUMBER
1142
1385
  real, imaginary, unit_s = unit_string.scan(COMPLEX_REGEX)[0]
1143
1386
  result = Unit(unit_s || '1') * Complex(real.to_f,imaginary.to_f)
1144
1387
  copy(result)
1145
- return
1388
+ return
1146
1389
  end
1147
-
1390
+
1148
1391
  if defined?(Rational) && unit_string =~ RATIONAL_NUMBER
1149
- numerator, denominator, unit_s = unit_string.scan(RATIONAL_REGEX)[0]
1150
- result = Unit(unit_s || '1') * Rational(numerator.to_i,denominator.to_i)
1151
- copy(result)
1152
- return
1392
+ numerator, denominator, unit_s = unit_string.scan(RATIONAL_REGEX)[0]
1393
+ result = Unit(unit_s || '1') * Rational(numerator.to_i,denominator.to_i)
1394
+ copy(result)
1395
+ return
1153
1396
  end
1154
-
1397
+
1155
1398
  unit_string =~ NUMBER_REGEX
1156
- unit = @@cached_units[$2]
1399
+ unit = @@cached_units[$2]
1157
1400
  mult = ($1.empty? ? 1.0 : $1.to_f) rescue 1.0
1158
1401
  mult = mult.to_int if (mult.to_int == mult)
1159
1402
  if unit
@@ -1164,61 +1407,59 @@ class Unit < Numeric
1164
1407
  end
1165
1408
  unit_string.gsub!(/<(#{@@UNIT_REGEX})><(#{@@UNIT_REGEX})>/, '\1*\2')
1166
1409
  unit_string.gsub!(/[<>]/,"")
1167
-
1410
+
1168
1411
  if unit_string =~ /:/
1169
1412
  hours, minutes, seconds, microseconds = unit_string.scan(TIME_REGEX)[0]
1170
1413
  raise ArgumentError, "Invalid Duration" if [hours, minutes, seconds, microseconds].all? {|x| x.nil?}
1171
- result = "#{hours || 0} h".unit +
1172
- "#{minutes || 0} minutes".unit +
1414
+ result = "#{hours || 0} h".unit +
1415
+ "#{minutes || 0} minutes".unit +
1173
1416
  "#{seconds || 0} seconds".unit +
1174
1417
  "#{microseconds || 0} usec".unit
1175
1418
  copy(result)
1176
1419
  return
1177
1420
  end
1178
-
1179
-
1421
+
1180
1422
  # Special processing for unusual unit strings
1181
1423
  # feet -- 6'5"
1182
1424
  feet, inches = unit_string.scan(FEET_INCH_REGEX)[0]
1183
1425
  if (feet && inches)
1184
1426
  result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches")
1185
1427
  copy(result)
1186
- return
1428
+ return
1187
1429
  end
1188
1430
 
1189
- # weight -- 8 lbs 12 oz
1431
+ # weight -- 8 lbs 12 oz
1190
1432
  pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0]
1191
1433
  if (pounds && oz)
1192
1434
  result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz")
1193
1435
  copy(result)
1194
- return
1436
+ return
1195
1437
  end
1196
-
1438
+
1197
1439
  raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1
1198
1440
  raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.scan(/\s[02-9]/).size > 0
1199
1441
  @scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] #parse the string into parts
1200
1442
  top.scan(TOP_REGEX).each do |item|
1201
1443
  n = item[1].to_i
1202
1444
  x = "#{item[0]} "
1203
- case
1445
+ case
1204
1446
  when n>=0
1205
1447
  top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) {|s| x * n}
1206
1448
  when n<0
1207
1449
  bottom = "#{bottom} #{x * -n}"; top.gsub!(/#{item[0]}(\^|\*\*)#{n}/,"")
1208
1450
  end
1209
- end
1451
+ end
1210
1452
  bottom.gsub!(BOTTOM_REGEX) {|s| "#{$1} " * $2.to_i} if bottom
1211
1453
  @scalar = @scalar.to_f unless @scalar.nil? || @scalar.empty?
1212
- @scalar = 1 unless @scalar.kind_of? Numeric
1454
+ @scalar = 1 unless @scalar.kind_of? Numeric
1213
1455
  @scalar = @scalar.to_int if (@scalar.to_int == @scalar)
1214
-
1456
+
1215
1457
  @numerator ||= UNITY_ARRAY
1216
1458
  @denominator ||= UNITY_ARRAY
1217
- @numerator = top.scan(@@UNIT_MATCH_REGEX).delete_if {|x| x.empty?}.compact if top
1218
- @denominator = bottom.scan(@@UNIT_MATCH_REGEX).delete_if {|x| x.empty?}.compact if bottom
1219
-
1220
-
1221
- us = "#{(top || '' + bottom || '')}".to_s.gsub(@@UNIT_MATCH_REGEX,'').gsub(/[\d\*, "'_^\/\$]/,'')
1459
+ @numerator = top.scan(Unit.unit_match_regex).delete_if {|x| x.empty?}.compact if top
1460
+ @denominator = bottom.scan(Unit.unit_match_regex).delete_if {|x| x.empty?}.compact if bottom
1461
+
1462
+ us = "#{(top || '' + bottom || '')}".to_s.gsub(Unit.unit_match_regex,'').gsub(/[\d\*, "'_^\/\$]/,'')
1222
1463
  raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless us.empty?
1223
1464
 
1224
1465
  @numerator = @numerator.map do |item|
@@ -1229,35 +1470,43 @@ class Unit < Numeric
1229
1470
  @@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]]
1230
1471
  end.flatten.compact.delete_if {|x| x.empty?}
1231
1472
 
1232
- @numerator = UNITY_ARRAY if @numerator.empty?
1473
+ @numerator = UNITY_ARRAY if @numerator.empty?
1233
1474
  @denominator = UNITY_ARRAY if @denominator.empty?
1234
- self
1235
- end
1475
+ return self
1476
+ end
1236
1477
 
1478
+ # return an array of base units
1479
+ # @return [Array]
1237
1480
  def self.base_units
1238
- @@BASE_UNITS.map {|u| Unit.new(u)}
1481
+ return @@base_units ||= @@definitions.dup.delete_if {|_, defn| !defn.base?}.keys.map {|u| Unit.new(u)}
1239
1482
  end
1240
-
1483
+
1241
1484
  private
1242
1485
 
1243
1486
  # parse a string consisting of a number and a unit string
1487
+ # @param [String] string
1488
+ # @return [Array] consisting of [Numeric, "unit"]
1489
+ # @private
1244
1490
  def self.parse_into_numbers_and_units(string)
1245
1491
  # scientific notation.... 123.234E22, -123.456e-10
1246
- sci = %r{[+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*}
1492
+ sci = %r{[+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*}
1247
1493
  # rational numbers.... -1/3, 1/5, 20/100
1248
- rational = %r{[+-]?\d+\/\d+}
1494
+ rational = %r{[+-]?\d+\/\d+}
1249
1495
  # complex numbers... -1.2+3i, +1.2-3.3i
1250
- complex = %r{#{sci}{2,2}i}
1496
+ complex = %r{#{sci}{2,2}i}
1251
1497
  anynumber = %r{(?:(#{complex}|#{rational}|#{sci})\b)?\s?([\D].*)?}
1252
1498
  num, unit = string.scan(anynumber).first
1253
- [case num
1499
+
1500
+ return [case num
1254
1501
  when NilClass
1255
1502
  1
1256
1503
  when complex
1257
1504
  if num.respond_to?(:to_c)
1258
1505
  num.to_c
1259
1506
  else
1507
+ #:nocov_19:
1260
1508
  Complex(*num.scan(/(#{sci})(#{sci})i/).flatten.map {|n| n.to_i})
1509
+ #:nocov_19:
1261
1510
  end
1262
1511
  when rational
1263
1512
  Rational(*num.split("/").map {|x| x.to_i})
@@ -1265,6 +1514,45 @@ class Unit < Numeric
1265
1514
  num.to_f
1266
1515
  end, unit.to_s.strip]
1267
1516
  end
1268
- end
1517
+
1518
+ # return a fragment of a regex to be used for matching units or reconstruct it if hasn't been used yet.
1519
+ # Unit names are reverse sorted by length so the regexp matcher will prefer longer and more specific names
1520
+ # @return [String]
1521
+ # @private
1522
+ def self.unit_regex
1523
+ @@UNIT_REGEX ||= @@UNIT_MAP.keys.sort_by {|unit_name| [unit_name.length, unit_name]}.reverse.join('|')
1524
+ end
1525
+
1526
+ # return a regex used to match units
1527
+ # @return [RegExp]
1528
+ # @private
1529
+ def self.unit_match_regex
1530
+ @@UNIT_MATCH_REGEX ||= /(#{Unit.prefix_regex})*?(#{Unit.unit_regex})\b/
1531
+ end
1269
1532
 
1270
- Unit.setup
1533
+ # return a regexp fragment used to match prefixes
1534
+ # @return [String]
1535
+ # @private
1536
+ def self.prefix_regex
1537
+ return @@PREFIX_REGEX ||= @@PREFIX_MAP.keys.sort_by {|prefix| [prefix.length, prefix]}.reverse.join('|')
1538
+ end
1539
+
1540
+ # inject a definition into the internal array and set it up for use
1541
+ # @private
1542
+ def self.use_definition(definition)
1543
+ @@UNIT_MATCH_REGEX = nil #invalidate the unit match regex
1544
+ if definition.prefix?
1545
+ @@PREFIX_VALUES[definition.name] = definition.scalar
1546
+ definition.aliases.each {|_alias| @@PREFIX_MAP[_alias] = definition.name }
1547
+ @@PREFIX_REGEX = nil #invalidate the prefix regex
1548
+ else
1549
+ @@UNIT_VALUES[definition.name] = {}
1550
+ @@UNIT_VALUES[definition.name][:scalar] = definition.scalar
1551
+ @@UNIT_VALUES[definition.name][:numerator] = definition.numerator if definition.numerator
1552
+ @@UNIT_VALUES[definition.name][:denominator] = definition.denominator if definition.denominator
1553
+ definition.aliases.each {|_alias| @@UNIT_MAP[_alias] = definition.name}
1554
+ @@UNIT_REGEX = nil #invalidate the unit regex
1555
+ end
1556
+ end
1557
+
1558
+ end