ruby-units 2.4.1 → 4.0.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.
- 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?
|