ruby-units 3.0.0 → 4.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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?
|