ruby-units 2.3.2 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +16 -0
- data/.github/workflows/codeql-analysis.yml +70 -0
- data/.github/workflows/tests.yml +10 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.txt +2 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +86 -67
- data/lib/ruby-units.rb +0 -1
- data/lib/ruby_units/configuration.rb +5 -4
- data/lib/ruby_units/definition.rb +6 -2
- data/lib/ruby_units/namespaced.rb +0 -3
- data/lib/ruby_units/unit.rb +230 -220
- data/lib/ruby_units/version.rb +1 -1
- data/ruby-units.gemspec +4 -2
- metadata +39 -9
data/lib/ruby_units/unit.rb
CHANGED
@@ -31,30 +31,41 @@ module RubyUnits
|
|
31
31
|
@@unit_match_regex = nil
|
32
32
|
UNITY = '<1>'.freeze
|
33
33
|
UNITY_ARRAY = [UNITY].freeze
|
34
|
-
# ideally we would like to generate this regex from the alias for a 'feet'
|
35
|
-
# defined at the point in the code where we
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
34
|
+
# ideally we would like to generate this regex from the alias for a 'feet'
|
35
|
+
# and 'inches', but they aren't defined at the point in the code where we
|
36
|
+
# need this regex.
|
37
|
+
FEET_INCH_UNITS_REGEX = /(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inch(?:es)?)/.freeze
|
38
|
+
FEET_INCH_REGEX = /(\d+)\s*#{FEET_INCH_UNITS_REGEX}/.freeze
|
39
|
+
# ideally we would like to generate this regex from the alias for a 'pound'
|
40
|
+
# and 'ounce', but they aren't defined at the point in the code where we
|
41
|
+
# need this regex.
|
42
|
+
LBS_OZ_UNIT_REGEX = /(?:#|lbs?|pounds?|pound-mass)+[\s,]*(\d+)\s*(?:ozs?|ounces?)/.freeze
|
43
|
+
LBS_OZ_REGEX = /(\d+)\s*#{LBS_OZ_UNIT_REGEX}/.freeze
|
44
|
+
# ideally we would like to generate this regex from the alias for a 'stone'
|
45
|
+
# and 'pound', but they aren't defined at the point in the code where we
|
46
|
+
# need this regex. also note that the plural of 'stone' is still 'stone',
|
47
|
+
# but we accept 'stones' anyway.
|
48
|
+
STONE_LB_UNIT_REGEX = /(?:sts?|stones?)+[\s,]*(\d+)\s*(?:#|lbs?|pounds?|pound-mass)*/.freeze
|
49
|
+
STONE_LB_REGEX = /(\d+)\s*#{STONE_LB_UNIT_REGEX}/.freeze
|
50
|
+
# Time formats: 12:34:56,78, (hh:mm:ss,msec) etc.
|
51
|
+
TIME_REGEX = /(?<hour>\d+):(?<min>\d+):(?:(?<sec>\d+))?(?:,(?<msec>\d+))?/.freeze
|
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
|
57
|
+
# Complex numbers: 1+2i, 1.0+2.0i, -1-1i, etc.
|
58
|
+
COMPLEX_NUMBER = /#{SCI_NUMBER}?#{SCI_NUMBER}i\b/.freeze
|
59
|
+
# Any Complex, Rational, or scientific number
|
60
|
+
ANY_NUMBER = /(#{COMPLEX_NUMBER}|#{RATIONAL_NUMBER}|#{SCI_NUMBER})/.freeze
|
61
|
+
ANY_NUMBER_REGEX = /(?:#{ANY_NUMBER})?\s?([^-\d.].*)?/.freeze
|
62
|
+
NUMBER_REGEX = /#{SCI_NUMBER}*\s*(.+)?/.freeze
|
63
|
+
UNIT_STRING_REGEX = %r{#{SCI_NUMBER}*\s*([^\/]*)\/*(.+)*}.freeze
|
64
|
+
TOP_REGEX = /([^ \*]+)(?:\^|\*\*)([\d-]+)/.freeze
|
65
|
+
BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/.freeze
|
66
|
+
NUMBER_UNIT_REGEX = /#{SCI_NUMBER}?(.*)/.freeze
|
67
|
+
COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(.+)?/.freeze
|
68
|
+
RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(.+)?/.freeze
|
58
69
|
KELVIN = ['<kelvin>'].freeze
|
59
70
|
FAHRENHEIT = ['<fahrenheit>'].freeze
|
60
71
|
RANKINE = ['<rankine>'].freeze
|
@@ -139,11 +150,11 @@ module RubyUnits
|
|
139
150
|
@@unit_match_regex = nil
|
140
151
|
@@prefix_regex = nil
|
141
152
|
|
142
|
-
@@definitions.
|
153
|
+
@@definitions.each_value do |definition|
|
143
154
|
use_definition(definition)
|
144
155
|
end
|
145
156
|
|
146
|
-
|
157
|
+
new(1)
|
147
158
|
true
|
148
159
|
end
|
149
160
|
|
@@ -155,7 +166,7 @@ module RubyUnits
|
|
155
166
|
end
|
156
167
|
|
157
168
|
# return the unit definition for a unit
|
158
|
-
# @param [String]
|
169
|
+
# @param unit_name [String]
|
159
170
|
# @return [RubyUnits::Unit::Definition, nil]
|
160
171
|
def self.definition(unit_name)
|
161
172
|
unit = unit_name =~ /^<.+>$/ ? unit_name : "<#{unit_name}>"
|
@@ -163,12 +174,12 @@ module RubyUnits
|
|
163
174
|
end
|
164
175
|
|
165
176
|
# return a list of all defined units
|
166
|
-
# @return [Array]
|
177
|
+
# @return [Array<RubyUnits::Units::Definition>]
|
167
178
|
def self.definitions
|
168
179
|
@@definitions
|
169
180
|
end
|
170
181
|
|
171
|
-
# @param [RubyUnits::Unit::Definition
|
182
|
+
# @param [RubyUnits::Unit::Definition, String] unit_definition
|
172
183
|
# @param [Block] block
|
173
184
|
# @return [RubyUnits::Unit::Definition]
|
174
185
|
# @raise [ArgumentError] when passed a non-string if using the block form
|
@@ -187,33 +198,38 @@ module RubyUnits
|
|
187
198
|
raise ArgumentError, 'When using the block form of RubyUnits::Unit.define, pass the name of the unit' unless unit_definition.instance_of?(String)
|
188
199
|
unit_definition = RubyUnits::Unit::Definition.new(unit_definition, &block)
|
189
200
|
end
|
190
|
-
|
191
|
-
|
201
|
+
definitions[unit_definition.name] = unit_definition
|
202
|
+
use_definition(unit_definition)
|
192
203
|
unit_definition
|
193
204
|
end
|
194
205
|
|
206
|
+
# Get the definition for a unit and allow it to be redefined
|
207
|
+
#
|
195
208
|
# @param [String] name Name of unit to redefine
|
196
|
-
# @param [Block]
|
209
|
+
# @param [Block] _block
|
197
210
|
# @raise [ArgumentError] if a block is not given
|
198
|
-
# @
|
211
|
+
# @yieldparam [RubyUnits::Unit::Definition] the definition of the unit being
|
212
|
+
# redefined
|
199
213
|
# @return (see RubyUnits::Unit.define)
|
200
|
-
|
201
|
-
def self.redefine!(name)
|
214
|
+
def self.redefine!(name, &_block)
|
202
215
|
raise ArgumentError, 'A block is required to redefine a unit' unless block_given?
|
216
|
+
|
203
217
|
unit_definition = definition(name)
|
204
218
|
raise(ArgumentError, "'#{name}' Unit not recognized") unless unit_definition
|
219
|
+
|
205
220
|
yield unit_definition
|
206
221
|
@@definitions.delete("<#{name}>")
|
207
222
|
define(unit_definition)
|
208
|
-
|
223
|
+
setup
|
209
224
|
end
|
210
225
|
|
211
|
-
# @param [String] name of unit to undefine
|
212
|
-
# @return (see RubyUnits::Unit.setup)
|
213
226
|
# Undefine a unit. Will not raise an exception for unknown units.
|
227
|
+
#
|
228
|
+
# @param unit [String] name of unit to undefine
|
229
|
+
# @return (see RubyUnits::Unit.setup)
|
214
230
|
def self.undefine!(unit)
|
215
231
|
@@definitions.delete("<#{unit}>")
|
216
|
-
|
232
|
+
setup
|
217
233
|
end
|
218
234
|
|
219
235
|
# @return [Hash]
|
@@ -225,7 +241,7 @@ module RubyUnits
|
|
225
241
|
def self.clear_cache
|
226
242
|
@@cached_units = {}
|
227
243
|
@@base_unit_cache = {}
|
228
|
-
|
244
|
+
new(1)
|
229
245
|
true
|
230
246
|
end
|
231
247
|
|
@@ -240,104 +256,74 @@ module RubyUnits
|
|
240
256
|
# @return [Unit]
|
241
257
|
def self.parse(input)
|
242
258
|
first, second = input.scan(/(.+)\s(?:in|to|as)\s(.+)/i).first
|
243
|
-
second.nil? ?
|
259
|
+
second.nil? ? new(first) : new(first).convert_to(second)
|
244
260
|
end
|
245
261
|
|
246
|
-
# @param [Numeric]
|
247
|
-
# @param [Array]
|
248
|
-
# @param [Array]
|
262
|
+
# @param q [Numeric] quantity
|
263
|
+
# @param n [Array] numerator
|
264
|
+
# @param d [Array] denominator
|
249
265
|
# @return [Hash]
|
250
266
|
def self.eliminate_terms(q, n, d)
|
251
267
|
num = n.dup
|
252
268
|
den = d.dup
|
269
|
+
num.delete(UNITY)
|
270
|
+
den.delete(UNITY)
|
253
271
|
|
254
|
-
num.delete_if { |v| v == UNITY }
|
255
|
-
den.delete_if { |v| v == UNITY }
|
256
272
|
combined = Hash.new(0)
|
257
273
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
k = [num[i], num[i + 1]]
|
263
|
-
i += 2
|
264
|
-
else
|
265
|
-
k = num[i]
|
266
|
-
i += 1
|
267
|
-
end
|
268
|
-
combined[k] += 1 unless k.nil? || k == UNITY
|
269
|
-
end
|
270
|
-
|
271
|
-
j = 0
|
272
|
-
loop do
|
273
|
-
break if j > den.size
|
274
|
-
if @@prefix_values.key? den[j]
|
275
|
-
k = [den[j], den[j + 1]]
|
276
|
-
j += 2
|
277
|
-
else
|
278
|
-
k = den[j]
|
279
|
-
j += 1
|
280
|
-
end
|
281
|
-
combined[k] -= 1 unless k.nil? || k == UNITY
|
274
|
+
[[num, 1], [den, -1]].each do |array, increment|
|
275
|
+
array.chunk_while { |elt_before, _| definition(elt_before).prefix? }
|
276
|
+
.to_a
|
277
|
+
.each { |unit| combined[unit] += increment }
|
282
278
|
end
|
283
279
|
|
284
280
|
num = []
|
285
281
|
den = []
|
286
282
|
combined.each do |key, value|
|
287
|
-
if value
|
283
|
+
if value.positive?
|
288
284
|
value.times { num << key }
|
289
|
-
elsif value
|
285
|
+
elsif value.negative?
|
290
286
|
value.abs.times { den << key }
|
291
287
|
end
|
292
288
|
end
|
293
289
|
num = UNITY_ARRAY if num.empty?
|
294
290
|
den = UNITY_ARRAY if den.empty?
|
295
|
-
{ scalar: q, numerator: num.flatten
|
291
|
+
{ scalar: q, numerator: num.flatten, denominator: den.flatten }
|
292
|
+
end
|
293
|
+
|
294
|
+
# Creates a new unit from the current one with all common terms eliminated.
|
295
|
+
#
|
296
|
+
# @return [RubyUnits::Unit]
|
297
|
+
def eliminate_terms
|
298
|
+
self.class.new(self.class.eliminate_terms(@scalar, @numerator, @denominator))
|
296
299
|
end
|
297
300
|
|
298
301
|
# return an array of base units
|
299
302
|
# @return [Array]
|
300
303
|
def self.base_units
|
301
|
-
@@base_units ||= @@definitions.dup.delete_if { |_, defn| !defn.base? }.keys.map { |u|
|
304
|
+
@@base_units ||= @@definitions.dup.delete_if { |_, defn| !defn.base? }.keys.map { |u| new(u) }
|
302
305
|
end
|
303
306
|
|
304
307
|
# parse a string consisting of a number and a unit string
|
305
308
|
# NOTE: This does not properly handle units formatted like '12mg/6ml'
|
306
309
|
# @param [String] string
|
307
|
-
# @return [Array] consisting of [
|
310
|
+
# @return [Array(Numeric, String)] consisting of [number, "unit"]
|
308
311
|
def self.parse_into_numbers_and_units(string)
|
309
|
-
|
310
|
-
sci = /[+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*/
|
311
|
-
# rational numbers.... -1/3, 1/5, 20/100, -6 1/2, -6-1/2
|
312
|
-
rational = %r{\(?[+-]?(?:\d+[ -])?\d+\/\d+\)?}
|
313
|
-
# complex numbers... -1.2+3i, +1.2-3.3i
|
314
|
-
complex = /#{sci}{2,2}i/
|
315
|
-
anynumber = /(?:(#{complex}|#{rational}|#{sci}))?\s?([^-\d\.].*)?/
|
316
|
-
|
317
|
-
num, unit = string.scan(anynumber).first
|
312
|
+
num, unit = string.scan(ANY_NUMBER_REGEX).first
|
318
313
|
|
319
314
|
[
|
320
315
|
case num
|
321
|
-
when
|
316
|
+
when nil # This happens when no number is passed and we are parsing a pure unit string
|
322
317
|
1
|
323
|
-
when
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
# if it has whitespace, it will be of the form '6 1/2'
|
333
|
-
if num =~ RATIONAL_NUMBER
|
334
|
-
sign = Regexp.last_match(1) == '-' ? -1 : 1
|
335
|
-
n = Regexp.last_match(2).to_i
|
336
|
-
f = Rational(Regexp.last_match(3).to_i, Regexp.last_match(4).to_i)
|
337
|
-
sign * (n + f)
|
338
|
-
else
|
339
|
-
Rational(*num.split('/').map(&:to_i))
|
340
|
-
end
|
318
|
+
when COMPLEX_NUMBER
|
319
|
+
num.to_c
|
320
|
+
when RATIONAL_NUMBER
|
321
|
+
# We use this method instead of relying on `to_r` because it does not
|
322
|
+
# handle improper fractions correctly.
|
323
|
+
sign = Regexp.last_match(1) == '-' ? -1 : 1
|
324
|
+
n = Regexp.last_match(2).to_i
|
325
|
+
f = Rational(Regexp.last_match(3).to_i, Regexp.last_match(4).to_i)
|
326
|
+
sign * (n + f)
|
341
327
|
else
|
342
328
|
num.to_f
|
343
329
|
end,
|
@@ -355,7 +341,7 @@ module RubyUnits
|
|
355
341
|
# return a regex used to match units
|
356
342
|
# @return [RegExp]
|
357
343
|
def self.unit_match_regex
|
358
|
-
@@unit_match_regex ||= /(#{
|
344
|
+
@@unit_match_regex ||= /(#{prefix_regex})??(#{unit_regex})\b/
|
359
345
|
end
|
360
346
|
|
361
347
|
# return a regexp fragment used to match prefixes
|
@@ -365,11 +351,14 @@ module RubyUnits
|
|
365
351
|
@@prefix_regex ||= @@prefix_map.keys.sort_by { |prefix| [prefix.length, prefix] }.reverse.join('|')
|
366
352
|
end
|
367
353
|
|
354
|
+
# Generates (and memoizes) a regexp matching any of the temperature units or their aliases.
|
355
|
+
#
|
356
|
+
# @return [RegExp]
|
368
357
|
def self.temp_regex
|
369
358
|
@@temp_regex ||= begin
|
370
359
|
temp_units = %w[tempK tempC tempF tempR degK degC degF degR]
|
371
360
|
aliases = temp_units.map do |unit|
|
372
|
-
d =
|
361
|
+
d = definition(unit)
|
373
362
|
d && d.aliases
|
374
363
|
end.flatten.compact
|
375
364
|
regex_str = aliases.empty? ? '(?!x)x' : aliases.join('|')
|
@@ -378,6 +367,8 @@ module RubyUnits
|
|
378
367
|
end
|
379
368
|
|
380
369
|
# inject a definition into the internal array and set it up for use
|
370
|
+
#
|
371
|
+
# @param definition [RubyUnits::Unit::Definition]
|
381
372
|
def self.use_definition(definition)
|
382
373
|
@@unit_match_regex = nil # invalidate the unit match regex
|
383
374
|
@@temp_regex = nil # invalidate the temp regex
|
@@ -469,6 +460,7 @@ module RubyUnits
|
|
469
460
|
@signature = nil
|
470
461
|
@output = {}
|
471
462
|
raise ArgumentError, 'Invalid Unit Format' if options[0].nil?
|
463
|
+
|
472
464
|
if options.size == 2
|
473
465
|
# options[0] is the scalar
|
474
466
|
# options[1] is a unit string
|
@@ -528,16 +520,17 @@ module RubyUnits
|
|
528
520
|
end
|
529
521
|
update_base_scalar
|
530
522
|
raise ArgumentError, 'Temperatures must not be less than absolute zero' if temperature? && base_scalar < 0
|
523
|
+
|
531
524
|
unary_unit = units || ''
|
532
525
|
if options.first.instance_of?(String)
|
533
|
-
_opt_scalar, opt_units =
|
526
|
+
_opt_scalar, opt_units = self.class.parse_into_numbers_and_units(options[0])
|
534
527
|
unless @@cached_units.keys.include?(opt_units) ||
|
535
|
-
(opt_units =~ %r{\D/[\d
|
536
|
-
(opt_units =~ %r{(#{
|
528
|
+
(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?(.+)?|±|\+\/-})
|
537
530
|
@@cached_units[opt_units] = (scalar == 1 ? self : opt_units.to_unit) if opt_units && !opt_units.empty?
|
538
531
|
end
|
539
532
|
end
|
540
|
-
unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /#{
|
533
|
+
unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /#{self.class.temp_regex}/)
|
541
534
|
@@cached_units[unary_unit] = (scalar == 1 ? self : unary_unit.to_unit)
|
542
535
|
end
|
543
536
|
[@scalar, @numerator, @denominator, @base_scalar, @signature, @base].each(&:freeze)
|
@@ -565,7 +558,7 @@ module RubyUnits
|
|
565
558
|
@base = (@numerator + @denominator)
|
566
559
|
.compact
|
567
560
|
.uniq
|
568
|
-
.map { |unit|
|
561
|
+
.map { |unit| self.class.definition(unit) }
|
569
562
|
.all? { |element| element.unity? || element.base? }
|
570
563
|
@base
|
571
564
|
end
|
@@ -620,7 +613,7 @@ module RubyUnits
|
|
620
613
|
num = num.flatten.compact
|
621
614
|
den = den.flatten.compact
|
622
615
|
num = UNITY_ARRAY if num.empty?
|
623
|
-
base =
|
616
|
+
base = self.class.new(self.class.eliminate_terms(q, num, den))
|
624
617
|
@@base_unit_cache[units] = base
|
625
618
|
base * @scalar
|
626
619
|
end
|
@@ -841,12 +834,12 @@ module RubyUnits
|
|
841
834
|
raise ArgumentError, 'Cannot add two temperatures' if [self, other].all?(&:temperature?)
|
842
835
|
if [self, other].any?(&:temperature?)
|
843
836
|
if temperature?
|
844
|
-
|
837
|
+
self.class.new(scalar: (scalar + other.convert_to(temperature_scale).scalar), numerator: @numerator, denominator: @denominator, signature: @signature)
|
845
838
|
else
|
846
|
-
|
839
|
+
self.class.new(scalar: (other.scalar + convert_to(other.temperature_scale).scalar), numerator: other.numerator, denominator: other.denominator, signature: other.signature)
|
847
840
|
end
|
848
841
|
else
|
849
|
-
|
842
|
+
self.class.new(scalar: (base_scalar + other.base_scalar), numerator: base.numerator, denominator: base.denominator, signature: @signature).convert_to(self)
|
850
843
|
end
|
851
844
|
else
|
852
845
|
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')"
|
@@ -876,13 +869,13 @@ module RubyUnits
|
|
876
869
|
end
|
877
870
|
elsif self =~ other
|
878
871
|
if [self, other].all?(&:temperature?)
|
879
|
-
|
872
|
+
self.class.new(scalar: (base_scalar - other.base_scalar), numerator: KELVIN, denominator: UNITY_ARRAY, signature: @signature).convert_to(temperature_scale)
|
880
873
|
elsif temperature?
|
881
|
-
|
874
|
+
self.class.new(scalar: (base_scalar - other.base_scalar), numerator: ['<tempK>'], denominator: UNITY_ARRAY, signature: @signature).convert_to(self)
|
882
875
|
elsif other.temperature?
|
883
876
|
raise ArgumentError, 'Cannot subtract a temperature from a differential degree unit'
|
884
877
|
else
|
885
|
-
|
878
|
+
self.class.new(scalar: (base_scalar - other.base_scalar), numerator: base.numerator, denominator: base.denominator, signature: @signature).convert_to(self)
|
886
879
|
end
|
887
880
|
else
|
888
881
|
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')"
|
@@ -903,11 +896,11 @@ module RubyUnits
|
|
903
896
|
case other
|
904
897
|
when Unit
|
905
898
|
raise ArgumentError, 'Cannot multiply by temperatures' if [other, self].any?(&:temperature?)
|
906
|
-
opts =
|
899
|
+
opts = self.class.eliminate_terms(@scalar * other.scalar, @numerator + other.numerator, @denominator + other.denominator)
|
907
900
|
opts[:signature] = @signature + other.signature
|
908
|
-
|
901
|
+
self.class.new(opts)
|
909
902
|
when Numeric
|
910
|
-
|
903
|
+
self.class.new(scalar: @scalar * other, numerator: @numerator, denominator: @denominator, signature: @signature)
|
911
904
|
else
|
912
905
|
x, y = coerce(other)
|
913
906
|
x * y
|
@@ -927,14 +920,14 @@ module RubyUnits
|
|
927
920
|
raise ArgumentError, 'Cannot divide with temperatures' if [other, self].any?(&:temperature?)
|
928
921
|
sc = Rational(@scalar, other.scalar)
|
929
922
|
sc = sc.numerator if sc.denominator == 1
|
930
|
-
opts =
|
923
|
+
opts = self.class.eliminate_terms(sc, @numerator + other.denominator, @denominator + other.numerator)
|
931
924
|
opts[:signature] = @signature - other.signature
|
932
|
-
|
925
|
+
self.class.new(opts)
|
933
926
|
when Numeric
|
934
927
|
raise ZeroDivisionError if other.zero?
|
935
928
|
sc = Rational(@scalar, other)
|
936
929
|
sc = sc.numerator if sc.denominator == 1
|
937
|
-
|
930
|
+
self.class.new(scalar: sc, numerator: @numerator, denominator: @denominator, signature: @signature)
|
938
931
|
else
|
939
932
|
x, y = coerce(other)
|
940
933
|
y / x
|
@@ -1043,13 +1036,13 @@ module RubyUnits
|
|
1043
1036
|
r = ((x / n) * (n - 1)).to_int
|
1044
1037
|
r.times { den.delete_at(den.index(item)) }
|
1045
1038
|
end
|
1046
|
-
|
1039
|
+
self.class.new(scalar: @scalar**Rational(1, n), numerator: num, denominator: den)
|
1047
1040
|
end
|
1048
1041
|
|
1049
1042
|
# returns inverse of Unit (1/unit)
|
1050
1043
|
# @return [Unit]
|
1051
1044
|
def inverse
|
1052
|
-
|
1045
|
+
self.class.new('1') / self
|
1053
1046
|
end
|
1054
1047
|
|
1055
1048
|
# convert to a specified unit string or to the same units as another Unit
|
@@ -1060,13 +1053,17 @@ module RubyUnits
|
|
1060
1053
|
# To convert a Unit object to match another Unit object, use:
|
1061
1054
|
# unit1 >>= unit2
|
1062
1055
|
#
|
1063
|
-
# Special handling for temperature conversions is supported. If the Unit
|
1064
|
-
# from one temperature unit to another, the proper
|
1065
|
-
# Supports Kelvin, Celsius, Fahrenheit,
|
1056
|
+
# Special handling for temperature conversions is supported. If the Unit
|
1057
|
+
# object is converted from one temperature unit to another, the proper
|
1058
|
+
# temperature offsets will be used. Supports Kelvin, Celsius, Fahrenheit,
|
1059
|
+
# and Rankine scales.
|
1066
1060
|
#
|
1067
|
-
# @note If temperature is part of a compound unit, the temperature will be
|
1068
|
-
# and the units will be scaled appropriately.
|
1069
|
-
# @
|
1061
|
+
# @note If temperature is part of a compound unit, the temperature will be
|
1062
|
+
# treated as a differential and the units will be scaled appropriately.
|
1063
|
+
# @note When converting units with Integer scalars, the scalar will be
|
1064
|
+
# converted to a Rational to avoid unexpected behavior caused by Integer
|
1065
|
+
# division.
|
1066
|
+
# @param other [Unit, String]
|
1070
1067
|
# @return [Unit]
|
1071
1068
|
# @raise [ArgumentError] when attempting to convert a degree to a temperature
|
1072
1069
|
# @raise [ArgumentError] when target unit is unknown
|
@@ -1075,14 +1072,23 @@ module RubyUnits
|
|
1075
1072
|
return self if other.nil?
|
1076
1073
|
return self if TrueClass === other
|
1077
1074
|
return self if FalseClass === other
|
1078
|
-
|
1075
|
+
|
1076
|
+
if (other.is_a?(Unit) && other.temperature?) || (other.is_a?(String) && other =~ self.class.temp_regex)
|
1079
1077
|
raise ArgumentError, 'Receiver is not a temperature unit' unless degree?
|
1078
|
+
|
1080
1079
|
start_unit = units
|
1081
|
-
|
1080
|
+
# @type [String]
|
1081
|
+
target_unit = case other
|
1082
|
+
when Unit
|
1082
1083
|
other.units
|
1083
|
-
|
1084
|
+
when String
|
1084
1085
|
other
|
1086
|
+
else
|
1087
|
+
raise ArgumentError, 'Unknown target units'
|
1085
1088
|
end
|
1089
|
+
return self if target_unit == start_unit
|
1090
|
+
|
1091
|
+
# @type [Numeric]
|
1086
1092
|
@base_scalar ||= case @@unit_map[start_unit]
|
1087
1093
|
when '<tempC>'
|
1088
1094
|
@scalar + 273.15
|
@@ -1093,36 +1099,48 @@ module RubyUnits
|
|
1093
1099
|
when '<tempR>'
|
1094
1100
|
@scalar.to_r * Rational(5, 9)
|
1095
1101
|
end
|
1102
|
+
# @type [Numeric]
|
1096
1103
|
q = case @@unit_map[target_unit]
|
1097
1104
|
when '<tempC>'
|
1098
|
-
@base_scalar - 273.
|
1105
|
+
@base_scalar - 273.15
|
1099
1106
|
when '<tempK>'
|
1100
1107
|
@base_scalar
|
1101
1108
|
when '<tempF>'
|
1102
|
-
@base_scalar.to_r * Rational(9, 5) - 459.67r
|
1109
|
+
(@base_scalar.to_r * Rational(9, 5)) - 459.67r
|
1103
1110
|
when '<tempR>'
|
1104
1111
|
@base_scalar.to_r * Rational(9, 5)
|
1105
1112
|
end
|
1106
|
-
return
|
1113
|
+
return self.class.new("#{q} #{target_unit}")
|
1107
1114
|
else
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1115
|
+
# @type [Unit]
|
1116
|
+
target = case other
|
1117
|
+
when Unit
|
1118
|
+
other
|
1119
|
+
when String
|
1120
|
+
self.class.new(other)
|
1121
|
+
else
|
1122
|
+
raise ArgumentError, 'Unknown target units'
|
1123
|
+
end
|
1124
|
+
return self if target.units == units
|
1125
|
+
|
1117
1126
|
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless self =~ target
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1127
|
+
|
1128
|
+
numerator1 = @numerator.map { |x| @@prefix_values[x] || x }.map { |i| i.is_a?(Numeric) ? i : @@unit_values[i][:scalar] }.compact
|
1129
|
+
denominator1 = @denominator.map { |x| @@prefix_values[x] || x }.map { |i| i.is_a?(Numeric) ? i : @@unit_values[i][:scalar] }.compact
|
1130
|
+
numerator2 = target.numerator.map { |x| @@prefix_values[x] || x }.map { |x| x.is_a?(Numeric) ? x : @@unit_values[x][:scalar] }.compact
|
1131
|
+
denominator2 = target.denominator.map { |x| @@prefix_values[x] || x }.map { |x| x.is_a?(Numeric) ? x : @@unit_values[x][:scalar] }.compact
|
1132
|
+
|
1133
|
+
# If the scalar is an Integer, convert it to a Rational number so that
|
1134
|
+
# if the value is scaled during conversion, resolution is not lost due
|
1135
|
+
# to integer math
|
1136
|
+
# @type [Rational, Numeric]
|
1137
|
+
conversion_scalar = @scalar.is_a?(Integer) ? @scalar.to_r : @scalar
|
1138
|
+
q = conversion_scalar * (numerator1 + denominator2).reduce(1, :*) / (numerator2 + denominator1).reduce(1, :*)
|
1139
|
+
# Convert the scalar to an Integer if the result is equivalent to an
|
1140
|
+
# integer
|
1141
|
+
|
1142
|
+
q = q.to_i if @scalar.is_a?(Integer) && q.to_i == q
|
1143
|
+
self.class.new(scalar: q, numerator: target.numerator, denominator: target.denominator, signature: target.signature)
|
1126
1144
|
end
|
1127
1145
|
end
|
1128
1146
|
|
@@ -1169,52 +1187,29 @@ module RubyUnits
|
|
1169
1187
|
to_s
|
1170
1188
|
end
|
1171
1189
|
|
1172
|
-
#
|
1190
|
+
# Returns the 'unit' part of the Unit object without the scalar
|
1191
|
+
#
|
1192
|
+
# @param with_prefix [Boolean] include prefixes in output
|
1173
1193
|
# @return [String]
|
1174
1194
|
def units(with_prefix: true)
|
1175
1195
|
return '' if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
|
1196
|
+
|
1176
1197
|
output_numerator = ['1']
|
1177
1198
|
output_denominator = []
|
1178
1199
|
num = @numerator.clone.compact
|
1179
1200
|
den = @denominator.clone.compact
|
1180
1201
|
|
1181
1202
|
unless num == UNITY_ARRAY
|
1182
|
-
definitions = num.map { |element|
|
1203
|
+
definitions = num.map { |element| self.class.definition(element) }
|
1183
1204
|
definitions.reject!(&:prefix?) unless with_prefix
|
1184
|
-
|
1185
|
-
# see https://github.com/jruby/jruby/issues/4410
|
1186
|
-
# TODO: fix this after jruby fixes their bug.
|
1187
|
-
definitions = if definitions.respond_to?(:chunk_while) && RUBY_ENGINE != 'jruby'
|
1188
|
-
definitions.chunk_while { |defn, _| defn.prefix? }.to_a
|
1189
|
-
else # chunk_while is new to ruby 2.3+, so fallback to less efficient methods for older ruby
|
1190
|
-
result = []
|
1191
|
-
enumerator = definitions.to_enum
|
1192
|
-
loop do
|
1193
|
-
first = enumerator.next
|
1194
|
-
result << (first.prefix? ? [first, enumerator.next] : [first])
|
1195
|
-
end
|
1196
|
-
result
|
1197
|
-
end
|
1205
|
+
definitions = definitions.chunk_while { |defn, _| defn.prefix? }.to_a
|
1198
1206
|
output_numerator = definitions.map { |element| element.map(&:display_name).join }
|
1199
1207
|
end
|
1200
1208
|
|
1201
1209
|
unless den == UNITY_ARRAY
|
1202
|
-
definitions = den.map { |element|
|
1210
|
+
definitions = den.map { |element| self.class.definition(element) }
|
1203
1211
|
definitions.reject!(&:prefix?) unless with_prefix
|
1204
|
-
|
1205
|
-
# see https://github.com/jruby/jruby/issues/4410
|
1206
|
-
# TODO: fix this after jruby fixes their bug.
|
1207
|
-
definitions = if definitions.respond_to?(:chunk_while) && RUBY_ENGINE != 'jruby'
|
1208
|
-
definitions.chunk_while { |defn, _| defn.prefix? }.to_a
|
1209
|
-
else # chunk_while is new to ruby 2.3+, so fallback to less efficient methods for older ruby
|
1210
|
-
result = []
|
1211
|
-
enumerator = definitions.to_enum
|
1212
|
-
loop do
|
1213
|
-
first = enumerator.next
|
1214
|
-
result << (first.prefix? ? [first, enumerator.next] : [first])
|
1215
|
-
end
|
1216
|
-
result
|
1217
|
-
end
|
1212
|
+
definitions = definitions.chunk_while { |defn, _| defn.prefix? }.to_a
|
1218
1213
|
output_denominator = definitions.map { |element| element.map(&:display_name).join }
|
1219
1214
|
end
|
1220
1215
|
|
@@ -1226,7 +1221,7 @@ module RubyUnits
|
|
1226
1221
|
.uniq
|
1227
1222
|
.map { |x| [x, output_denominator.count(x)] }
|
1228
1223
|
.map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }
|
1229
|
-
"#{on.join('*')}#{od.empty? ? '' :
|
1224
|
+
"#{on.join('*')}#{od.empty? ? '' : "/#{od.join('*')}"}".strip
|
1230
1225
|
end
|
1231
1226
|
|
1232
1227
|
# negates the scalar of the Unit
|
@@ -1240,32 +1235,45 @@ module RubyUnits
|
|
1240
1235
|
# @return [Numeric,Unit]
|
1241
1236
|
def abs
|
1242
1237
|
return @scalar.abs if unitless?
|
1243
|
-
|
1238
|
+
self.class.new(@scalar.abs, @numerator, @denominator)
|
1244
1239
|
end
|
1245
1240
|
|
1246
1241
|
# ceil of a unit
|
1247
1242
|
# @return [Numeric,Unit]
|
1248
|
-
def ceil
|
1249
|
-
return @scalar.ceil if unitless?
|
1250
|
-
|
1243
|
+
def ceil(*args)
|
1244
|
+
return @scalar.ceil(*args) if unitless?
|
1245
|
+
|
1246
|
+
self.class.new(@scalar.ceil(*args), @numerator, @denominator)
|
1251
1247
|
end
|
1252
1248
|
|
1253
1249
|
# @return [Numeric,Unit]
|
1254
|
-
def floor
|
1255
|
-
return @scalar.floor if unitless?
|
1256
|
-
|
1250
|
+
def floor(*args)
|
1251
|
+
return @scalar.floor(*args) if unitless?
|
1252
|
+
|
1253
|
+
self.class.new(@scalar.floor(*args), @numerator, @denominator)
|
1257
1254
|
end
|
1258
1255
|
|
1256
|
+
# Round the unit according to the rules of the scalar's class. Call this
|
1257
|
+
# with the arguments appropriate for the scalar's class (e.g., Integer,
|
1258
|
+
# Rational, etc..). Because unit conversions can often result in Rational
|
1259
|
+
# scalars (to preserve precision), it may be advisable to use +to_s+ to
|
1260
|
+
# format output instead of using +round+.
|
1261
|
+
# @example
|
1262
|
+
# RubyUnits::Unit.new('21870 mm/min').convert_to('m/min').round(1) #=> 2187/100 m/min
|
1263
|
+
# RubyUnits::Unit.new('21870 mm/min').convert_to('m/min').to_s('%0.1f') #=> 21.9 m/min
|
1264
|
+
#
|
1259
1265
|
# @return [Numeric,Unit]
|
1260
|
-
def round(
|
1261
|
-
return @scalar.round(
|
1262
|
-
|
1266
|
+
def round(*args, **kwargs)
|
1267
|
+
return @scalar.round(*args, **kwargs) if unitless?
|
1268
|
+
|
1269
|
+
self.class.new(@scalar.round(*args, **kwargs), @numerator, @denominator)
|
1263
1270
|
end
|
1264
1271
|
|
1265
1272
|
# @return [Numeric, Unit]
|
1266
|
-
def truncate
|
1267
|
-
return @scalar.truncate if unitless?
|
1268
|
-
|
1273
|
+
def truncate(*args)
|
1274
|
+
return @scalar.truncate(*args) if unitless?
|
1275
|
+
|
1276
|
+
self.class.new(@scalar.truncate(*args), @numerator, @denominator)
|
1269
1277
|
end
|
1270
1278
|
|
1271
1279
|
# returns next unit in a range. '1 mm'.to_unit.succ #=> '2 mm'.to_unit
|
@@ -1274,7 +1282,7 @@ module RubyUnits
|
|
1274
1282
|
# @raise [ArgumentError] when scalar is not equal to an integer
|
1275
1283
|
def succ
|
1276
1284
|
raise ArgumentError, 'Non Integer Scalar' unless @scalar == @scalar.to_i
|
1277
|
-
|
1285
|
+
self.class.new(@scalar.to_i.succ, @numerator, @denominator)
|
1278
1286
|
end
|
1279
1287
|
|
1280
1288
|
alias next succ
|
@@ -1285,7 +1293,7 @@ module RubyUnits
|
|
1285
1293
|
# @raise [ArgumentError] when scalar is not equal to an integer
|
1286
1294
|
def pred
|
1287
1295
|
raise ArgumentError, 'Non Integer Scalar' unless @scalar == @scalar.to_i
|
1288
|
-
|
1296
|
+
self.class.new(@scalar.to_i.pred, @numerator, @denominator)
|
1289
1297
|
end
|
1290
1298
|
|
1291
1299
|
# Tries to make a Time object from current unit. Assumes the current unit hold the duration in seconds from the epoch.
|
@@ -1388,7 +1396,7 @@ module RubyUnits
|
|
1388
1396
|
|
1389
1397
|
# automatically coerce objects to units when possible
|
1390
1398
|
# if an object defines a 'to_unit' method, it will be coerced using that method
|
1391
|
-
# @param [Object, #to_unit]
|
1399
|
+
# @param other [Object, #to_unit]
|
1392
1400
|
# @return [Array]
|
1393
1401
|
def coerce(other)
|
1394
1402
|
return [other.to_unit, self] if other.respond_to? :to_unit
|
@@ -1396,7 +1404,7 @@ module RubyUnits
|
|
1396
1404
|
when Unit
|
1397
1405
|
[other, self]
|
1398
1406
|
else
|
1399
|
-
[
|
1407
|
+
[self.class.new(other), self]
|
1400
1408
|
end
|
1401
1409
|
end
|
1402
1410
|
|
@@ -1408,7 +1416,7 @@ module RubyUnits
|
|
1408
1416
|
else
|
1409
1417
|
@@prefix_values.key(10**((Math.log10(base_scalar) / 3.0).floor * 3))
|
1410
1418
|
end
|
1411
|
-
to(
|
1419
|
+
to(self.class.new(@@prefix_map.key(best_prefix) + units(with_prefix: false)))
|
1412
1420
|
end
|
1413
1421
|
|
1414
1422
|
# override hash method so objects with same values are considered equal
|
@@ -1448,11 +1456,11 @@ module RubyUnits
|
|
1448
1456
|
vector = Array.new(SIGNATURE_VECTOR.size, 0)
|
1449
1457
|
# it's possible to have a kind that misses the array... kinds like :counting
|
1450
1458
|
# are more like prefixes, so don't use them to calculate the vector
|
1451
|
-
@numerator.map { |element|
|
1459
|
+
@numerator.map { |element| self.class.definition(element) }.each do |definition|
|
1452
1460
|
index = SIGNATURE_VECTOR.index(definition.kind)
|
1453
1461
|
vector[index] += 1 if index
|
1454
1462
|
end
|
1455
|
-
@denominator.map { |element|
|
1463
|
+
@denominator.map { |element| self.class.definition(element) }.each do |definition|
|
1456
1464
|
index = SIGNATURE_VECTOR.index(definition.kind)
|
1457
1465
|
vector[index] -= 1 if index
|
1458
1466
|
end
|
@@ -1506,7 +1514,7 @@ module RubyUnits
|
|
1506
1514
|
|
1507
1515
|
if defined?(Complex) && unit_string =~ COMPLEX_NUMBER
|
1508
1516
|
real, imaginary, unit_s = unit_string.scan(COMPLEX_REGEX)[0]
|
1509
|
-
result =
|
1517
|
+
result = self.class.new(unit_s || '1') * Complex(real.to_f, imaginary.to_f)
|
1510
1518
|
copy(result)
|
1511
1519
|
return
|
1512
1520
|
end
|
@@ -1515,7 +1523,7 @@ module RubyUnits
|
|
1515
1523
|
sign, proper, numerator, denominator, unit_s = unit_string.scan(RATIONAL_REGEX)[0]
|
1516
1524
|
sign = sign == '-' ? -1 : 1
|
1517
1525
|
rational = sign * (proper.to_i + Rational(numerator.to_i, denominator.to_i))
|
1518
|
-
result =
|
1526
|
+
result = self.class.new(unit_s || '1') * rational
|
1519
1527
|
copy(result)
|
1520
1528
|
return
|
1521
1529
|
end
|
@@ -1541,13 +1549,14 @@ module RubyUnits
|
|
1541
1549
|
# ... and then strip the remaining brackets for x*y*z
|
1542
1550
|
unit_string.gsub!(/[<>]/, '')
|
1543
1551
|
|
1544
|
-
if unit_string =~
|
1552
|
+
if unit_string =~ TIME_REGEX
|
1545
1553
|
hours, minutes, seconds, microseconds = unit_string.scan(TIME_REGEX)[0]
|
1546
|
-
raise ArgumentError,
|
1547
|
-
|
1548
|
-
|
1549
|
-
|
1550
|
-
|
1554
|
+
raise ArgumentError,'Invalid Duration' if [hours, minutes, seconds, microseconds].all?(&:nil?)
|
1555
|
+
|
1556
|
+
result = self.class.new("#{hours || 0} h") +
|
1557
|
+
self.class.new("#{minutes || 0} minutes") +
|
1558
|
+
self.class.new("#{seconds || 0} seconds") +
|
1559
|
+
self.class.new("#{microseconds || 0} usec")
|
1551
1560
|
copy(result)
|
1552
1561
|
return
|
1553
1562
|
end
|
@@ -1556,7 +1565,7 @@ module RubyUnits
|
|
1556
1565
|
# feet -- 6'5"
|
1557
1566
|
feet, inches = unit_string.scan(FEET_INCH_REGEX)[0]
|
1558
1567
|
if feet && inches
|
1559
|
-
result =
|
1568
|
+
result = self.class.new("#{feet} ft") + self.class.new("#{inches} inches")
|
1560
1569
|
copy(result)
|
1561
1570
|
return
|
1562
1571
|
end
|
@@ -1564,7 +1573,7 @@ module RubyUnits
|
|
1564
1573
|
# weight -- 8 lbs 12 oz
|
1565
1574
|
pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0]
|
1566
1575
|
if pounds && oz
|
1567
|
-
result =
|
1576
|
+
result = self.class.new("#{pounds} lbs") + self.class.new("#{oz} oz")
|
1568
1577
|
copy(result)
|
1569
1578
|
return
|
1570
1579
|
end
|
@@ -1572,7 +1581,7 @@ module RubyUnits
|
|
1572
1581
|
# stone -- 3 stone 5, 2 stone, 14 stone 3 pounds, etc.
|
1573
1582
|
stone, pounds = unit_string.scan(STONE_LB_REGEX)[0]
|
1574
1583
|
if stone && pounds
|
1575
|
-
result =
|
1584
|
+
result = self.class.new("#{stone} stone") + self.class.new("#{pounds} lbs")
|
1576
1585
|
copy(result)
|
1577
1586
|
return
|
1578
1587
|
end
|
@@ -1580,6 +1589,7 @@ module RubyUnits
|
|
1580
1589
|
# more than one per. I.e., "1 m/s/s"
|
1581
1590
|
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1
|
1582
1591
|
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string =~ /\s[02-9]/
|
1592
|
+
|
1583
1593
|
@scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] # parse the string into parts
|
1584
1594
|
top.scan(TOP_REGEX).each do |item|
|
1585
1595
|
n = item[1].to_i
|
@@ -1612,12 +1622,12 @@ module RubyUnits
|
|
1612
1622
|
|
1613
1623
|
@numerator ||= UNITY_ARRAY
|
1614
1624
|
@denominator ||= UNITY_ARRAY
|
1615
|
-
@numerator = top.scan(
|
1616
|
-
@denominator = bottom.scan(
|
1625
|
+
@numerator = top.scan(self.class.unit_match_regex).delete_if(&:empty?).compact if top
|
1626
|
+
@denominator = bottom.scan(self.class.unit_match_regex).delete_if(&:empty?).compact if bottom
|
1617
1627
|
|
1618
1628
|
# eliminate all known terms from this string. This is a quick check to see if the passed unit
|
1619
1629
|
# contains terms that are not defined.
|
1620
|
-
used = "#{top} #{bottom}".to_s.gsub(
|
1630
|
+
used = "#{top} #{bottom}".to_s.gsub(self.class.unit_match_regex, '').gsub(%r{[\d\*, "'_^\/\$]}, '')
|
1621
1631
|
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless used.empty?
|
1622
1632
|
|
1623
1633
|
@numerator = @numerator.map do |item|
|