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.
- data/CHANGELOG.txt +25 -7
- data/LICENSE.txt +1 -1
- data/README.md +68 -55
- data/RakeFile +27 -18
- data/TODO +2 -1
- data/VERSION +1 -1
- data/lib/ruby-units.rb +2 -2
- data/lib/ruby_units.rb +2 -2
- data/lib/ruby_units/array.rb +4 -2
- data/lib/ruby_units/date.rb +17 -4
- data/lib/ruby_units/definition.rb +100 -0
- data/lib/ruby_units/fixnum.rb +6 -4
- data/lib/ruby_units/math.rb +32 -2
- data/lib/ruby_units/numeric.rb +2 -1
- data/lib/ruby_units/object.rb +8 -1
- data/lib/ruby_units/string.rb +10 -109
- data/lib/ruby_units/string/extra.rb +45 -11
- data/lib/ruby_units/time.rb +11 -2
- data/lib/ruby_units/unit.rb +722 -434
- data/lib/ruby_units/unit_definitions.rb +3 -252
- data/lib/ruby_units/unit_definitions/base.rb +103 -0
- data/lib/ruby_units/unit_definitions/prefix.rb +40 -0
- data/lib/ruby_units/unit_definitions/standard.rb +705 -0
- data/lib/ruby_units/version.rb +1 -0
- data/ruby-units.gemspec +15 -20
- metadata +46 -35
- data/Gemfile +0 -12
- data/Manifest.txt +0 -19
- data/autotest/discover.rb +0 -1
- data/spec/ruby-units/array_spec.rb +0 -14
- data/spec/ruby-units/complex_spec.rb +0 -37
- data/spec/ruby-units/date_spec.rb +0 -38
- data/spec/ruby-units/math_spec.rb +0 -63
- data/spec/ruby-units/numeric_spec.rb +0 -12
- data/spec/ruby-units/object_spec.rb +0 -7
- data/spec/ruby-units/string/extra_spec.rb +0 -45
- data/spec/ruby-units/string_spec.rb +0 -20
- data/spec/ruby-units/time_spec.rb +0 -28
- data/spec/ruby-units/unit_spec.rb +0 -965
- data/spec/spec_helper.rb +0 -5
- data/test/test_cache.rb +0 -26
- data/test/test_ruby-units.rb +0 -976
data/lib/ruby_units/time.rb
CHANGED
@@ -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
|
-
#
|
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
|
data/lib/ruby_units/unit.rb
CHANGED
@@ -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
|
-
#
|
8
|
+
# Copyright 2006-2012
|
7
9
|
#
|
8
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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,
|
21
|
+
# in the United States. If your favorite units are not listed here, file an issue on github.
|
32
22
|
#
|
33
|
-
# To add
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
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
|
-
#
|
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
|
-
|
43
|
-
|
44
|
-
@@
|
45
|
-
@@
|
46
|
-
@@
|
47
|
-
@@
|
48
|
-
@@
|
49
|
-
@@
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
75
|
-
-312058
|
76
|
-
-312038
|
77
|
-
-152040
|
78
|
-
-152038
|
79
|
-
-152058
|
80
|
-
-7997
|
81
|
-
-79
|
82
|
-
-59
|
83
|
-
-39
|
84
|
-
-38
|
85
|
-
-20
|
86
|
-
-19
|
87
|
-
-18
|
88
|
-
-17
|
89
|
-
-1
|
90
|
-
0
|
91
|
-
1
|
92
|
-
2
|
93
|
-
3
|
94
|
-
20
|
95
|
-
400
|
96
|
-
7941
|
97
|
-
7942
|
98
|
-
7959
|
99
|
-
7962
|
100
|
-
7979
|
101
|
-
7961
|
102
|
-
7981
|
103
|
-
7982
|
104
|
-
7997
|
105
|
-
7998
|
106
|
-
8000
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
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
|
-
|
156
|
-
|
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
|
167
|
-
@numerator
|
256
|
+
@scalar = from.scalar
|
257
|
+
@numerator = from.numerator
|
168
258
|
@denominator = from.denominator
|
169
|
-
@is_base
|
170
|
-
@signature
|
259
|
+
@is_base = from.is_base?
|
260
|
+
@signature = from.signature
|
171
261
|
@base_scalar = from.base_scalar
|
172
|
-
@unit_name
|
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
|
-
#
|
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
|
312
|
+
@scalar = nil
|
203
313
|
@base_scalar = nil
|
204
|
-
@unit_name
|
205
|
-
@signature
|
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
|
233
|
-
@numerator
|
342
|
+
@scalar = options[0][:scalar] || 1
|
343
|
+
@numerator = options[0][:numerator] || UNITY_ARRAY
|
234
344
|
@denominator = options[0][:denominator] || UNITY_ARRAY
|
235
|
-
@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
|
-
|
269
|
-
|
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
|
-
#
|
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
|
-
#
|
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? &&
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
-
#
|
379
|
-
# :
|
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
|
-
|
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
|
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
|
-
|
492
|
-
|
493
|
-
|
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
|
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
|
-
|
509
|
-
|
510
|
-
|
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
|
-
#
|
519
|
-
#
|
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
|
-
|
526
|
-
|
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
|
-
#
|
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
|
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
|
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
|
732
|
-
# unit1
|
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,
|
934
|
+
# Supports Kelvin, Celsius, Fahrenheit, and Rankine scales.
|
741
935
|
#
|
742
|
-
#
|
743
|
-
#
|
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
|
-
|
831
|
-
|
832
|
-
num
|
833
|
-
den
|
1036
|
+
output_numerator = []
|
1037
|
+
output_denominator = []
|
1038
|
+
num = @numerator.clone.compact
|
1039
|
+
den = @denominator.clone.compact
|
1040
|
+
|
834
1041
|
if @numerator == UNITY_ARRAY
|
835
|
-
|
1042
|
+
output_numerator << "1"
|
836
1043
|
else
|
837
|
-
num.
|
838
|
-
if
|
839
|
-
|
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
|
-
|
1048
|
+
output_numerator << defn.display_name
|
843
1049
|
end
|
844
1050
|
end
|
845
1051
|
end
|
1052
|
+
|
846
1053
|
if @denominator == UNITY_ARRAY
|
847
|
-
|
1054
|
+
output_denominator = []
|
848
1055
|
else
|
849
|
-
den.
|
850
|
-
if
|
851
|
-
|
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
|
-
|
1060
|
+
output_denominator << defn.display_name
|
855
1061
|
end
|
856
1062
|
end
|
857
1063
|
end
|
858
|
-
|
859
|
-
|
860
|
-
|
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
|
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
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
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
|
-
|
1030
|
-
|
1031
|
-
|
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 =~ /(\+\/-|±)/
|
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
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
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(
|
1218
|
-
@denominator = bottom.scan(
|
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
|
-
@@
|
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
|
1492
|
+
sci = %r{[+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*}
|
1247
1493
|
# rational numbers.... -1/3, 1/5, 20/100
|
1248
|
-
rational
|
1494
|
+
rational = %r{[+-]?\d+\/\d+}
|
1249
1495
|
# complex numbers... -1.2+3i, +1.2-3.3i
|
1250
|
-
complex
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|