ruby-units 3.0.0 → 4.0.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/workflows/tests.yml +3 -1
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.tool-versions +1 -2
- data/Gemfile +16 -1
- data/Gemfile.lock +88 -66
- data/README.md +2 -0
- data/lib/ruby_units/unit.rb +261 -178
- data/lib/ruby_units/version.rb +1 -1
- data/ruby-units.gemspec +2 -14
- metadata +7 -175
data/lib/ruby_units/unit.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'date'
|
2
2
|
module RubyUnits
|
3
|
-
# Copyright 2006-
|
3
|
+
# Copyright 2006-2023
|
4
4
|
# @author Kevin C. Olbrich, Ph.D.
|
5
5
|
# @see https://github.com/olbrich/ruby-units
|
6
6
|
#
|
@@ -22,56 +22,85 @@ module RubyUnits
|
|
22
22
|
# end
|
23
23
|
#
|
24
24
|
class Unit < ::Numeric
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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 =
|
91
|
+
NUMBER_REGEX = /(?<scalar>#{SCI_NUMBER}*)\s*(?<unit>.+)?/.freeze # a number followed by a unit
|
63
92
|
UNIT_STRING_REGEX = %r{#{SCI_NUMBER}*\s*([^/]*)/*(.+)*}.freeze
|
64
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,7 +112,7 @@ module RubyUnits
|
|
83
112
|
information
|
84
113
|
angle
|
85
114
|
].freeze
|
86
|
-
|
115
|
+
@kinds = {
|
87
116
|
-312_078 => :elastance,
|
88
117
|
-312_058 => :resistance,
|
89
118
|
-312_038 => :inductance,
|
@@ -140,15 +169,15 @@ module RubyUnits
|
|
140
169
|
# @return [Boolean]
|
141
170
|
def self.setup
|
142
171
|
clear_cache
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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|
|
152
181
|
use_definition(definition)
|
153
182
|
end
|
154
183
|
|
@@ -160,7 +189,7 @@ module RubyUnits
|
|
160
189
|
# @param [String] unit
|
161
190
|
# @return [Boolean]
|
162
191
|
def self.defined?(unit)
|
163
|
-
definitions.values.any? {
|
192
|
+
definitions.values.any? { _1.aliases.include?(unit) }
|
164
193
|
end
|
165
194
|
|
166
195
|
# return the unit definition for a unit
|
@@ -168,13 +197,7 @@ module RubyUnits
|
|
168
197
|
# @return [RubyUnits::Unit::Definition, nil]
|
169
198
|
def self.definition(unit_name)
|
170
199
|
unit = unit_name =~ /^<.+>$/ ? unit_name : "<#{unit_name}>"
|
171
|
-
|
172
|
-
end
|
173
|
-
|
174
|
-
# return a list of all defined units
|
175
|
-
# @return [Array<RubyUnits::Units::Definition>]
|
176
|
-
def self.definitions
|
177
|
-
@@definitions
|
200
|
+
definitions[unit]
|
178
201
|
end
|
179
202
|
|
180
203
|
# @param [RubyUnits::Unit::Definition, String] unit_definition
|
@@ -217,7 +240,7 @@ module RubyUnits
|
|
217
240
|
raise(ArgumentError, "'#{name}' Unit not recognized") unless unit_definition
|
218
241
|
|
219
242
|
yield unit_definition
|
220
|
-
|
243
|
+
definitions.delete("<#{name}>")
|
221
244
|
define(unit_definition)
|
222
245
|
setup
|
223
246
|
end
|
@@ -227,7 +250,7 @@ module RubyUnits
|
|
227
250
|
# @param unit [String] name of unit to undefine
|
228
251
|
# @return (see RubyUnits::Unit.setup)
|
229
252
|
def self.undefine!(unit)
|
230
|
-
|
253
|
+
definitions.delete("<#{unit}>")
|
231
254
|
setup
|
232
255
|
end
|
233
256
|
|
@@ -275,7 +298,7 @@ module RubyUnits
|
|
275
298
|
[[num, 1], [den, -1]].each do |array, increment|
|
276
299
|
array.chunk_while { |elt_before, _| definition(elt_before).prefix? }
|
277
300
|
.to_a
|
278
|
-
.each {
|
301
|
+
.each { combined[_1] += increment }
|
279
302
|
end
|
280
303
|
|
281
304
|
num = []
|
@@ -302,7 +325,7 @@ module RubyUnits
|
|
302
325
|
# return an array of base units
|
303
326
|
# @return [Array]
|
304
327
|
def self.base_units
|
305
|
-
|
328
|
+
@base_units ||= definitions.dup.select { |_, definition| definition.base? }.keys.map { new(_1) }
|
306
329
|
end
|
307
330
|
|
308
331
|
# Parse a string consisting of a number and a unit string
|
@@ -337,27 +360,27 @@ module RubyUnits
|
|
337
360
|
# Unit names are reverse sorted by length so the regexp matcher will prefer longer and more specific names
|
338
361
|
# @return [String]
|
339
362
|
def self.unit_regex
|
340
|
-
|
363
|
+
@unit_regex ||= unit_map.keys.sort_by { [_1.length, _1] }.reverse.join('|')
|
341
364
|
end
|
342
365
|
|
343
366
|
# return a regex used to match units
|
344
367
|
# @return [Regexp]
|
345
368
|
def self.unit_match_regex
|
346
|
-
|
369
|
+
@unit_match_regex ||= /(#{prefix_regex})??(#{unit_regex})\b/
|
347
370
|
end
|
348
371
|
|
349
372
|
# return a regexp fragment used to match prefixes
|
350
373
|
# @return [String]
|
351
374
|
# @private
|
352
375
|
def self.prefix_regex
|
353
|
-
|
376
|
+
@prefix_regex ||= prefix_map.keys.sort_by { [_1.length, _1] }.reverse.join('|')
|
354
377
|
end
|
355
378
|
|
356
379
|
# Generates (and memoizes) a regexp matching any of the temperature units or their aliases.
|
357
380
|
#
|
358
381
|
# @return [Regexp]
|
359
382
|
def self.temp_regex
|
360
|
-
|
383
|
+
@temp_regex ||= begin
|
361
384
|
temp_units = %w[tempK tempC tempF tempR degK degC degF degR]
|
362
385
|
aliases = temp_units.map do |unit|
|
363
386
|
d = definition(unit)
|
@@ -372,19 +395,19 @@ module RubyUnits
|
|
372
395
|
#
|
373
396
|
# @param definition [RubyUnits::Unit::Definition]
|
374
397
|
def self.use_definition(definition)
|
375
|
-
|
376
|
-
|
398
|
+
@unit_match_regex = nil # invalidate the unit match regex
|
399
|
+
@temp_regex = nil # invalidate the temp regex
|
377
400
|
if definition.prefix?
|
378
|
-
|
379
|
-
definition.aliases.each {
|
380
|
-
|
401
|
+
prefix_values[definition.name] = definition.scalar
|
402
|
+
definition.aliases.each { prefix_map[_1] = definition.name }
|
403
|
+
@prefix_regex = nil # invalidate the prefix regex
|
381
404
|
else
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
definition.aliases.each {
|
387
|
-
|
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
|
388
411
|
end
|
389
412
|
end
|
390
413
|
|
@@ -513,7 +536,7 @@ module RubyUnits
|
|
513
536
|
raise ArgumentError, 'Invalid Unit Format'
|
514
537
|
end
|
515
538
|
update_base_scalar
|
516
|
-
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?
|
517
540
|
|
518
541
|
unary_unit = units || ''
|
519
542
|
if options.first.instance_of?(String)
|
@@ -535,7 +558,7 @@ module RubyUnits
|
|
535
558
|
# return the kind of the unit (:mass, :length, etc...)
|
536
559
|
# @return [Symbol]
|
537
560
|
def kind
|
538
|
-
|
561
|
+
self.class.kinds[signature]
|
539
562
|
end
|
540
563
|
|
541
564
|
# Convert the unit to a Unit, possibly performing a conversion.
|
@@ -558,8 +581,8 @@ module RubyUnits
|
|
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
|
|
@@ -572,8 +595,8 @@ module RubyUnits
|
|
572
595
|
def to_base
|
573
596
|
return self if base?
|
574
597
|
|
575
|
-
if
|
576
|
-
@signature =
|
598
|
+
if self.class.unit_map[units] =~ /\A<(?:temp|deg)[CRF]>\Z/
|
599
|
+
@signature = self.class.kinds.key(:temperature)
|
577
600
|
base = if temperature?
|
578
601
|
convert_to('tempK')
|
579
602
|
elsif degree?
|
@@ -589,21 +612,21 @@ module RubyUnits
|
|
589
612
|
den = []
|
590
613
|
q = Rational(1)
|
591
614
|
@numerator.compact.each do |num_unit|
|
592
|
-
if
|
593
|
-
q *=
|
615
|
+
if self.class.prefix_values[num_unit]
|
616
|
+
q *= self.class.prefix_values[num_unit]
|
594
617
|
else
|
595
|
-
q *=
|
596
|
-
num <<
|
597
|
-
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]
|
598
621
|
end
|
599
622
|
end
|
600
623
|
@denominator.compact.each do |num_unit|
|
601
|
-
if
|
602
|
-
q /=
|
624
|
+
if self.class.prefix_values[num_unit]
|
625
|
+
q /= self.class.prefix_values[num_unit]
|
603
626
|
else
|
604
|
-
q /=
|
605
|
-
den <<
|
606
|
-
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]
|
607
630
|
end
|
608
631
|
end
|
609
632
|
|
@@ -632,27 +655,34 @@ module RubyUnits
|
|
632
655
|
#
|
633
656
|
# @note Rational scalars that are equal to an integer will be represented as integers (i.e, 6/1 => 6, 4/2 => 2, etc..)
|
634
657
|
# @param [Symbol] target_units
|
658
|
+
# @param [Float] precision - the precision to use when converting to a rational
|
635
659
|
# @return [String]
|
636
|
-
def to_s(target_units = nil)
|
660
|
+
def to_s(target_units = nil, precision: 0.0001)
|
637
661
|
out = @output[target_units]
|
638
662
|
return out if out
|
639
663
|
|
640
664
|
separator = RubyUnits.configuration.separator
|
641
665
|
case target_units
|
642
666
|
when :ft
|
643
|
-
inches = convert_to('in').scalar.
|
644
|
-
|
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}\""
|
645
671
|
when :lbs
|
646
|
-
ounces = convert_to('oz').scalar.
|
647
|
-
|
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"
|
648
676
|
when :stone
|
649
|
-
pounds = convert_to('lbs').scalar.
|
650
|
-
|
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"
|
651
681
|
when String
|
652
682
|
out = case target_units.strip
|
653
683
|
when /\A\s*\Z/ # whitespace only
|
654
684
|
''
|
655
|
-
when /(%[
|
685
|
+
when /(%[-+.\w#]+)\s*(.+)*/ # format string like '%0.2f in'
|
656
686
|
begin
|
657
687
|
if Regexp.last_match(2) # unit specified, need to convert
|
658
688
|
convert_to(Regexp.last_match(2)).to_s(Regexp.last_match(1))
|
@@ -713,7 +743,7 @@ module RubyUnits
|
|
713
743
|
def temperature_scale
|
714
744
|
return nil unless temperature?
|
715
745
|
|
716
|
-
"deg#{
|
746
|
+
"deg#{self.class.unit_map[units][/temp([CFRK])/, 1]}"
|
717
747
|
end
|
718
748
|
|
719
749
|
# returns true if no associated units
|
@@ -771,9 +801,10 @@ module RubyUnits
|
|
771
801
|
end
|
772
802
|
end
|
773
803
|
|
774
|
-
#
|
775
|
-
# this
|
776
|
-
#
|
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
|
+
#
|
777
808
|
# @example this permits a syntax like:
|
778
809
|
# unit =~ "mm"
|
779
810
|
# @note if you want to do a regexp comparison of the unit string do this ...
|
@@ -781,17 +812,12 @@ module RubyUnits
|
|
781
812
|
# @param [Object] other
|
782
813
|
# @return [Boolean]
|
783
814
|
def =~(other)
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
x =~ y
|
791
|
-
rescue ArgumentError
|
792
|
-
false
|
793
|
-
end
|
794
|
-
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
|
795
821
|
end
|
796
822
|
|
797
823
|
alias compatible? =~
|
@@ -941,27 +967,50 @@ module RubyUnits
|
|
941
967
|
end
|
942
968
|
end
|
943
969
|
|
944
|
-
#
|
945
|
-
#
|
946
|
-
#
|
947
|
-
# @
|
948
|
-
# @
|
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
|
949
986
|
def divmod(other)
|
950
|
-
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless
|
951
|
-
return scalar.divmod(other.scalar) if units == other.units
|
987
|
+
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other)
|
952
988
|
|
953
|
-
|
989
|
+
[quo(other).to_base.floor, self % other]
|
954
990
|
end
|
955
991
|
|
956
|
-
#
|
957
|
-
#
|
992
|
+
# Perform a modulo on a unit, will raise an exception if the units are not compatible
|
993
|
+
#
|
994
|
+
# @param [Unit] other
|
958
995
|
# @return [Integer]
|
996
|
+
# @raise [ArgumentError] if units are not compatible
|
959
997
|
def %(other)
|
960
|
-
|
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)
|
1001
|
+
end
|
1002
|
+
alias modulo %
|
1003
|
+
|
1004
|
+
# @param [Object] other
|
1005
|
+
# @return [Unit]
|
1006
|
+
# @raise [ZeroDivisionError] if other is zero
|
1007
|
+
def quo(other)
|
1008
|
+
self / other
|
961
1009
|
end
|
1010
|
+
alias fdiv quo
|
962
1011
|
|
963
1012
|
# Exponentiation. Only takes integer powers.
|
964
|
-
# Note that anything raised to the power of 0 results in a Unit object with a scalar of 1, and no units.
|
1013
|
+
# Note that anything raised to the power of 0 results in a [Unit] object with a scalar of 1, and no units.
|
965
1014
|
# Throws an exception if exponent is not an integer.
|
966
1015
|
# Ideally this routine should accept a float for the exponent
|
967
1016
|
# It should then convert the float to a rational and raise the unit by the numerator and root it by the denominator
|
@@ -990,7 +1039,7 @@ module RubyUnits
|
|
990
1039
|
when Float
|
991
1040
|
return self**other.to_i if other == other.to_i
|
992
1041
|
|
993
|
-
valid = (1..9).map {
|
1042
|
+
valid = (1..9).map { Rational(1, _1) }
|
994
1043
|
raise ArgumentError, 'Not a n-th root (1..9), use 1/n' unless valid.include? other.abs
|
995
1044
|
|
996
1045
|
root(Rational(1, other).to_int)
|
@@ -1029,23 +1078,23 @@ module RubyUnits
|
|
1029
1078
|
raise ArgumentError, 'Exponent must an Integer' unless n.is_a?(Integer)
|
1030
1079
|
raise ArgumentError, '0th root undefined' if n.zero?
|
1031
1080
|
return self if n == 1
|
1032
|
-
return root(n.abs).inverse if n
|
1081
|
+
return root(n.abs).inverse if n.negative?
|
1033
1082
|
|
1034
1083
|
vec = unit_signature_vector
|
1035
|
-
vec = vec.map {
|
1084
|
+
vec = vec.map { _1 % n }
|
1036
1085
|
raise ArgumentError, 'Illegal root' unless vec.max.zero?
|
1037
1086
|
|
1038
1087
|
num = @numerator.dup
|
1039
1088
|
den = @denominator.dup
|
1040
1089
|
|
1041
1090
|
@numerator.uniq.each do |item|
|
1042
|
-
x = num.find_all {
|
1091
|
+
x = num.find_all { _1 == item }.size
|
1043
1092
|
r = ((x / n) * (n - 1)).to_int
|
1044
1093
|
r.times { num.delete_at(num.index(item)) }
|
1045
1094
|
end
|
1046
1095
|
|
1047
1096
|
@denominator.uniq.each do |item|
|
1048
|
-
x = den.find_all {
|
1097
|
+
x = den.find_all { _1 == item }.size
|
1049
1098
|
r = ((x / n) * (n - 1)).to_int
|
1050
1099
|
r.times { den.delete_at(den.index(item)) }
|
1051
1100
|
end
|
@@ -1102,7 +1151,7 @@ module RubyUnits
|
|
1102
1151
|
return self if target_unit == start_unit
|
1103
1152
|
|
1104
1153
|
# @type [Numeric]
|
1105
|
-
@base_scalar ||= case
|
1154
|
+
@base_scalar ||= case self.class.unit_map[start_unit]
|
1106
1155
|
when '<tempC>'
|
1107
1156
|
@scalar + 273.15
|
1108
1157
|
when '<tempK>'
|
@@ -1113,7 +1162,7 @@ module RubyUnits
|
|
1113
1162
|
@scalar.to_r * Rational(5, 9)
|
1114
1163
|
end
|
1115
1164
|
# @type [Numeric]
|
1116
|
-
q = case
|
1165
|
+
q = case self.class.unit_map[target_unit]
|
1117
1166
|
when '<tempC>'
|
1118
1167
|
@base_scalar - 273.15
|
1119
1168
|
when '<tempK>'
|
@@ -1138,10 +1187,10 @@ module RubyUnits
|
|
1138
1187
|
|
1139
1188
|
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless self =~ target
|
1140
1189
|
|
1141
|
-
numerator1 = @numerator.map {
|
1142
|
-
denominator1 = @denominator.map {
|
1143
|
-
numerator2 = target.numerator.map {
|
1144
|
-
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
|
1145
1194
|
|
1146
1195
|
# If the scalar is an Integer, convert it to a Rational number so that
|
1147
1196
|
# if the value is scaled during conversion, resolution is not lost due
|
@@ -1216,26 +1265,26 @@ module RubyUnits
|
|
1216
1265
|
den = @denominator.clone.compact
|
1217
1266
|
|
1218
1267
|
unless num == UNITY_ARRAY
|
1219
|
-
definitions = num.map {
|
1268
|
+
definitions = num.map { self.class.definition(_1) }
|
1220
1269
|
definitions.reject!(&:prefix?) unless with_prefix
|
1221
1270
|
definitions = definitions.chunk_while { |definition, _| definition.prefix? }.to_a
|
1222
|
-
output_numerator = definitions.map {
|
1271
|
+
output_numerator = definitions.map { _1.map(&:display_name).join }
|
1223
1272
|
end
|
1224
1273
|
|
1225
1274
|
unless den == UNITY_ARRAY
|
1226
|
-
definitions = den.map {
|
1275
|
+
definitions = den.map { self.class.definition(_1) }
|
1227
1276
|
definitions.reject!(&:prefix?) unless with_prefix
|
1228
1277
|
definitions = definitions.chunk_while { |definition, _| definition.prefix? }.to_a
|
1229
|
-
output_denominator = definitions.map {
|
1278
|
+
output_denominator = definitions.map { _1.map(&:display_name).join }
|
1230
1279
|
end
|
1231
1280
|
|
1232
1281
|
on = output_numerator
|
1233
1282
|
.uniq
|
1234
|
-
.map {
|
1283
|
+
.map { [_1, output_numerator.count(_1)] }
|
1235
1284
|
.map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }
|
1236
1285
|
od = output_denominator
|
1237
1286
|
.uniq
|
1238
|
-
.map {
|
1287
|
+
.map { [_1, output_denominator.count(_1)] }
|
1239
1288
|
.map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }
|
1240
1289
|
"#{on.join('*')}#{od.empty? ? '' : "/#{od.join('*')}"}".strip
|
1241
1290
|
end
|
@@ -1414,19 +1463,16 @@ module RubyUnits
|
|
1414
1463
|
alias after from
|
1415
1464
|
alias from_now from
|
1416
1465
|
|
1417
|
-
#
|
1418
|
-
#
|
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
|
+
#
|
1419
1469
|
# @param other [Object, #to_unit]
|
1420
|
-
# @return [Array]
|
1470
|
+
# @return [Array(Unit, Unit)]
|
1471
|
+
# @raise [ArgumentError] when `other` cannot be converted to a [Unit]
|
1421
1472
|
def coerce(other)
|
1422
|
-
return [other.to_unit, self] if other.respond_to?
|
1473
|
+
return [other.to_unit, self] if other.respond_to?(:to_unit)
|
1423
1474
|
|
1424
|
-
|
1425
|
-
when Unit
|
1426
|
-
[other, self]
|
1427
|
-
else
|
1428
|
-
[self.class.new(other), self]
|
1429
|
-
end
|
1475
|
+
[self.class.new(other), self]
|
1430
1476
|
end
|
1431
1477
|
|
1432
1478
|
# returns a new unit that has been scaled to be more in line with typical usage.
|
@@ -1434,11 +1480,11 @@ module RubyUnits
|
|
1434
1480
|
return to_base if scalar.zero?
|
1435
1481
|
|
1436
1482
|
best_prefix = if kind == :information
|
1437
|
-
|
1483
|
+
self.class.prefix_values.key(2**((::Math.log(base_scalar, 2) / 10.0).floor * 10))
|
1438
1484
|
else
|
1439
|
-
|
1485
|
+
self.class.prefix_values.key(10**((::Math.log10(base_scalar) / 3.0).floor * 3))
|
1440
1486
|
end
|
1441
|
-
to(self.class.new(
|
1487
|
+
to(self.class.new(self.class.prefix_map.key(best_prefix) + units(with_prefix: false)))
|
1442
1488
|
end
|
1443
1489
|
|
1444
1490
|
# override hash method so objects with same values are considered equal
|
@@ -1475,18 +1521,19 @@ module RubyUnits
|
|
1475
1521
|
# @raise [ArgumentError] when exponent associated with a unit is > 20 or < -20
|
1476
1522
|
def unit_signature_vector
|
1477
1523
|
return to_base.unit_signature_vector unless base?
|
1524
|
+
|
1478
1525
|
vector = ::Array.new(SIGNATURE_VECTOR.size, 0)
|
1479
1526
|
# it's possible to have a kind that misses the array... kinds like :counting
|
1480
1527
|
# are more like prefixes, so don't use them to calculate the vector
|
1481
|
-
@numerator.map {
|
1528
|
+
@numerator.map { self.class.definition(_1) }.each do |definition|
|
1482
1529
|
index = SIGNATURE_VECTOR.index(definition.kind)
|
1483
1530
|
vector[index] += 1 if index
|
1484
1531
|
end
|
1485
|
-
@denominator.map {
|
1532
|
+
@denominator.map { self.class.definition(_1) }.each do |definition|
|
1486
1533
|
index = SIGNATURE_VECTOR.index(definition.kind)
|
1487
1534
|
vector[index] -= 1 if index
|
1488
1535
|
end
|
1489
|
-
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 }
|
1490
1537
|
|
1491
1538
|
vector
|
1492
1539
|
end
|
@@ -1534,27 +1581,45 @@ module RubyUnits
|
|
1534
1581
|
unit_string = "#{Regexp.last_match(1)} USD" if unit_string =~ /\$\s*(#{NUMBER_REGEX})/
|
1535
1582
|
unit_string.gsub!("\u00b0".force_encoding('utf-8'), 'deg') if unit_string.encoding == Encoding::UTF_8
|
1536
1583
|
|
1537
|
-
unit_string.gsub!(/[
|
1584
|
+
unit_string.gsub!(/(\d)[_,](\d)/, '\1\2') # remove underscores and commas in numbers
|
1538
1585
|
|
1539
|
-
|
1540
|
-
|
1541
|
-
|
1586
|
+
unit_string.gsub!(/[%'"#]/, '%' => 'percent', "'" => 'feet', '"' => 'inch', '#' => 'pound')
|
1587
|
+
if unit_string.start_with?(COMPLEX_NUMBER)
|
1588
|
+
match = unit_string.match(COMPLEX_REGEX)
|
1589
|
+
real = Float(match[:real]) if match[:real]
|
1590
|
+
imaginary = Float(match[:imaginary])
|
1591
|
+
unit_s = match[:unit]
|
1592
|
+
real = real.to_i if real.to_i == real
|
1593
|
+
imaginary = imaginary.to_i if imaginary.to_i == imaginary
|
1594
|
+
complex = Complex(real || 0, imaginary)
|
1595
|
+
complex = complex.to_i if complex.imaginary.zero? && complex.real == complex.real.to_i
|
1596
|
+
result = self.class.new(unit_s || 1) * complex
|
1542
1597
|
copy(result)
|
1543
1598
|
return
|
1544
1599
|
end
|
1545
1600
|
|
1546
|
-
if
|
1547
|
-
|
1548
|
-
|
1549
|
-
|
1550
|
-
|
1601
|
+
if unit_string.start_with?(RATIONAL_NUMBER)
|
1602
|
+
match = unit_string.match(RATIONAL_REGEX)
|
1603
|
+
numerator = Integer(match[:numerator])
|
1604
|
+
denominator = Integer(match[:denominator])
|
1605
|
+
raise ArgumentError, 'Improper fractions must have a whole number part' if !match[:proper].nil? && !match[:proper].match?(/^#{INTEGER_REGEX}$/)
|
1606
|
+
|
1607
|
+
proper = match[:proper].to_i
|
1608
|
+
unit_s = match[:unit]
|
1609
|
+
rational = if proper.negative?
|
1610
|
+
(proper - Rational(numerator, denominator))
|
1611
|
+
else
|
1612
|
+
(proper + Rational(numerator, denominator))
|
1613
|
+
end
|
1614
|
+
rational = rational.to_int if rational.to_int == rational
|
1615
|
+
result = self.class.new(unit_s || 1) * rational
|
1551
1616
|
copy(result)
|
1552
1617
|
return
|
1553
1618
|
end
|
1554
1619
|
|
1555
|
-
|
1556
|
-
unit = self.class.cached.get(
|
1557
|
-
mult =
|
1620
|
+
match = unit_string.match(NUMBER_REGEX)
|
1621
|
+
unit = self.class.cached.get(match[:unit])
|
1622
|
+
mult = match[:scalar] == '' ? 1.0 : match[:scalar].to_f
|
1558
1623
|
mult = mult.to_int if mult.to_int == mult
|
1559
1624
|
|
1560
1625
|
if unit
|
@@ -1564,52 +1629,70 @@ module RubyUnits
|
|
1564
1629
|
return self
|
1565
1630
|
end
|
1566
1631
|
|
1567
|
-
while unit_string.gsub!(/(<#{
|
1632
|
+
while unit_string.gsub!(/(<#{self.class.unit_regex})><(#{self.class.unit_regex}>)/, '\1*\2')
|
1568
1633
|
# collapse <x><y><z> into <x*y*z>...
|
1569
1634
|
end
|
1570
1635
|
# ... and then strip the remaining brackets for x*y*z
|
1571
1636
|
unit_string.gsub!(/[<>]/, '')
|
1572
1637
|
|
1573
|
-
if
|
1574
|
-
hours
|
1575
|
-
|
1638
|
+
if (match = unit_string.match(TIME_REGEX))
|
1639
|
+
hours = match[:hour]
|
1640
|
+
minutes = match[:min]
|
1641
|
+
seconds = match[:sec]
|
1642
|
+
milliseconds = match[:msec]
|
1643
|
+
raise ArgumentError, 'Invalid Duration' if [hours, minutes, seconds, milliseconds].all?(&:nil?)
|
1576
1644
|
|
1577
|
-
result = self.class.new("#{hours || 0}
|
1645
|
+
result = self.class.new("#{hours || 0} hours") +
|
1578
1646
|
self.class.new("#{minutes || 0} minutes") +
|
1579
1647
|
self.class.new("#{seconds || 0} seconds") +
|
1580
|
-
self.class.new("#{
|
1648
|
+
self.class.new("#{milliseconds || 0} milliseconds")
|
1581
1649
|
copy(result)
|
1582
1650
|
return
|
1583
1651
|
end
|
1584
1652
|
|
1585
1653
|
# Special processing for unusual unit strings
|
1586
1654
|
# feet -- 6'5"
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1655
|
+
if (match = unit_string.match(FEET_INCH_REGEX))
|
1656
|
+
feet = Integer(match[:feet])
|
1657
|
+
inches = match[:inches]
|
1658
|
+
result = if feet.negative?
|
1659
|
+
self.class.new("#{feet} ft") - self.class.new("#{inches} inches")
|
1660
|
+
else
|
1661
|
+
self.class.new("#{feet} ft") + self.class.new("#{inches} inches")
|
1662
|
+
end
|
1590
1663
|
copy(result)
|
1591
1664
|
return
|
1592
1665
|
end
|
1593
1666
|
|
1594
1667
|
# weight -- 8 lbs 12 oz
|
1595
|
-
|
1596
|
-
|
1597
|
-
|
1668
|
+
if (match = unit_string.match(LBS_OZ_REGEX))
|
1669
|
+
pounds = Integer(match[:pounds])
|
1670
|
+
oz = match[:oz]
|
1671
|
+
result = if pounds.negative?
|
1672
|
+
self.class.new("#{pounds} lbs") - self.class.new("#{oz} oz")
|
1673
|
+
else
|
1674
|
+
self.class.new("#{pounds} lbs") + self.class.new("#{oz} oz")
|
1675
|
+
end
|
1598
1676
|
copy(result)
|
1599
1677
|
return
|
1600
1678
|
end
|
1601
1679
|
|
1602
1680
|
# stone -- 3 stone 5, 2 stone, 14 stone 3 pounds, etc.
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1681
|
+
if (match = unit_string.match(STONE_LB_REGEX))
|
1682
|
+
stone = Integer(match[:stone])
|
1683
|
+
pounds = match[:pounds]
|
1684
|
+
result = if stone.negative?
|
1685
|
+
self.class.new("#{stone} stone") - self.class.new("#{pounds} lbs")
|
1686
|
+
else
|
1687
|
+
self.class.new("#{stone} stone") + self.class.new("#{pounds} lbs")
|
1688
|
+
end
|
1606
1689
|
copy(result)
|
1607
1690
|
return
|
1608
1691
|
end
|
1609
1692
|
|
1610
1693
|
# more than one per. I.e., "1 m/s/s"
|
1611
1694
|
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1
|
1612
|
-
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string =~ /\s[02-9]/
|
1695
|
+
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized #{unit_string}") if unit_string =~ /\s[02-9]/
|
1613
1696
|
|
1614
1697
|
@scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] # parse the string into parts
|
1615
1698
|
top.scan(TOP_REGEX).each do |item|
|
@@ -1617,7 +1700,7 @@ module RubyUnits
|
|
1617
1700
|
x = "#{item[0]} "
|
1618
1701
|
if n >= 0
|
1619
1702
|
top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) { x * n }
|
1620
|
-
elsif n
|
1703
|
+
elsif n.negative?
|
1621
1704
|
bottom = "#{bottom} #{x * -n}"
|
1622
1705
|
top.gsub!(/#{item[0]}(\^|\*\*)#{n}/, '')
|
1623
1706
|
end
|
@@ -1652,11 +1735,11 @@ module RubyUnits
|
|
1652
1735
|
raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless used.empty?
|
1653
1736
|
|
1654
1737
|
@numerator = @numerator.map do |item|
|
1655
|
-
|
1738
|
+
self.class.prefix_map[item[0]] ? [self.class.prefix_map[item[0]], self.class.unit_map[item[1]]] : [self.class.unit_map[item[1]]]
|
1656
1739
|
end.flatten.compact.delete_if(&:empty?)
|
1657
1740
|
|
1658
1741
|
@denominator = @denominator.map do |item|
|
1659
|
-
|
1742
|
+
self.class.prefix_map[item[0]] ? [self.class.prefix_map[item[0]], self.class.unit_map[item[1]]] : [self.class.unit_map[item[1]]]
|
1660
1743
|
end.flatten.compact.delete_if(&:empty?)
|
1661
1744
|
|
1662
1745
|
@numerator = UNITY_ARRAY if @numerator.empty?
|