ruby-units 2.4.1 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/codeql-analysis.yml +3 -3
- data/.github/workflows/tests.yml +3 -1
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.solargraph.yml +1 -1
- data/.tool-versions +2 -0
- data/CHANGELOG.txt +3 -1
- data/Gemfile +16 -1
- data/Gemfile.lock +91 -66
- data/README.md +2 -0
- data/lib/ruby_units/array.rb +17 -7
- data/lib/ruby_units/cache.rb +27 -14
- data/lib/ruby_units/configuration.rb +8 -8
- data/lib/ruby_units/date.rb +46 -53
- data/lib/ruby_units/math.rb +136 -113
- data/lib/ruby_units/numeric.rb +21 -5
- data/lib/ruby_units/string.rb +34 -21
- data/lib/ruby_units/time.rb +76 -69
- data/lib/ruby_units/unit.rb +442 -340
- data/lib/ruby_units/version.rb +1 -1
- data/ruby-units.gemspec +2 -14
- metadata +8 -175
data/lib/ruby_units/unit.rb
CHANGED
@@ -1,77 +1,106 @@
|
|
1
1
|
require 'date'
|
2
|
-
# Copyright 2006-2015
|
3
|
-
# @author Kevin C. Olbrich, Ph.D.
|
4
|
-
# @see https://github.com/olbrich/ruby-units
|
5
|
-
#
|
6
|
-
# @note The accuracy of unit conversions depends on the precision of the conversion factor.
|
7
|
-
# If you have more accurate estimates for particular conversion factors, please send them
|
8
|
-
# to me and I will incorporate them into the next release. It is also incumbent on the end-user
|
9
|
-
# to ensure that the accuracy of any conversions is sufficient for their intended application.
|
10
|
-
#
|
11
|
-
# While there are a large number of unit specified in the base package,
|
12
|
-
# there are also a large number of units that are not included.
|
13
|
-
# This package covers nearly all SI, Imperial, and units commonly used
|
14
|
-
# in the United States. If your favorite units are not listed here, file an issue on github.
|
15
|
-
#
|
16
|
-
# To add or override a unit definition, add a code block like this..
|
17
|
-
# @example Define a new unit
|
18
|
-
# RubyUnits::Unit.define("foobar") do |unit|
|
19
|
-
# unit.aliases = %w{foo fb foo-bar}
|
20
|
-
# unit.definition = RubyUnits::Unit.new("1 baz")
|
21
|
-
# end
|
22
|
-
#
|
23
2
|
module RubyUnits
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
3
|
+
# Copyright 2006-2023
|
4
|
+
# @author Kevin C. Olbrich, Ph.D.
|
5
|
+
# @see https://github.com/olbrich/ruby-units
|
6
|
+
#
|
7
|
+
# @note The accuracy of unit conversions depends on the precision of the conversion factor.
|
8
|
+
# If you have more accurate estimates for particular conversion factors, please send them
|
9
|
+
# to me and I will incorporate them into the next release. It is also incumbent on the end-user
|
10
|
+
# to ensure that the accuracy of any conversions is sufficient for their intended application.
|
11
|
+
#
|
12
|
+
# While there are a large number of unit specified in the base package,
|
13
|
+
# there are also a large number of units that are not included.
|
14
|
+
# This package covers nearly all SI, Imperial, and units commonly used
|
15
|
+
# in the United States. If your favorite units are not listed here, file an issue on GitHub.
|
16
|
+
#
|
17
|
+
# To add or override a unit definition, add a code block like this..
|
18
|
+
# @example Define a new unit
|
19
|
+
# RubyUnits::Unit.define("foobar") do |unit|
|
20
|
+
# unit.aliases = %w{foo fb foo-bar}
|
21
|
+
# unit.definition = RubyUnits::Unit.new("1 baz")
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
class Unit < ::Numeric
|
25
|
+
class << self
|
26
|
+
# return a list of all defined units
|
27
|
+
# @return [Hash{Symbol=>RubyUnits::Units::Definition}]
|
28
|
+
attr_accessor :definitions
|
29
|
+
|
30
|
+
# @return [Hash{Symbol => String}] the list of units and their prefixes
|
31
|
+
attr_accessor :prefix_values
|
32
|
+
|
33
|
+
# @return [Hash{Symbol => String}]
|
34
|
+
attr_accessor :prefix_map
|
35
|
+
|
36
|
+
# @return [Hash{Symbol => String}]
|
37
|
+
attr_accessor :unit_map
|
38
|
+
|
39
|
+
# @return [Hash{Symbol => String}]
|
40
|
+
attr_accessor :unit_values
|
41
|
+
|
42
|
+
# @return [Hash{Integer => Symbol}]
|
43
|
+
attr_reader :kinds
|
44
|
+
end
|
45
|
+
self.definitions = {}
|
46
|
+
self.prefix_values = {}
|
47
|
+
self.prefix_map = {}
|
48
|
+
self.unit_map = {}
|
49
|
+
self.unit_values = {}
|
50
|
+
@unit_regex = nil
|
51
|
+
@unit_match_regex = nil
|
32
52
|
UNITY = '<1>'.freeze
|
33
53
|
UNITY_ARRAY = [UNITY].freeze
|
54
|
+
|
55
|
+
SIGN_REGEX = /(?:[+-])?/.freeze # +, -, or nothing
|
56
|
+
|
57
|
+
# regex for matching an integer number but not a fraction
|
58
|
+
INTEGER_DIGITS_REGEX = %r{(?<!/)\d+(?!/)}.freeze # 1, 2, 3, but not 1/2 or -1
|
59
|
+
INTEGER_REGEX = /(#{SIGN_REGEX}#{INTEGER_DIGITS_REGEX})/.freeze # -1, 1, +1, but not 1/2
|
60
|
+
UNSIGNED_INTEGER_REGEX = /((?<!-)#{INTEGER_DIGITS_REGEX})/.freeze # 1, 2, 3, but not -1
|
61
|
+
DIGITS_REGEX = /\d+/.freeze # 0, 1, 2, 3
|
62
|
+
DECIMAL_REGEX = /\d*[.]?#{DIGITS_REGEX}/.freeze # 1, 0.1, .1
|
63
|
+
# Rational number, including improper fractions: 1 2/3, -1 2/3, 5/3, etc.
|
64
|
+
RATIONAL_NUMBER = %r{\(?(?:(?<proper>#{SIGN_REGEX}#{DECIMAL_REGEX})[ -])?(?<numerator>#{SIGN_REGEX}#{DECIMAL_REGEX})/(?<denominator>#{SIGN_REGEX}#{DECIMAL_REGEX})\)?} # 1 2/3, -1 2/3, 5/3, 1-2/3, (1/2) etc.
|
65
|
+
# Scientific notation: 1, -1, +1, 1.2, +1.2, -1.2, 123.4E5, +123.4e5,
|
66
|
+
# -123.4E+5, -123.4e-5, etc.
|
67
|
+
SCI_NUMBER = /([+-]?\d*[.]?\d+(?:[Ee][+-]?\d+(?![.]))?)/
|
34
68
|
# ideally we would like to generate this regex from the alias for a 'feet'
|
35
69
|
# and 'inches', but they aren't defined at the point in the code where we
|
36
70
|
# need this regex.
|
37
|
-
FEET_INCH_UNITS_REGEX = /(?:'|ft|feet)\s*(
|
38
|
-
FEET_INCH_REGEX = /(
|
71
|
+
FEET_INCH_UNITS_REGEX = /(?:'|ft|feet)\s*(?<inches>#{RATIONAL_NUMBER}|#{SCI_NUMBER})\s*(?:"|in|inch(?:es)?)/.freeze
|
72
|
+
FEET_INCH_REGEX = /(?<feet>#{INTEGER_REGEX})\s*#{FEET_INCH_UNITS_REGEX}/.freeze
|
39
73
|
# ideally we would like to generate this regex from the alias for a 'pound'
|
40
74
|
# and 'ounce', but they aren't defined at the point in the code where we
|
41
75
|
# need this regex.
|
42
|
-
LBS_OZ_UNIT_REGEX = /(?:#|lbs?|pounds?|pound-mass)+[\s,]*(
|
43
|
-
LBS_OZ_REGEX = /(
|
76
|
+
LBS_OZ_UNIT_REGEX = /(?:#|lbs?|pounds?|pound-mass)+[\s,]*(?<oz>#{RATIONAL_NUMBER}|#{UNSIGNED_INTEGER_REGEX})\s*(?:ozs?|ounces?)/.freeze
|
77
|
+
LBS_OZ_REGEX = /(?<pounds>#{INTEGER_REGEX})\s*#{LBS_OZ_UNIT_REGEX}/.freeze
|
44
78
|
# ideally we would like to generate this regex from the alias for a 'stone'
|
45
79
|
# and 'pound', but they aren't defined at the point in the code where we
|
46
80
|
# need this regex. also note that the plural of 'stone' is still 'stone',
|
47
81
|
# but we accept 'stones' anyway.
|
48
|
-
STONE_LB_UNIT_REGEX = /(?:sts?|stones?)+[\s,]*(
|
49
|
-
STONE_LB_REGEX = /(
|
82
|
+
STONE_LB_UNIT_REGEX = /(?:sts?|stones?)+[\s,]*(?<pounds>#{RATIONAL_NUMBER}|#{UNSIGNED_INTEGER_REGEX})\s*(?:#|lbs?|pounds?|pound-mass)*/.freeze
|
83
|
+
STONE_LB_REGEX = /(?<stone>#{INTEGER_REGEX})\s*#{STONE_LB_UNIT_REGEX}/.freeze
|
50
84
|
# Time formats: 12:34:56,78, (hh:mm:ss,msec) etc.
|
51
|
-
TIME_REGEX = /(?<hour>\d+):(?<min>\d+)
|
52
|
-
# Scientific notation: 1, -1, +1, 1.2, +1.2, -1.2, 123.4E5, +123.4e5,
|
53
|
-
# -123.4E+5, -123.4e-5, etc.
|
54
|
-
SCI_NUMBER = /([+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*)/.freeze
|
55
|
-
# Rational number, including improper fractions: 1 2/3, -1 2/3, 5/3, etc.
|
56
|
-
RATIONAL_NUMBER = %r{\(?([+-])?(\d+[ -])?(\d+)\/(\d+)\)?}.freeze
|
85
|
+
TIME_REGEX = /(?<hour>\d+):(?<min>\d+):?(?:(?<sec>\d+))?(?:[.](?<msec>\d+))?/.freeze
|
57
86
|
# Complex numbers: 1+2i, 1.0+2.0i, -1-1i, etc.
|
58
|
-
COMPLEX_NUMBER =
|
87
|
+
COMPLEX_NUMBER = /(?<real>#{SCI_NUMBER})?(?<imaginary>#{SCI_NUMBER})i\b/.freeze
|
59
88
|
# Any Complex, Rational, or scientific number
|
60
89
|
ANY_NUMBER = /(#{COMPLEX_NUMBER}|#{RATIONAL_NUMBER}|#{SCI_NUMBER})/.freeze
|
61
90
|
ANY_NUMBER_REGEX = /(?:#{ANY_NUMBER})?\s?([^-\d.].*)?/.freeze
|
62
|
-
NUMBER_REGEX =
|
63
|
-
UNIT_STRING_REGEX = %r{#{SCI_NUMBER}*\s*([
|
64
|
-
TOP_REGEX = /([^
|
91
|
+
NUMBER_REGEX = /(?<scalar>#{SCI_NUMBER}*)\s*(?<unit>.+)?/.freeze # a number followed by a unit
|
92
|
+
UNIT_STRING_REGEX = %r{#{SCI_NUMBER}*\s*([^/]*)/*(.+)*}.freeze
|
93
|
+
TOP_REGEX = /([^ *]+)(?:\^|\*\*)([\d-]+)/.freeze
|
65
94
|
BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/.freeze
|
66
95
|
NUMBER_UNIT_REGEX = /#{SCI_NUMBER}?(.*)/.freeze
|
67
|
-
COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(
|
68
|
-
RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(
|
96
|
+
COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(?<unit>.+)?/.freeze
|
97
|
+
RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(?<unit>.+)?/.freeze
|
69
98
|
KELVIN = ['<kelvin>'].freeze
|
70
99
|
FAHRENHEIT = ['<fahrenheit>'].freeze
|
71
100
|
RANKINE = ['<rankine>'].freeze
|
72
101
|
CELSIUS = ['<celsius>'].freeze
|
73
|
-
|
74
|
-
SIGNATURE_VECTOR
|
102
|
+
@temp_regex = nil
|
103
|
+
SIGNATURE_VECTOR = %i[
|
75
104
|
length
|
76
105
|
time
|
77
106
|
temperature
|
@@ -83,74 +112,72 @@ module RubyUnits
|
|
83
112
|
information
|
84
113
|
angle
|
85
114
|
].freeze
|
86
|
-
|
87
|
-
-312_078
|
88
|
-
-312_058
|
89
|
-
-312_038
|
90
|
-
-152_040
|
91
|
-
-152_038
|
92
|
-
-152_058
|
93
|
-
-7997
|
94
|
-
-79
|
95
|
-
-59
|
96
|
-
-39
|
97
|
-
-38
|
98
|
-
-20
|
99
|
-
-19
|
100
|
-
-18
|
101
|
-
-17
|
102
|
-
-1
|
103
|
-
0
|
104
|
-
1
|
105
|
-
2
|
106
|
-
3
|
107
|
-
20
|
108
|
-
400
|
109
|
-
7941
|
110
|
-
7942
|
111
|
-
7959
|
112
|
-
7962
|
113
|
-
7979
|
114
|
-
7961
|
115
|
-
7981
|
116
|
-
7982
|
117
|
-
7997
|
118
|
-
7998
|
119
|
-
8000
|
120
|
-
152_020
|
121
|
-
159_999
|
122
|
-
160_000
|
123
|
-
160_020
|
124
|
-
312_058
|
125
|
-
312_078
|
126
|
-
3_199_980
|
127
|
-
3_199_997
|
128
|
-
3_200_000
|
129
|
-
63_999_998
|
130
|
-
64_000_000
|
115
|
+
@kinds = {
|
116
|
+
-312_078 => :elastance,
|
117
|
+
-312_058 => :resistance,
|
118
|
+
-312_038 => :inductance,
|
119
|
+
-152_040 => :magnetism,
|
120
|
+
-152_038 => :magnetism,
|
121
|
+
-152_058 => :potential,
|
122
|
+
-7997 => :specific_volume,
|
123
|
+
-79 => :snap,
|
124
|
+
-59 => :jolt,
|
125
|
+
-39 => :acceleration,
|
126
|
+
-38 => :radiation,
|
127
|
+
-20 => :frequency,
|
128
|
+
-19 => :speed,
|
129
|
+
-18 => :viscosity,
|
130
|
+
-17 => :volumetric_flow,
|
131
|
+
-1 => :wavenumber,
|
132
|
+
0 => :unitless,
|
133
|
+
1 => :length,
|
134
|
+
2 => :area,
|
135
|
+
3 => :volume,
|
136
|
+
20 => :time,
|
137
|
+
400 => :temperature,
|
138
|
+
7941 => :yank,
|
139
|
+
7942 => :power,
|
140
|
+
7959 => :pressure,
|
141
|
+
7962 => :energy,
|
142
|
+
7979 => :viscosity,
|
143
|
+
7961 => :force,
|
144
|
+
7981 => :momentum,
|
145
|
+
7982 => :angular_momentum,
|
146
|
+
7997 => :density,
|
147
|
+
7998 => :area_density,
|
148
|
+
8000 => :mass,
|
149
|
+
152_020 => :radiation_exposure,
|
150
|
+
159_999 => :magnetism,
|
151
|
+
160_000 => :current,
|
152
|
+
160_020 => :charge,
|
153
|
+
312_058 => :conductance,
|
154
|
+
312_078 => :capacitance,
|
155
|
+
3_199_980 => :activity,
|
156
|
+
3_199_997 => :molar_concentration,
|
157
|
+
3_200_000 => :substance,
|
158
|
+
63_999_998 => :illuminance,
|
159
|
+
64_000_000 => :luminous_power,
|
131
160
|
1_280_000_000 => :currency,
|
132
|
-
25_600_000_000
|
161
|
+
25_600_000_000 => :information,
|
133
162
|
511_999_999_980 => :angular_velocity,
|
134
163
|
512_000_000_000 => :angle
|
135
164
|
}.freeze
|
136
|
-
@@cached_units = {}
|
137
|
-
@@base_unit_cache = {}
|
138
165
|
|
139
166
|
# Class Methods
|
140
167
|
|
141
168
|
# setup internal arrays and hashes
|
142
|
-
# @return [
|
169
|
+
# @return [Boolean]
|
143
170
|
def self.setup
|
144
171
|
clear_cache
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
172
|
+
self.prefix_values = {}
|
173
|
+
self.prefix_map = {}
|
174
|
+
self.unit_map = {}
|
175
|
+
self.unit_values = {}
|
176
|
+
@unit_regex = nil
|
177
|
+
@unit_match_regex = nil
|
178
|
+
@prefix_regex = nil
|
179
|
+
|
180
|
+
definitions.each_value do |definition|
|
154
181
|
use_definition(definition)
|
155
182
|
end
|
156
183
|
|
@@ -162,7 +189,7 @@ module RubyUnits
|
|
162
189
|
# @param [String] unit
|
163
190
|
# @return [Boolean]
|
164
191
|
def self.defined?(unit)
|
165
|
-
definitions.values.any? {
|
192
|
+
definitions.values.any? { _1.aliases.include?(unit) }
|
166
193
|
end
|
167
194
|
|
168
195
|
# return the unit definition for a unit
|
@@ -170,17 +197,11 @@ module RubyUnits
|
|
170
197
|
# @return [RubyUnits::Unit::Definition, nil]
|
171
198
|
def self.definition(unit_name)
|
172
199
|
unit = unit_name =~ /^<.+>$/ ? unit_name : "<#{unit_name}>"
|
173
|
-
|
174
|
-
end
|
175
|
-
|
176
|
-
# return a list of all defined units
|
177
|
-
# @return [Array<RubyUnits::Units::Definition>]
|
178
|
-
def self.definitions
|
179
|
-
@@definitions
|
200
|
+
definitions[unit]
|
180
201
|
end
|
181
202
|
|
182
203
|
# @param [RubyUnits::Unit::Definition, String] unit_definition
|
183
|
-
# @param [
|
204
|
+
# @param [Proc] block
|
184
205
|
# @return [RubyUnits::Unit::Definition]
|
185
206
|
# @raise [ArgumentError] when passed a non-string if using the block form
|
186
207
|
# Unpack a unit definition and add it to the array of defined units
|
@@ -195,7 +216,8 @@ module RubyUnits
|
|
195
216
|
# RubyUnits::Unit.define(unit_definition)
|
196
217
|
def self.define(unit_definition, &block)
|
197
218
|
if block_given?
|
198
|
-
raise ArgumentError, 'When using the block form of RubyUnits::Unit.define, pass the name of the unit' unless unit_definition.
|
219
|
+
raise ArgumentError, 'When using the block form of RubyUnits::Unit.define, pass the name of the unit' unless unit_definition.is_a?(String)
|
220
|
+
|
199
221
|
unit_definition = RubyUnits::Unit::Definition.new(unit_definition, &block)
|
200
222
|
end
|
201
223
|
definitions[unit_definition.name] = unit_definition
|
@@ -206,7 +228,7 @@ module RubyUnits
|
|
206
228
|
# Get the definition for a unit and allow it to be redefined
|
207
229
|
#
|
208
230
|
# @param [String] name Name of unit to redefine
|
209
|
-
# @param [
|
231
|
+
# @param [Proc] _block
|
210
232
|
# @raise [ArgumentError] if a block is not given
|
211
233
|
# @yieldparam [RubyUnits::Unit::Definition] the definition of the unit being
|
212
234
|
# redefined
|
@@ -218,7 +240,7 @@ module RubyUnits
|
|
218
240
|
raise(ArgumentError, "'#{name}' Unit not recognized") unless unit_definition
|
219
241
|
|
220
242
|
yield unit_definition
|
221
|
-
|
243
|
+
definitions.delete("<#{name}>")
|
222
244
|
define(unit_definition)
|
223
245
|
setup
|
224
246
|
end
|
@@ -228,26 +250,28 @@ module RubyUnits
|
|
228
250
|
# @param unit [String] name of unit to undefine
|
229
251
|
# @return (see RubyUnits::Unit.setup)
|
230
252
|
def self.undefine!(unit)
|
231
|
-
|
253
|
+
definitions.delete("<#{unit}>")
|
232
254
|
setup
|
233
255
|
end
|
234
256
|
|
235
|
-
#
|
257
|
+
# Unit cache
|
258
|
+
#
|
259
|
+
# @return [RubyUnits::Cache]
|
236
260
|
def self.cached
|
237
|
-
|
261
|
+
@cached ||= RubyUnits::Cache.new
|
238
262
|
end
|
239
263
|
|
240
|
-
# @return [
|
264
|
+
# @return [Boolean]
|
241
265
|
def self.clear_cache
|
242
|
-
|
243
|
-
|
266
|
+
cached.clear
|
267
|
+
base_unit_cache.clear
|
244
268
|
new(1)
|
245
269
|
true
|
246
270
|
end
|
247
271
|
|
248
|
-
# @return [
|
272
|
+
# @return [RubyUnits::Cache]
|
249
273
|
def self.base_unit_cache
|
250
|
-
|
274
|
+
@base_unit_cache ||= RubyUnits::Cache.new
|
251
275
|
end
|
252
276
|
|
253
277
|
# @example parse strings
|
@@ -269,12 +293,12 @@ module RubyUnits
|
|
269
293
|
num.delete(UNITY)
|
270
294
|
den.delete(UNITY)
|
271
295
|
|
272
|
-
combined = Hash.new(0)
|
296
|
+
combined = ::Hash.new(0)
|
273
297
|
|
274
298
|
[[num, 1], [den, -1]].each do |array, increment|
|
275
299
|
array.chunk_while { |elt_before, _| definition(elt_before).prefix? }
|
276
300
|
.to_a
|
277
|
-
.each {
|
301
|
+
.each { combined[_1] += increment }
|
278
302
|
end
|
279
303
|
|
280
304
|
num = []
|
@@ -301,11 +325,12 @@ module RubyUnits
|
|
301
325
|
# return an array of base units
|
302
326
|
# @return [Array]
|
303
327
|
def self.base_units
|
304
|
-
|
328
|
+
@base_units ||= definitions.dup.select { |_, definition| definition.base? }.keys.map { new(_1) }
|
305
329
|
end
|
306
330
|
|
307
|
-
#
|
331
|
+
# Parse a string consisting of a number and a unit string
|
308
332
|
# NOTE: This does not properly handle units formatted like '12mg/6ml'
|
333
|
+
#
|
309
334
|
# @param [String] string
|
310
335
|
# @return [Array(Numeric, String)] consisting of [number, "unit"]
|
311
336
|
def self.parse_into_numbers_and_units(string)
|
@@ -335,27 +360,27 @@ module RubyUnits
|
|
335
360
|
# Unit names are reverse sorted by length so the regexp matcher will prefer longer and more specific names
|
336
361
|
# @return [String]
|
337
362
|
def self.unit_regex
|
338
|
-
|
363
|
+
@unit_regex ||= unit_map.keys.sort_by { [_1.length, _1] }.reverse.join('|')
|
339
364
|
end
|
340
365
|
|
341
366
|
# return a regex used to match units
|
342
|
-
# @return [
|
367
|
+
# @return [Regexp]
|
343
368
|
def self.unit_match_regex
|
344
|
-
|
369
|
+
@unit_match_regex ||= /(#{prefix_regex})??(#{unit_regex})\b/
|
345
370
|
end
|
346
371
|
|
347
372
|
# return a regexp fragment used to match prefixes
|
348
373
|
# @return [String]
|
349
374
|
# @private
|
350
375
|
def self.prefix_regex
|
351
|
-
|
376
|
+
@prefix_regex ||= prefix_map.keys.sort_by { [_1.length, _1] }.reverse.join('|')
|
352
377
|
end
|
353
378
|
|
354
379
|
# Generates (and memoizes) a regexp matching any of the temperature units or their aliases.
|
355
380
|
#
|
356
|
-
# @return [
|
381
|
+
# @return [Regexp]
|
357
382
|
def self.temp_regex
|
358
|
-
|
383
|
+
@temp_regex ||= begin
|
359
384
|
temp_units = %w[tempK tempC tempF tempR degK degC degF degR]
|
360
385
|
aliases = temp_units.map do |unit|
|
361
386
|
d = definition(unit)
|
@@ -370,19 +395,19 @@ module RubyUnits
|
|
370
395
|
#
|
371
396
|
# @param definition [RubyUnits::Unit::Definition]
|
372
397
|
def self.use_definition(definition)
|
373
|
-
|
374
|
-
|
398
|
+
@unit_match_regex = nil # invalidate the unit match regex
|
399
|
+
@temp_regex = nil # invalidate the temp regex
|
375
400
|
if definition.prefix?
|
376
|
-
|
377
|
-
definition.aliases.each {
|
378
|
-
|
401
|
+
prefix_values[definition.name] = definition.scalar
|
402
|
+
definition.aliases.each { prefix_map[_1] = definition.name }
|
403
|
+
@prefix_regex = nil # invalidate the prefix regex
|
379
404
|
else
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
definition.aliases.each {
|
385
|
-
|
405
|
+
unit_values[definition.name] = {}
|
406
|
+
unit_values[definition.name][:scalar] = definition.scalar
|
407
|
+
unit_values[definition.name][:numerator] = definition.numerator if definition.numerator
|
408
|
+
unit_values[definition.name][:denominator] = definition.denominator if definition.denominator
|
409
|
+
definition.aliases.each { unit_map[_1] = definition.name }
|
410
|
+
@unit_regex = nil # invalidate the unit regex
|
386
411
|
end
|
387
412
|
end
|
388
413
|
|
@@ -416,20 +441,16 @@ module RubyUnits
|
|
416
441
|
attr_accessor :unit_name
|
417
442
|
|
418
443
|
# Used to copy one unit to another
|
419
|
-
# @param [Unit]
|
420
|
-
# @return [Unit]
|
444
|
+
# @param from [RubyUnits::Unit] Unit to copy definition from
|
445
|
+
# @return [RubyUnits::Unit]
|
421
446
|
def copy(from)
|
422
|
-
@scalar
|
423
|
-
@numerator
|
447
|
+
@scalar = from.scalar
|
448
|
+
@numerator = from.numerator
|
424
449
|
@denominator = from.denominator
|
425
450
|
@base = from.base?
|
426
|
-
@signature
|
451
|
+
@signature = from.signature
|
427
452
|
@base_scalar = from.base_scalar
|
428
|
-
@unit_name =
|
429
|
-
from.unit_name
|
430
|
-
rescue
|
431
|
-
nil
|
432
|
-
end
|
453
|
+
@unit_name = from.unit_name
|
433
454
|
self
|
434
455
|
end
|
435
456
|
|
@@ -464,26 +485,22 @@ module RubyUnits
|
|
464
485
|
if options.size == 2
|
465
486
|
# options[0] is the scalar
|
466
487
|
# options[1] is a unit string
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
options[1].units
|
473
|
-
rescue
|
474
|
-
options[1]
|
475
|
-
end)}")
|
488
|
+
cached = self.class.cached.get(options[1])
|
489
|
+
if cached.nil?
|
490
|
+
initialize("#{options[0]} #{options[1]}")
|
491
|
+
else
|
492
|
+
copy(cached * options[0])
|
476
493
|
end
|
477
494
|
return
|
478
495
|
end
|
479
496
|
if options.size == 3
|
480
497
|
options[1] = options[1].join if options[1].is_a?(Array)
|
481
498
|
options[2] = options[2].join if options[2].is_a?(Array)
|
482
|
-
|
483
|
-
|
484
|
-
copy(cached)
|
485
|
-
rescue
|
499
|
+
cached = self.class.cached.get("#{options[1]}/#{options[2]}")
|
500
|
+
if cached.nil?
|
486
501
|
initialize("#{options[0]} #{options[1]}/#{options[2]}")
|
502
|
+
else
|
503
|
+
copy(cached) * options[0]
|
487
504
|
end
|
488
505
|
return
|
489
506
|
end
|
@@ -519,34 +536,39 @@ module RubyUnits
|
|
519
536
|
raise ArgumentError, 'Invalid Unit Format'
|
520
537
|
end
|
521
538
|
update_base_scalar
|
522
|
-
raise ArgumentError, 'Temperatures must not be less than absolute zero' if temperature? && base_scalar
|
539
|
+
raise ArgumentError, 'Temperatures must not be less than absolute zero' if temperature? && base_scalar.negative?
|
523
540
|
|
524
541
|
unary_unit = units || ''
|
525
542
|
if options.first.instance_of?(String)
|
526
543
|
_opt_scalar, opt_units = self.class.parse_into_numbers_and_units(options[0])
|
527
|
-
|
544
|
+
if !(self.class.cached.keys.include?(opt_units) ||
|
528
545
|
(opt_units =~ %r{\D/[\d+.]+}) ||
|
529
|
-
(opt_units =~ %r{(#{self.class.temp_regex})|(#{STONE_LB_UNIT_REGEX})|(#{LBS_OZ_UNIT_REGEX})|(#{FEET_INCH_UNITS_REGEX})|%|(#{TIME_REGEX})|i\s?(.+)?|±
|
530
|
-
|
546
|
+
(opt_units =~ %r{(#{self.class.temp_regex})|(#{STONE_LB_UNIT_REGEX})|(#{LBS_OZ_UNIT_REGEX})|(#{FEET_INCH_UNITS_REGEX})|%|(#{TIME_REGEX})|i\s?(.+)?|±|\+/-})) && (opt_units && !opt_units.empty?)
|
547
|
+
self.class.cached.set(opt_units, scalar == 1 ? self : opt_units.to_unit)
|
531
548
|
end
|
532
549
|
end
|
533
|
-
unless
|
534
|
-
|
550
|
+
unless self.class.cached.keys.include?(unary_unit) || (unary_unit =~ self.class.temp_regex)
|
551
|
+
self.class.cached.set(unary_unit, scalar == 1 ? self : unary_unit.to_unit)
|
535
552
|
end
|
536
553
|
[@scalar, @numerator, @denominator, @base_scalar, @signature, @base].each(&:freeze)
|
537
|
-
|
554
|
+
super()
|
538
555
|
end
|
539
556
|
|
540
557
|
# @todo: figure out how to handle :counting units. This method should probably return :counting instead of :unitless for 'each'
|
541
558
|
# return the kind of the unit (:mass, :length, etc...)
|
542
559
|
# @return [Symbol]
|
543
560
|
def kind
|
544
|
-
|
561
|
+
self.class.kinds[signature]
|
545
562
|
end
|
546
563
|
|
547
|
-
#
|
548
|
-
|
549
|
-
|
564
|
+
# Convert the unit to a Unit, possibly performing a conversion.
|
565
|
+
# > The ability to pass a Unit to convert to was added in v3.0.0 for
|
566
|
+
# > consistency with other uses of #to_unit.
|
567
|
+
#
|
568
|
+
# @param other [RubyUnits::Unit, String] unit to convert to
|
569
|
+
# @return [RubyUnits::Unit]
|
570
|
+
def to_unit(other = nil)
|
571
|
+
other ? convert_to(other) : self
|
550
572
|
end
|
551
573
|
|
552
574
|
alias unit to_unit
|
@@ -555,11 +577,12 @@ module RubyUnits
|
|
555
577
|
# @return [Boolean]
|
556
578
|
def base?
|
557
579
|
return @base if defined? @base
|
580
|
+
|
558
581
|
@base = (@numerator + @denominator)
|
559
582
|
.compact
|
560
583
|
.uniq
|
561
|
-
.map {
|
562
|
-
.all? {
|
584
|
+
.map { self.class.definition(_1) }
|
585
|
+
.all? { _1.unity? || _1.base? }
|
563
586
|
@base
|
564
587
|
end
|
565
588
|
|
@@ -571,8 +594,9 @@ module RubyUnits
|
|
571
594
|
# @todo this is brittle as it depends on the display_name of a unit, which can be changed
|
572
595
|
def to_base
|
573
596
|
return self if base?
|
574
|
-
|
575
|
-
|
597
|
+
|
598
|
+
if self.class.unit_map[units] =~ /\A<(?:temp|deg)[CRF]>\Z/
|
599
|
+
@signature = self.class.kinds.key(:temperature)
|
576
600
|
base = if temperature?
|
577
601
|
convert_to('tempK')
|
578
602
|
elsif degree?
|
@@ -581,32 +605,28 @@ module RubyUnits
|
|
581
605
|
return base
|
582
606
|
end
|
583
607
|
|
584
|
-
|
585
|
-
|
586
|
-
rescue
|
587
|
-
nil
|
588
|
-
end)
|
589
|
-
return cached if cached
|
608
|
+
cached_unit = self.class.base_unit_cache.get(units)
|
609
|
+
return cached_unit * scalar unless cached_unit.nil?
|
590
610
|
|
591
611
|
num = []
|
592
612
|
den = []
|
593
613
|
q = Rational(1)
|
594
614
|
@numerator.compact.each do |num_unit|
|
595
|
-
if
|
596
|
-
q *=
|
615
|
+
if self.class.prefix_values[num_unit]
|
616
|
+
q *= self.class.prefix_values[num_unit]
|
597
617
|
else
|
598
|
-
q *=
|
599
|
-
num <<
|
600
|
-
den <<
|
618
|
+
q *= self.class.unit_values[num_unit][:scalar] if self.class.unit_values[num_unit]
|
619
|
+
num << self.class.unit_values[num_unit][:numerator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:numerator]
|
620
|
+
den << self.class.unit_values[num_unit][:denominator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:denominator]
|
601
621
|
end
|
602
622
|
end
|
603
623
|
@denominator.compact.each do |num_unit|
|
604
|
-
if
|
605
|
-
q /=
|
624
|
+
if self.class.prefix_values[num_unit]
|
625
|
+
q /= self.class.prefix_values[num_unit]
|
606
626
|
else
|
607
|
-
q /=
|
608
|
-
den <<
|
609
|
-
num <<
|
627
|
+
q /= self.class.unit_values[num_unit][:scalar] if self.class.unit_values[num_unit]
|
628
|
+
den << self.class.unit_values[num_unit][:numerator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:numerator]
|
629
|
+
num << self.class.unit_values[num_unit][:denominator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:denominator]
|
610
630
|
end
|
611
631
|
end
|
612
632
|
|
@@ -614,7 +634,7 @@ module RubyUnits
|
|
614
634
|
den = den.flatten.compact
|
615
635
|
num = UNITY_ARRAY if num.empty?
|
616
636
|
base = self.class.new(self.class.eliminate_terms(q, num, den))
|
617
|
-
|
637
|
+
self.class.base_unit_cache.set(units, base)
|
618
638
|
base * @scalar
|
619
639
|
end
|
620
640
|
|
@@ -635,33 +655,41 @@ module RubyUnits
|
|
635
655
|
#
|
636
656
|
# @note Rational scalars that are equal to an integer will be represented as integers (i.e, 6/1 => 6, 4/2 => 2, etc..)
|
637
657
|
# @param [Symbol] target_units
|
658
|
+
# @param [Float] precision - the precision to use when converting to a rational
|
638
659
|
# @return [String]
|
639
|
-
def to_s(target_units = nil)
|
660
|
+
def to_s(target_units = nil, precision: 0.0001)
|
640
661
|
out = @output[target_units]
|
641
662
|
return out if out
|
663
|
+
|
642
664
|
separator = RubyUnits.configuration.separator
|
643
665
|
case target_units
|
644
666
|
when :ft
|
645
|
-
inches = convert_to('in').scalar.
|
646
|
-
|
667
|
+
feet, inches = convert_to('in').scalar.abs.divmod(12)
|
668
|
+
improper, frac = inches.divmod(1)
|
669
|
+
frac = frac.zero? ? '' : "-#{frac.rationalize(precision)}"
|
670
|
+
out = "#{negative? ? '-' : nil}#{feet}'#{improper}#{frac}\""
|
647
671
|
when :lbs
|
648
|
-
ounces = convert_to('oz').scalar.
|
649
|
-
|
672
|
+
pounds, ounces = convert_to('oz').scalar.abs.divmod(16)
|
673
|
+
improper, frac = ounces.divmod(1)
|
674
|
+
frac = frac.zero? ? '' : "-#{frac.rationalize(precision)}"
|
675
|
+
out = "#{negative? ? '-' : nil}#{pounds}#{separator}lbs #{improper}#{frac}#{separator}oz"
|
650
676
|
when :stone
|
651
|
-
pounds = convert_to('lbs').scalar.
|
652
|
-
|
677
|
+
stone, pounds = convert_to('lbs').scalar.abs.divmod(14)
|
678
|
+
improper, frac = pounds.divmod(1)
|
679
|
+
frac = frac.zero? ? '' : "-#{frac.rationalize(precision)}"
|
680
|
+
out = "#{negative? ? '-' : nil}#{stone}#{separator}stone #{improper}#{frac}#{separator}lbs"
|
653
681
|
when String
|
654
682
|
out = case target_units.strip
|
655
683
|
when /\A\s*\Z/ # whitespace only
|
656
684
|
''
|
657
|
-
when /(%[
|
685
|
+
when /(%[-+.\w#]+)\s*(.+)*/ # format string like '%0.2f in'
|
658
686
|
begin
|
659
687
|
if Regexp.last_match(2) # unit specified, need to convert
|
660
688
|
convert_to(Regexp.last_match(2)).to_s(Regexp.last_match(1))
|
661
689
|
else
|
662
690
|
"#{Regexp.last_match(1) % @scalar}#{separator}#{Regexp.last_match(2) || units}".strip
|
663
691
|
end
|
664
|
-
rescue # parse it like a strftime format string
|
692
|
+
rescue StandardError # parse it like a strftime format string
|
665
693
|
(DateTime.new(0) + self).strftime(target_units)
|
666
694
|
end
|
667
695
|
when /(\S+)/ # unit only 'mm' or '1/mm'
|
@@ -688,6 +716,7 @@ module RubyUnits
|
|
688
716
|
# @return [String]
|
689
717
|
def inspect(dump = nil)
|
690
718
|
return super() if dump
|
719
|
+
|
691
720
|
to_s
|
692
721
|
end
|
693
722
|
|
@@ -695,7 +724,7 @@ module RubyUnits
|
|
695
724
|
# @return [Boolean]
|
696
725
|
# @todo use unit definition to determine if it's a temperature instead of a regex
|
697
726
|
def temperature?
|
698
|
-
degree? &&
|
727
|
+
degree? && units.match?(self.class.temp_regex)
|
699
728
|
end
|
700
729
|
|
701
730
|
alias is_temperature? temperature?
|
@@ -713,7 +742,8 @@ module RubyUnits
|
|
713
742
|
# @return [String] possible values: degC, degF, degR, or degK
|
714
743
|
def temperature_scale
|
715
744
|
return nil unless temperature?
|
716
|
-
|
745
|
+
|
746
|
+
"deg#{self.class.unit_map[units][/temp([CFRK])/, 1]}"
|
717
747
|
end
|
718
748
|
|
719
749
|
# returns true if no associated units
|
@@ -726,17 +756,19 @@ module RubyUnits
|
|
726
756
|
# Compare two Unit objects. Throws an exception if they are not of compatible types.
|
727
757
|
# Comparisons are done based on the value of the unit in base SI units.
|
728
758
|
# @param [Object] other
|
729
|
-
# @return [
|
759
|
+
# @return [Integer,nil]
|
730
760
|
# @raise [NoMethodError] when other does not define <=>
|
731
761
|
# @raise [ArgumentError] when units are not compatible
|
732
762
|
def <=>(other)
|
733
763
|
raise NoMethodError, "undefined method `<=>' for #{base_scalar.inspect}" unless base_scalar.respond_to?(:<=>)
|
764
|
+
|
734
765
|
if other.nil?
|
735
766
|
base_scalar <=> nil
|
736
767
|
elsif !temperature? && other.respond_to?(:zero?) && other.zero?
|
737
768
|
base_scalar <=> 0
|
738
769
|
elsif other.instance_of?(Unit)
|
739
770
|
raise ArgumentError, "Incompatible Units ('#{units}' not compatible with '#{other.units}')" unless self =~ other
|
771
|
+
|
740
772
|
base_scalar <=> other.base_scalar
|
741
773
|
else
|
742
774
|
x, y = coerce(other)
|
@@ -757,6 +789,7 @@ module RubyUnits
|
|
757
789
|
zero?
|
758
790
|
elsif other.instance_of?(Unit)
|
759
791
|
return false unless self =~ other
|
792
|
+
|
760
793
|
base_scalar == other.base_scalar
|
761
794
|
else
|
762
795
|
begin
|
@@ -768,9 +801,10 @@ module RubyUnits
|
|
768
801
|
end
|
769
802
|
end
|
770
803
|
|
771
|
-
#
|
772
|
-
# this
|
773
|
-
#
|
804
|
+
# Check to see if units are compatible, ignoring the scalar part. This check is done by comparing unit signatures
|
805
|
+
# for performance reasons. If passed a string, this will create a [Unit] object with the string and then do the
|
806
|
+
# comparison.
|
807
|
+
#
|
774
808
|
# @example this permits a syntax like:
|
775
809
|
# unit =~ "mm"
|
776
810
|
# @note if you want to do a regexp comparison of the unit string do this ...
|
@@ -778,17 +812,12 @@ module RubyUnits
|
|
778
812
|
# @param [Object] other
|
779
813
|
# @return [Boolean]
|
780
814
|
def =~(other)
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
return x =~ y
|
788
|
-
rescue ArgumentError
|
789
|
-
return false
|
790
|
-
end
|
791
|
-
end
|
815
|
+
return signature == other.signature if other.is_a?(Unit)
|
816
|
+
|
817
|
+
x, y = coerce(other)
|
818
|
+
x =~ y
|
819
|
+
rescue ArgumentError # return false when `other` cannot be converted to a [Unit]
|
820
|
+
false
|
792
821
|
end
|
793
822
|
|
794
823
|
alias compatible? =~
|
@@ -807,9 +836,9 @@ module RubyUnits
|
|
807
836
|
else
|
808
837
|
begin
|
809
838
|
x, y = coerce(other)
|
810
|
-
|
839
|
+
x === y
|
811
840
|
rescue ArgumentError
|
812
|
-
|
841
|
+
false
|
813
842
|
end
|
814
843
|
end
|
815
844
|
end
|
@@ -832,6 +861,7 @@ module RubyUnits
|
|
832
861
|
other.dup
|
833
862
|
elsif self =~ other
|
834
863
|
raise ArgumentError, 'Cannot add two temperatures' if [self, other].all?(&:temperature?)
|
864
|
+
|
835
865
|
if [self, other].any?(&:temperature?)
|
836
866
|
if temperature?
|
837
867
|
self.class.new(scalar: (scalar + other.convert_to(temperature_scale).scalar), numerator: @numerator, denominator: @denominator, signature: @signature)
|
@@ -881,7 +911,7 @@ module RubyUnits
|
|
881
911
|
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')"
|
882
912
|
end
|
883
913
|
when Time
|
884
|
-
raise ArgumentError, 'Date and Time objects represent fixed points in time and cannot be subtracted from
|
914
|
+
raise ArgumentError, 'Date and Time objects represent fixed points in time and cannot be subtracted from a Unit'
|
885
915
|
else
|
886
916
|
x, y = coerce(other)
|
887
917
|
y - x
|
@@ -896,6 +926,7 @@ module RubyUnits
|
|
896
926
|
case other
|
897
927
|
when Unit
|
898
928
|
raise ArgumentError, 'Cannot multiply by temperatures' if [other, self].any?(&:temperature?)
|
929
|
+
|
899
930
|
opts = self.class.eliminate_terms(@scalar * other.scalar, @numerator + other.numerator, @denominator + other.denominator)
|
900
931
|
opts[:signature] = @signature + other.signature
|
901
932
|
self.class.new(opts)
|
@@ -918,6 +949,7 @@ module RubyUnits
|
|
918
949
|
when Unit
|
919
950
|
raise ZeroDivisionError if other.zero?
|
920
951
|
raise ArgumentError, 'Cannot divide with temperatures' if [other, self].any?(&:temperature?)
|
952
|
+
|
921
953
|
sc = Rational(@scalar, other.scalar)
|
922
954
|
sc = sc.numerator if sc.denominator == 1
|
923
955
|
opts = self.class.eliminate_terms(sc, @numerator + other.denominator, @denominator + other.numerator)
|
@@ -925,6 +957,7 @@ module RubyUnits
|
|
925
957
|
self.class.new(opts)
|
926
958
|
when Numeric
|
927
959
|
raise ZeroDivisionError if other.zero?
|
960
|
+
|
928
961
|
sc = Rational(@scalar, other)
|
929
962
|
sc = sc.numerator if sc.denominator == 1
|
930
963
|
self.class.new(scalar: sc, numerator: @numerator, denominator: @denominator, signature: @signature)
|
@@ -934,26 +967,50 @@ module RubyUnits
|
|
934
967
|
end
|
935
968
|
end
|
936
969
|
|
937
|
-
#
|
938
|
-
#
|
939
|
-
#
|
940
|
-
# @
|
941
|
-
# @
|
970
|
+
# Returns the remainder when one unit is divided by another
|
971
|
+
#
|
972
|
+
# @param [Unit] other
|
973
|
+
# @return [Unit]
|
974
|
+
# @raise [ArgumentError] if units are not compatible
|
975
|
+
def remainder(other)
|
976
|
+
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other)
|
977
|
+
|
978
|
+
self.class.new(base_scalar.remainder(other.to_unit.base_scalar), to_base.units).convert_to(self)
|
979
|
+
end
|
980
|
+
|
981
|
+
# Divide two units and return quotient and remainder
|
982
|
+
#
|
983
|
+
# @param [Unit] other
|
984
|
+
# @return [Array(Integer, Unit)]
|
985
|
+
# @raise [ArgumentError] if units are not compatible
|
942
986
|
def divmod(other)
|
943
|
-
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless
|
944
|
-
|
945
|
-
|
987
|
+
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other)
|
988
|
+
|
989
|
+
[quo(other).to_base.floor, self % other]
|
946
990
|
end
|
947
991
|
|
948
|
-
#
|
949
|
-
#
|
992
|
+
# Perform a modulo on a unit, will raise an exception if the units are not compatible
|
993
|
+
#
|
994
|
+
# @param [Unit] other
|
950
995
|
# @return [Integer]
|
996
|
+
# @raise [ArgumentError] if units are not compatible
|
951
997
|
def %(other)
|
952
|
-
|
998
|
+
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other)
|
999
|
+
|
1000
|
+
self.class.new(base_scalar % other.to_unit.base_scalar, to_base.units).convert_to(self)
|
953
1001
|
end
|
1002
|
+
alias modulo %
|
954
1003
|
|
955
|
-
#
|
956
|
-
#
|
1004
|
+
# @param [Object] other
|
1005
|
+
# @return [Unit]
|
1006
|
+
# @raise [ZeroDivisionError] if other is zero
|
1007
|
+
def quo(other)
|
1008
|
+
self / other
|
1009
|
+
end
|
1010
|
+
alias fdiv quo
|
1011
|
+
|
1012
|
+
# Exponentiation. Only takes integer powers.
|
1013
|
+
# Note that anything raised to the power of 0 results in a [Unit] object with a scalar of 1, and no units.
|
957
1014
|
# Throws an exception if exponent is not an integer.
|
958
1015
|
# Ideally this routine should accept a float for the exponent
|
959
1016
|
# It should then convert the float to a rational and raise the unit by the numerator and root it by the denominator
|
@@ -968,6 +1025,7 @@ module RubyUnits
|
|
968
1025
|
# @raise [ArgumentError] when an invalid exponent is passed
|
969
1026
|
def **(other)
|
970
1027
|
raise ArgumentError, 'Cannot raise a temperature to a power' if temperature?
|
1028
|
+
|
971
1029
|
if other.is_a?(Numeric)
|
972
1030
|
return inverse if other == -1
|
973
1031
|
return self if other == 1
|
@@ -975,14 +1033,16 @@ module RubyUnits
|
|
975
1033
|
end
|
976
1034
|
case other
|
977
1035
|
when Rational
|
978
|
-
|
1036
|
+
power(other.numerator).root(other.denominator)
|
979
1037
|
when Integer
|
980
|
-
|
1038
|
+
power(other)
|
981
1039
|
when Float
|
982
1040
|
return self**other.to_i if other == other.to_i
|
983
|
-
|
1041
|
+
|
1042
|
+
valid = (1..9).map { Rational(1, _1) }
|
984
1043
|
raise ArgumentError, 'Not a n-th root (1..9), use 1/n' unless valid.include? other.abs
|
985
|
-
|
1044
|
+
|
1045
|
+
root(Rational(1, other).to_int)
|
986
1046
|
when Complex
|
987
1047
|
raise ArgumentError, 'exponentiation of complex numbers is not supported.'
|
988
1048
|
else
|
@@ -1002,6 +1062,7 @@ module RubyUnits
|
|
1002
1062
|
return 1 if n.zero?
|
1003
1063
|
return self if n == 1
|
1004
1064
|
return (1..(n - 1).to_i).inject(self) { |acc, _elem| acc * self } if n >= 0
|
1065
|
+
|
1005
1066
|
(1..-(n - 1).to_i).inject(self) { |acc, _elem| acc / self }
|
1006
1067
|
end
|
1007
1068
|
|
@@ -1009,7 +1070,7 @@ module RubyUnits
|
|
1009
1070
|
# if n < 0, returns 1/unit^(1/n)
|
1010
1071
|
# @param [Integer] n
|
1011
1072
|
# @return [Unit]
|
1012
|
-
# @raise [ArgumentError] when
|
1073
|
+
# @raise [ArgumentError] when attempting to take the root of a temperature
|
1013
1074
|
# @raise [ArgumentError] when n is not an integer
|
1014
1075
|
# @raise [ArgumentError] when n is 0
|
1015
1076
|
def root(n)
|
@@ -1017,22 +1078,23 @@ module RubyUnits
|
|
1017
1078
|
raise ArgumentError, 'Exponent must an Integer' unless n.is_a?(Integer)
|
1018
1079
|
raise ArgumentError, '0th root undefined' if n.zero?
|
1019
1080
|
return self if n == 1
|
1020
|
-
return root(n.abs).inverse if n
|
1081
|
+
return root(n.abs).inverse if n.negative?
|
1021
1082
|
|
1022
1083
|
vec = unit_signature_vector
|
1023
|
-
vec = vec.map {
|
1084
|
+
vec = vec.map { _1 % n }
|
1024
1085
|
raise ArgumentError, 'Illegal root' unless vec.max.zero?
|
1086
|
+
|
1025
1087
|
num = @numerator.dup
|
1026
1088
|
den = @denominator.dup
|
1027
1089
|
|
1028
1090
|
@numerator.uniq.each do |item|
|
1029
|
-
x = num.find_all {
|
1091
|
+
x = num.find_all { _1 == item }.size
|
1030
1092
|
r = ((x / n) * (n - 1)).to_int
|
1031
1093
|
r.times { num.delete_at(num.index(item)) }
|
1032
1094
|
end
|
1033
1095
|
|
1034
1096
|
@denominator.uniq.each do |item|
|
1035
|
-
x = den.find_all {
|
1097
|
+
x = den.find_all { _1 == item }.size
|
1036
1098
|
r = ((x / n) * (n - 1)).to_int
|
1037
1099
|
r.times { den.delete_at(den.index(item)) }
|
1038
1100
|
end
|
@@ -1089,7 +1151,7 @@ module RubyUnits
|
|
1089
1151
|
return self if target_unit == start_unit
|
1090
1152
|
|
1091
1153
|
# @type [Numeric]
|
1092
|
-
@base_scalar ||= case
|
1154
|
+
@base_scalar ||= case self.class.unit_map[start_unit]
|
1093
1155
|
when '<tempC>'
|
1094
1156
|
@scalar + 273.15
|
1095
1157
|
when '<tempK>'
|
@@ -1100,7 +1162,7 @@ module RubyUnits
|
|
1100
1162
|
@scalar.to_r * Rational(5, 9)
|
1101
1163
|
end
|
1102
1164
|
# @type [Numeric]
|
1103
|
-
q = case
|
1165
|
+
q = case self.class.unit_map[target_unit]
|
1104
1166
|
when '<tempC>'
|
1105
1167
|
@base_scalar - 273.15
|
1106
1168
|
when '<tempK>'
|
@@ -1110,7 +1172,7 @@ module RubyUnits
|
|
1110
1172
|
when '<tempR>'
|
1111
1173
|
@base_scalar.to_r * Rational(9, 5)
|
1112
1174
|
end
|
1113
|
-
|
1175
|
+
self.class.new("#{q} #{target_unit}")
|
1114
1176
|
else
|
1115
1177
|
# @type [Unit]
|
1116
1178
|
target = case other
|
@@ -1125,10 +1187,10 @@ module RubyUnits
|
|
1125
1187
|
|
1126
1188
|
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless self =~ target
|
1127
1189
|
|
1128
|
-
numerator1 = @numerator.map {
|
1129
|
-
denominator1 = @denominator.map {
|
1130
|
-
numerator2 = target.numerator.map {
|
1131
|
-
denominator2 = target.denominator.map {
|
1190
|
+
numerator1 = @numerator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact
|
1191
|
+
denominator1 = @denominator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact
|
1192
|
+
numerator2 = target.numerator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact
|
1193
|
+
denominator2 = target.denominator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact
|
1132
1194
|
|
1133
1195
|
# If the scalar is an Integer, convert it to a Rational number so that
|
1134
1196
|
# if the value is scaled during conversion, resolution is not lost due
|
@@ -1138,7 +1200,6 @@ module RubyUnits
|
|
1138
1200
|
q = conversion_scalar * (numerator1 + denominator2).reduce(1, :*) / (numerator2 + denominator1).reduce(1, :*)
|
1139
1201
|
# Convert the scalar to an Integer if the result is equivalent to an
|
1140
1202
|
# integer
|
1141
|
-
|
1142
1203
|
q = q.to_i if @scalar.is_a?(Integer) && q.to_i == q
|
1143
1204
|
self.class.new(scalar: q, numerator: target.numerator, denominator: target.denominator, signature: target.signature)
|
1144
1205
|
end
|
@@ -1152,6 +1213,7 @@ module RubyUnits
|
|
1152
1213
|
# @raise [RuntimeError] when not unitless
|
1153
1214
|
def to_f
|
1154
1215
|
return @scalar.to_f if unitless?
|
1216
|
+
|
1155
1217
|
raise "Cannot convert '#{self}' to Float unless unitless. Use Unit#scalar"
|
1156
1218
|
end
|
1157
1219
|
|
@@ -1160,6 +1222,7 @@ module RubyUnits
|
|
1160
1222
|
# @raise [RuntimeError] when not unitless
|
1161
1223
|
def to_c
|
1162
1224
|
return Complex(@scalar) if unitless?
|
1225
|
+
|
1163
1226
|
raise "Cannot convert '#{self}' to Complex unless unitless. Use Unit#scalar"
|
1164
1227
|
end
|
1165
1228
|
|
@@ -1168,6 +1231,7 @@ module RubyUnits
|
|
1168
1231
|
# @raise [RuntimeError] when not unitless
|
1169
1232
|
def to_i
|
1170
1233
|
return @scalar.to_int if unitless?
|
1234
|
+
|
1171
1235
|
raise "Cannot convert '#{self}' to Integer unless unitless. Use Unit#scalar"
|
1172
1236
|
end
|
1173
1237
|
|
@@ -1178,6 +1242,7 @@ module RubyUnits
|
|
1178
1242
|
# @raise [RuntimeError] when not unitless
|
1179
1243
|
def to_r
|
1180
1244
|
return @scalar.to_r if unitless?
|
1245
|
+
|
1181
1246
|
raise "Cannot convert '#{self}' to Rational unless unitless. Use Unit#scalar"
|
1182
1247
|
end
|
1183
1248
|
|
@@ -1200,26 +1265,26 @@ module RubyUnits
|
|
1200
1265
|
den = @denominator.clone.compact
|
1201
1266
|
|
1202
1267
|
unless num == UNITY_ARRAY
|
1203
|
-
definitions = num.map {
|
1268
|
+
definitions = num.map { self.class.definition(_1) }
|
1204
1269
|
definitions.reject!(&:prefix?) unless with_prefix
|
1205
|
-
definitions = definitions.chunk_while { |
|
1206
|
-
output_numerator = definitions.map {
|
1270
|
+
definitions = definitions.chunk_while { |definition, _| definition.prefix? }.to_a
|
1271
|
+
output_numerator = definitions.map { _1.map(&:display_name).join }
|
1207
1272
|
end
|
1208
1273
|
|
1209
1274
|
unless den == UNITY_ARRAY
|
1210
|
-
definitions = den.map {
|
1275
|
+
definitions = den.map { self.class.definition(_1) }
|
1211
1276
|
definitions.reject!(&:prefix?) unless with_prefix
|
1212
|
-
definitions = definitions.chunk_while { |
|
1213
|
-
output_denominator = definitions.map {
|
1277
|
+
definitions = definitions.chunk_while { |definition, _| definition.prefix? }.to_a
|
1278
|
+
output_denominator = definitions.map { _1.map(&:display_name).join }
|
1214
1279
|
end
|
1215
1280
|
|
1216
1281
|
on = output_numerator
|
1217
1282
|
.uniq
|
1218
|
-
.map {
|
1283
|
+
.map { [_1, output_numerator.count(_1)] }
|
1219
1284
|
.map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }
|
1220
1285
|
od = output_denominator
|
1221
1286
|
.uniq
|
1222
|
-
.map {
|
1287
|
+
.map { [_1, output_denominator.count(_1)] }
|
1223
1288
|
.map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }
|
1224
1289
|
"#{on.join('*')}#{od.empty? ? '' : "/#{od.join('*')}"}".strip
|
1225
1290
|
end
|
@@ -1228,6 +1293,7 @@ module RubyUnits
|
|
1228
1293
|
# @return [Numeric,Unit]
|
1229
1294
|
def -@
|
1230
1295
|
return -@scalar if unitless?
|
1296
|
+
|
1231
1297
|
dup * -1
|
1232
1298
|
end
|
1233
1299
|
|
@@ -1235,6 +1301,7 @@ module RubyUnits
|
|
1235
1301
|
# @return [Numeric,Unit]
|
1236
1302
|
def abs
|
1237
1303
|
return @scalar.abs if unitless?
|
1304
|
+
|
1238
1305
|
self.class.new(@scalar.abs, @numerator, @denominator)
|
1239
1306
|
end
|
1240
1307
|
|
@@ -1282,6 +1349,7 @@ module RubyUnits
|
|
1282
1349
|
# @raise [ArgumentError] when scalar is not equal to an integer
|
1283
1350
|
def succ
|
1284
1351
|
raise ArgumentError, 'Non Integer Scalar' unless @scalar == @scalar.to_i
|
1352
|
+
|
1285
1353
|
self.class.new(@scalar.to_i.succ, @numerator, @denominator)
|
1286
1354
|
end
|
1287
1355
|
|
@@ -1293,6 +1361,7 @@ module RubyUnits
|
|
1293
1361
|
# @raise [ArgumentError] when scalar is not equal to an integer
|
1294
1362
|
def pred
|
1295
1363
|
raise ArgumentError, 'Non Integer Scalar' unless @scalar == @scalar.to_i
|
1364
|
+
|
1296
1365
|
self.class.new(@scalar.to_i.pred, @numerator, @denominator)
|
1297
1366
|
end
|
1298
1367
|
|
@@ -1306,7 +1375,7 @@ module RubyUnits
|
|
1306
1375
|
|
1307
1376
|
# convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date
|
1308
1377
|
# defined by DateTime
|
1309
|
-
# @return [DateTime]
|
1378
|
+
# @return [::DateTime]
|
1310
1379
|
def to_datetime
|
1311
1380
|
DateTime.new!(convert_to('d').scalar)
|
1312
1381
|
end
|
@@ -1333,11 +1402,11 @@ module RubyUnits
|
|
1333
1402
|
def before(time_point = ::Time.now)
|
1334
1403
|
case time_point
|
1335
1404
|
when Time, Date, DateTime
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1405
|
+
(begin
|
1406
|
+
time_point - self
|
1407
|
+
rescue StandardError
|
1408
|
+
time_point.to_datetime - self
|
1409
|
+
end)
|
1341
1410
|
else
|
1342
1411
|
raise ArgumentError, 'Must specify a Time, Date, or DateTime'
|
1343
1412
|
end
|
@@ -1352,9 +1421,9 @@ module RubyUnits
|
|
1352
1421
|
def since(time_point)
|
1353
1422
|
case time_point
|
1354
1423
|
when Time
|
1355
|
-
(Time.now - time_point
|
1424
|
+
self.class.new(::Time.now - time_point, 'second').convert_to(self)
|
1356
1425
|
when DateTime, Date
|
1357
|
-
(DateTime.now - time_point
|
1426
|
+
self.class.new(::DateTime.now - time_point, 'day').convert_to(self)
|
1358
1427
|
else
|
1359
1428
|
raise ArgumentError, 'Must specify a Time, Date, or DateTime'
|
1360
1429
|
end
|
@@ -1366,9 +1435,9 @@ module RubyUnits
|
|
1366
1435
|
def until(time_point)
|
1367
1436
|
case time_point
|
1368
1437
|
when Time
|
1369
|
-
(time_point - Time.now
|
1438
|
+
self.class.new(time_point - ::Time.now, 'second').convert_to(self)
|
1370
1439
|
when DateTime, Date
|
1371
|
-
(time_point - DateTime.now
|
1440
|
+
self.class.new(time_point - ::DateTime.now, 'day').convert_to(self)
|
1372
1441
|
else
|
1373
1442
|
raise ArgumentError, 'Must specify a Time, Date, or DateTime'
|
1374
1443
|
end
|
@@ -1382,10 +1451,10 @@ module RubyUnits
|
|
1382
1451
|
case time_point
|
1383
1452
|
when Time, DateTime, Date
|
1384
1453
|
(begin
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1454
|
+
time_point + self
|
1455
|
+
rescue StandardError
|
1456
|
+
time_point.to_datetime + self
|
1457
|
+
end)
|
1389
1458
|
else
|
1390
1459
|
raise ArgumentError, 'Must specify a Time, Date, or DateTime'
|
1391
1460
|
end
|
@@ -1394,29 +1463,28 @@ module RubyUnits
|
|
1394
1463
|
alias after from
|
1395
1464
|
alias from_now from
|
1396
1465
|
|
1397
|
-
#
|
1398
|
-
#
|
1466
|
+
# Automatically coerce objects to [Unit] when possible. If an object defines a '#to_unit' method, it will be coerced
|
1467
|
+
# using that method.
|
1468
|
+
#
|
1399
1469
|
# @param other [Object, #to_unit]
|
1400
|
-
# @return [Array]
|
1470
|
+
# @return [Array(Unit, Unit)]
|
1471
|
+
# @raise [ArgumentError] when `other` cannot be converted to a [Unit]
|
1401
1472
|
def coerce(other)
|
1402
|
-
return [other.to_unit, self] if other.respond_to?
|
1403
|
-
|
1404
|
-
|
1405
|
-
[other, self]
|
1406
|
-
else
|
1407
|
-
[self.class.new(other), self]
|
1408
|
-
end
|
1473
|
+
return [other.to_unit, self] if other.respond_to?(:to_unit)
|
1474
|
+
|
1475
|
+
[self.class.new(other), self]
|
1409
1476
|
end
|
1410
1477
|
|
1411
1478
|
# returns a new unit that has been scaled to be more in line with typical usage.
|
1412
1479
|
def best_prefix
|
1413
1480
|
return to_base if scalar.zero?
|
1481
|
+
|
1414
1482
|
best_prefix = if kind == :information
|
1415
|
-
|
1483
|
+
self.class.prefix_values.key(2**((::Math.log(base_scalar, 2) / 10.0).floor * 10))
|
1416
1484
|
else
|
1417
|
-
|
1485
|
+
self.class.prefix_values.key(10**((::Math.log10(base_scalar) / 3.0).floor * 3))
|
1418
1486
|
end
|
1419
|
-
to(self.class.new(
|
1487
|
+
to(self.class.new(self.class.prefix_map.key(best_prefix) + units(with_prefix: false)))
|
1420
1488
|
end
|
1421
1489
|
|
1422
1490
|
# override hash method so objects with same values are considered equal
|
@@ -1453,18 +1521,20 @@ module RubyUnits
|
|
1453
1521
|
# @raise [ArgumentError] when exponent associated with a unit is > 20 or < -20
|
1454
1522
|
def unit_signature_vector
|
1455
1523
|
return to_base.unit_signature_vector unless base?
|
1456
|
-
|
1524
|
+
|
1525
|
+
vector = ::Array.new(SIGNATURE_VECTOR.size, 0)
|
1457
1526
|
# it's possible to have a kind that misses the array... kinds like :counting
|
1458
1527
|
# are more like prefixes, so don't use them to calculate the vector
|
1459
|
-
@numerator.map {
|
1528
|
+
@numerator.map { self.class.definition(_1) }.each do |definition|
|
1460
1529
|
index = SIGNATURE_VECTOR.index(definition.kind)
|
1461
1530
|
vector[index] += 1 if index
|
1462
1531
|
end
|
1463
|
-
@denominator.map {
|
1532
|
+
@denominator.map { self.class.definition(_1) }.each do |definition|
|
1464
1533
|
index = SIGNATURE_VECTOR.index(definition.kind)
|
1465
1534
|
vector[index] -= 1 if index
|
1466
1535
|
end
|
1467
|
-
raise ArgumentError, 'Power out of range (-20 < net power of a unit < 20)' if vector.any? {
|
1536
|
+
raise ArgumentError, 'Power out of range (-20 < net power of a unit < 20)' if vector.any? { _1.abs >= 20 }
|
1537
|
+
|
1468
1538
|
vector
|
1469
1539
|
end
|
1470
1540
|
|
@@ -1486,8 +1556,9 @@ module RubyUnits
|
|
1486
1556
|
# @return [Array]
|
1487
1557
|
def unit_signature
|
1488
1558
|
return @signature unless @signature.nil?
|
1559
|
+
|
1489
1560
|
vector = unit_signature_vector
|
1490
|
-
vector.each_with_index { |item, index| vector[index] = item * 20**index }
|
1561
|
+
vector.each_with_index { |item, index| vector[index] = item * (20**index) }
|
1491
1562
|
@signature = vector.inject(0) { |acc, elem| acc + elem }
|
1492
1563
|
@signature
|
1493
1564
|
end
|
@@ -1503,39 +1574,52 @@ module RubyUnits
|
|
1503
1574
|
# "GPa" -- creates a unit with scalar 1 with units 'GPa'
|
1504
1575
|
# 6'4" -- recognized as 6 feet + 4 inches
|
1505
1576
|
# 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
|
1506
|
-
# @return [nil
|
1577
|
+
# @return [nil,RubyUnits::Unit]
|
1507
1578
|
# @todo This should either be a separate class or at least a class method
|
1508
1579
|
def parse(passed_unit_string = '0')
|
1509
1580
|
unit_string = passed_unit_string.dup
|
1510
1581
|
unit_string = "#{Regexp.last_match(1)} USD" if unit_string =~ /\$\s*(#{NUMBER_REGEX})/
|
1511
1582
|
unit_string.gsub!("\u00b0".force_encoding('utf-8'), 'deg') if unit_string.encoding == Encoding::UTF_8
|
1512
1583
|
|
1513
|
-
unit_string.gsub!(/[%'"#]/, '%' => 'percent', "'" => 'feet', '"' => 'inch', '#' => 'pound')
|
1514
|
-
|
1515
|
-
|
1516
|
-
real
|
1517
|
-
|
1584
|
+
unit_string.gsub!(/[%'"#_,]/, '%' => 'percent', "'" => 'feet', '"' => 'inch', '#' => 'pound', '_' => '', ',' => '')
|
1585
|
+
if unit_string.start_with?(COMPLEX_NUMBER)
|
1586
|
+
match = unit_string.match(COMPLEX_REGEX)
|
1587
|
+
real = Float(match[:real]) if match[:real]
|
1588
|
+
imaginary = Float(match[:imaginary])
|
1589
|
+
unit_s = match[:unit]
|
1590
|
+
real = real.to_i if real.to_i == real
|
1591
|
+
imaginary = imaginary.to_i if imaginary.to_i == imaginary
|
1592
|
+
complex = Complex(real || 0, imaginary)
|
1593
|
+
complex = complex.to_i if complex.imaginary.zero? && complex.real == complex.real.to_i
|
1594
|
+
result = self.class.new(unit_s || 1) * complex
|
1518
1595
|
copy(result)
|
1519
1596
|
return
|
1520
1597
|
end
|
1521
1598
|
|
1522
|
-
if
|
1523
|
-
|
1524
|
-
|
1525
|
-
|
1526
|
-
|
1599
|
+
if unit_string.start_with?(RATIONAL_NUMBER)
|
1600
|
+
match = unit_string.match(RATIONAL_REGEX)
|
1601
|
+
numerator = Integer(match[:numerator])
|
1602
|
+
denominator = Integer(match[:denominator])
|
1603
|
+
raise ArgumentError, 'Improper fractions must have a whole number part' if !match[:proper].nil? && !match[:proper].match?(/^#{INTEGER_REGEX}$/)
|
1604
|
+
|
1605
|
+
proper = match[:proper].to_i
|
1606
|
+
unit_s = match[:unit]
|
1607
|
+
rational = if proper.negative?
|
1608
|
+
(proper - Rational(numerator, denominator))
|
1609
|
+
else
|
1610
|
+
(proper + Rational(numerator, denominator))
|
1611
|
+
end
|
1612
|
+
rational = rational.to_int if rational.to_int == rational
|
1613
|
+
result = self.class.new(unit_s || 1) * rational
|
1527
1614
|
copy(result)
|
1528
1615
|
return
|
1529
1616
|
end
|
1530
1617
|
|
1531
|
-
|
1532
|
-
unit =
|
1533
|
-
mult =
|
1534
|
-
(Regexp.last_match(1).empty? ? 1.0 : Regexp.last_match(1).to_f)
|
1535
|
-
rescue
|
1536
|
-
1.0
|
1537
|
-
end
|
1618
|
+
match = unit_string.match(NUMBER_REGEX)
|
1619
|
+
unit = self.class.cached.get(match[:unit])
|
1620
|
+
mult = match[:scalar] == '' ? 1.0 : match[:scalar].to_f
|
1538
1621
|
mult = mult.to_int if mult.to_int == mult
|
1622
|
+
|
1539
1623
|
if unit
|
1540
1624
|
copy(unit)
|
1541
1625
|
@scalar *= mult
|
@@ -1543,52 +1627,70 @@ module RubyUnits
|
|
1543
1627
|
return self
|
1544
1628
|
end
|
1545
1629
|
|
1546
|
-
while unit_string.gsub!(/(<#{
|
1630
|
+
while unit_string.gsub!(/(<#{self.class.unit_regex})><(#{self.class.unit_regex}>)/, '\1*\2')
|
1547
1631
|
# collapse <x><y><z> into <x*y*z>...
|
1548
1632
|
end
|
1549
1633
|
# ... and then strip the remaining brackets for x*y*z
|
1550
1634
|
unit_string.gsub!(/[<>]/, '')
|
1551
1635
|
|
1552
|
-
if
|
1553
|
-
hours
|
1554
|
-
|
1636
|
+
if (match = unit_string.match(TIME_REGEX))
|
1637
|
+
hours = match[:hour]
|
1638
|
+
minutes = match[:min]
|
1639
|
+
seconds = match[:sec]
|
1640
|
+
milliseconds = match[:msec]
|
1641
|
+
raise ArgumentError, 'Invalid Duration' if [hours, minutes, seconds, milliseconds].all?(&:nil?)
|
1555
1642
|
|
1556
|
-
result = self.class.new("#{hours || 0}
|
1643
|
+
result = self.class.new("#{hours || 0} hours") +
|
1557
1644
|
self.class.new("#{minutes || 0} minutes") +
|
1558
1645
|
self.class.new("#{seconds || 0} seconds") +
|
1559
|
-
self.class.new("#{
|
1646
|
+
self.class.new("#{milliseconds || 0} milliseconds")
|
1560
1647
|
copy(result)
|
1561
1648
|
return
|
1562
1649
|
end
|
1563
1650
|
|
1564
1651
|
# Special processing for unusual unit strings
|
1565
1652
|
# feet -- 6'5"
|
1566
|
-
|
1567
|
-
|
1568
|
-
|
1653
|
+
if (match = unit_string.match(FEET_INCH_REGEX))
|
1654
|
+
feet = Integer(match[:feet])
|
1655
|
+
inches = match[:inches]
|
1656
|
+
result = if feet.negative?
|
1657
|
+
self.class.new("#{feet} ft") - self.class.new("#{inches} inches")
|
1658
|
+
else
|
1659
|
+
self.class.new("#{feet} ft") + self.class.new("#{inches} inches")
|
1660
|
+
end
|
1569
1661
|
copy(result)
|
1570
1662
|
return
|
1571
1663
|
end
|
1572
1664
|
|
1573
1665
|
# weight -- 8 lbs 12 oz
|
1574
|
-
|
1575
|
-
|
1576
|
-
|
1666
|
+
if (match = unit_string.match(LBS_OZ_REGEX))
|
1667
|
+
pounds = Integer(match[:pounds])
|
1668
|
+
oz = match[:oz]
|
1669
|
+
result = if pounds.negative?
|
1670
|
+
self.class.new("#{pounds} lbs") - self.class.new("#{oz} oz")
|
1671
|
+
else
|
1672
|
+
self.class.new("#{pounds} lbs") + self.class.new("#{oz} oz")
|
1673
|
+
end
|
1577
1674
|
copy(result)
|
1578
1675
|
return
|
1579
1676
|
end
|
1580
1677
|
|
1581
1678
|
# stone -- 3 stone 5, 2 stone, 14 stone 3 pounds, etc.
|
1582
|
-
|
1583
|
-
|
1584
|
-
|
1679
|
+
if (match = unit_string.match(STONE_LB_REGEX))
|
1680
|
+
stone = Integer(match[:stone])
|
1681
|
+
pounds = match[:pounds]
|
1682
|
+
result = if stone.negative?
|
1683
|
+
self.class.new("#{stone} stone") - self.class.new("#{pounds} lbs")
|
1684
|
+
else
|
1685
|
+
self.class.new("#{stone} stone") + self.class.new("#{pounds} lbs")
|
1686
|
+
end
|
1585
1687
|
copy(result)
|
1586
1688
|
return
|
1587
1689
|
end
|
1588
1690
|
|
1589
1691
|
# more than one per. I.e., "1 m/s/s"
|
1590
1692
|
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1
|
1591
|
-
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string =~ /\s[02-9]/
|
1693
|
+
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized #{unit_string}") if unit_string =~ /\s[02-9]/
|
1592
1694
|
|
1593
1695
|
@scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] # parse the string into parts
|
1594
1696
|
top.scan(TOP_REGEX).each do |item|
|
@@ -1596,7 +1698,7 @@ module RubyUnits
|
|
1596
1698
|
x = "#{item[0]} "
|
1597
1699
|
if n >= 0
|
1598
1700
|
top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) { x * n }
|
1599
|
-
elsif n
|
1701
|
+
elsif n.negative?
|
1600
1702
|
bottom = "#{bottom} #{x * -n}"
|
1601
1703
|
top.gsub!(/#{item[0]}(\^|\*\*)#{n}/, '')
|
1602
1704
|
end
|
@@ -1627,15 +1729,15 @@ module RubyUnits
|
|
1627
1729
|
|
1628
1730
|
# eliminate all known terms from this string. This is a quick check to see if the passed unit
|
1629
1731
|
# contains terms that are not defined.
|
1630
|
-
used = "#{top} #{bottom}".to_s.gsub(self.class.unit_match_regex, '').gsub(%r{[\d
|
1732
|
+
used = "#{top} #{bottom}".to_s.gsub(self.class.unit_match_regex, '').gsub(%r{[\d*, "'_^/$]}, '')
|
1631
1733
|
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless used.empty?
|
1632
1734
|
|
1633
1735
|
@numerator = @numerator.map do |item|
|
1634
|
-
|
1736
|
+
self.class.prefix_map[item[0]] ? [self.class.prefix_map[item[0]], self.class.unit_map[item[1]]] : [self.class.unit_map[item[1]]]
|
1635
1737
|
end.flatten.compact.delete_if(&:empty?)
|
1636
1738
|
|
1637
1739
|
@denominator = @denominator.map do |item|
|
1638
|
-
|
1740
|
+
self.class.prefix_map[item[0]] ? [self.class.prefix_map[item[0]], self.class.unit_map[item[1]]] : [self.class.unit_map[item[1]]]
|
1639
1741
|
end.flatten.compact.delete_if(&:empty?)
|
1640
1742
|
|
1641
1743
|
@numerator = UNITY_ARRAY if @numerator.empty?
|