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.
@@ -1,6 +1,6 @@
1
1
  require 'date'
2
2
  module RubyUnits
3
- # Copyright 2006-2022
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
- @@definitions = {}
26
- @@prefix_values = {}
27
- @@prefix_map = {}
28
- @@unit_map = {}
29
- @@unit_values = {}
30
- @@unit_regex = nil
31
- @@unit_match_regex = nil
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*(\d+)\s*(?:"|in|inch(?:es)?)/.freeze
38
- FEET_INCH_REGEX = /(\d+)\s*#{FEET_INCH_UNITS_REGEX}/.freeze
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,]*(\d+)\s*(?:ozs?|ounces?)/.freeze
43
- LBS_OZ_REGEX = /(\d+)\s*#{LBS_OZ_UNIT_REGEX}/.freeze
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,]*(\d+)\s*(?:#|lbs?|pounds?|pound-mass)*/.freeze
49
- STONE_LB_REGEX = /(\d+)\s*#{STONE_LB_UNIT_REGEX}/.freeze
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+):(?:(?<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
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 = /#{SCI_NUMBER}?#{SCI_NUMBER}i\b/.freeze
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 = /#{SCI_NUMBER}*\s*(.+)?/.freeze
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?(.+)?/.freeze
68
- RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(.+)?/.freeze
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
- @@temp_regex = nil
74
- SIGNATURE_VECTOR = %i[
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
- @@kinds = {
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
- @@prefix_values = {}
144
- @@prefix_map = {}
145
- @@unit_values = {}
146
- @@unit_map = {}
147
- @@unit_regex = nil
148
- @@unit_match_regex = nil
149
- @@prefix_regex = nil
150
-
151
- @@definitions.each_value do |definition|
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? { |d| d.aliases.include?(unit) }
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
- @@definitions[unit]
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
- @@definitions.delete("<#{name}>")
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
- @@definitions.delete("<#{unit}>")
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 { |unit| combined[unit] += increment }
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
- @@base_units ||= @@definitions.dup.select { |_, definition| definition.base? }.keys.map { |u| new(u) }
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
- @@unit_regex ||= @@unit_map.keys.sort_by { |unit_name| [unit_name.length, unit_name] }.reverse.join('|')
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
- @@unit_match_regex ||= /(#{prefix_regex})??(#{unit_regex})\b/
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
- @@prefix_regex ||= @@prefix_map.keys.sort_by { |prefix| [prefix.length, prefix] }.reverse.join('|')
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
- @@temp_regex ||= begin
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
- @@unit_match_regex = nil # invalidate the unit match regex
376
- @@temp_regex = nil # invalidate the temp regex
398
+ @unit_match_regex = nil # invalidate the unit match regex
399
+ @temp_regex = nil # invalidate the temp regex
377
400
  if definition.prefix?
378
- @@prefix_values[definition.name] = definition.scalar
379
- definition.aliases.each { |alias_name| @@prefix_map[alias_name] = definition.name }
380
- @@prefix_regex = nil # invalidate the prefix regex
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
- @@unit_values[definition.name] = {}
383
- @@unit_values[definition.name][:scalar] = definition.scalar
384
- @@unit_values[definition.name][:numerator] = definition.numerator if definition.numerator
385
- @@unit_values[definition.name][:denominator] = definition.denominator if definition.denominator
386
- definition.aliases.each { |alias_name| @@unit_map[alias_name] = definition.name }
387
- @@unit_regex = nil # invalidate the unit regex
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 < 0
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
- @@kinds[signature]
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 { |unit| self.class.definition(unit) }
562
- .all? { |element| element.unity? || element.base? }
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 @@unit_map[units] =~ /\A<(?:temp|deg)[CRF]>\Z/
576
- @signature = @@kinds.key(:temperature)
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 @@prefix_values[num_unit]
593
- q *= @@prefix_values[num_unit]
615
+ if self.class.prefix_values[num_unit]
616
+ q *= self.class.prefix_values[num_unit]
594
617
  else
595
- q *= @@unit_values[num_unit][:scalar] if @@unit_values[num_unit]
596
- num << @@unit_values[num_unit][:numerator] if @@unit_values[num_unit] && @@unit_values[num_unit][:numerator]
597
- den << @@unit_values[num_unit][:denominator] if @@unit_values[num_unit] && @@unit_values[num_unit][:denominator]
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 @@prefix_values[num_unit]
602
- q /= @@prefix_values[num_unit]
624
+ if self.class.prefix_values[num_unit]
625
+ q /= self.class.prefix_values[num_unit]
603
626
  else
604
- q /= @@unit_values[num_unit][:scalar] if @@unit_values[num_unit]
605
- den << @@unit_values[num_unit][:numerator] if @@unit_values[num_unit] && @@unit_values[num_unit][:numerator]
606
- num << @@unit_values[num_unit][:denominator] if @@unit_values[num_unit] && @@unit_values[num_unit][:denominator]
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.to_int
644
- out = "#{(inches / 12).truncate}\'#{(inches % 12).round}\""
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.to_int
647
- out = "#{(ounces / 16).truncate}#{separator}lbs, #{(ounces % 16).round}#{separator}oz"
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.to_int
650
- out = "#{(pounds / 14).truncate}#{separator}stone, #{(pounds % 14).round}#{separator}lb"
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 /(%[\-+.\w#]+)\s*(.+)*/ # format string like '%0.2f in'
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#{@@unit_map[units][/temp([CFRK])/, 1]}"
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
- # check to see if units are compatible, but not the scalar part
775
- # this check is done by comparing signatures for performance reasons
776
- # if passed a string, it will create a unit object with the string and then do the comparison
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
- case other
785
- when Unit
786
- signature == other.signature
787
- else
788
- begin
789
- x, y = coerce(other)
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
- # divide two units and return quotient and remainder
945
- # when both units are in the same units we just use divmod on the raw scalars
946
- # otherwise we use the scalar of the base unit which will be a float
947
- # @param [Object] other
948
- # @return [Array]
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 self =~ other
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
- to_base.scalar.divmod(other.to_base.scalar)
989
+ [quo(other).to_base.floor, self % other]
954
990
  end
955
991
 
956
- # perform a modulo on a unit, will raise an exception if the units are not compatible
957
- # @param [Object] other
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
- divmod(other).last
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 { |n| Rational(1, n) }
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 < 0
1081
+ return root(n.abs).inverse if n.negative?
1033
1082
 
1034
1083
  vec = unit_signature_vector
1035
- vec = vec.map { |x| x % n }
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 { |i| i == item }.size
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 { |i| i == item }.size
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 @@unit_map[start_unit]
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 @@unit_map[target_unit]
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 { |x| @@prefix_values[x] || x }.map { |i| i.is_a?(Numeric) ? i : @@unit_values[i][:scalar] }.compact
1142
- denominator1 = @denominator.map { |x| @@prefix_values[x] || x }.map { |i| i.is_a?(Numeric) ? i : @@unit_values[i][:scalar] }.compact
1143
- numerator2 = target.numerator.map { |x| @@prefix_values[x] || x }.map { |x| x.is_a?(Numeric) ? x : @@unit_values[x][:scalar] }.compact
1144
- denominator2 = target.denominator.map { |x| @@prefix_values[x] || x }.map { |x| x.is_a?(Numeric) ? x : @@unit_values[x][:scalar] }.compact
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 { |element| self.class.definition(element) }
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 { |element| element.map(&:display_name).join }
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 { |element| self.class.definition(element) }
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 { |element| element.map(&:display_name).join }
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 { |x| [x, output_numerator.count(x)] }
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 { |x| [x, output_denominator.count(x)] }
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
- # automatically coerce objects to units when possible
1418
- # if an object defines a 'to_unit' method, it will be coerced using that method
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? :to_unit
1473
+ return [other.to_unit, self] if other.respond_to?(:to_unit)
1423
1474
 
1424
- case other
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
- @@prefix_values.key(2**((::Math.log(base_scalar, 2) / 10.0).floor * 10))
1483
+ self.class.prefix_values.key(2**((::Math.log(base_scalar, 2) / 10.0).floor * 10))
1438
1484
  else
1439
- @@prefix_values.key(10**((::Math.log10(base_scalar) / 3.0).floor * 3))
1485
+ self.class.prefix_values.key(10**((::Math.log10(base_scalar) / 3.0).floor * 3))
1440
1486
  end
1441
- to(self.class.new(@@prefix_map.key(best_prefix) + units(with_prefix: false)))
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 { |element| self.class.definition(element) }.each do |definition|
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 { |element| self.class.definition(element) }.each do |definition|
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? { |x| x.abs >= 20 }
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!(/[%'"#]/, '%' => 'percent', "'" => 'feet', '"' => 'inch', '#' => 'pound')
1584
+ unit_string.gsub!(/(\d)[_,](\d)/, '\1\2') # remove underscores and commas in numbers
1538
1585
 
1539
- if defined?(Complex) && unit_string =~ COMPLEX_NUMBER
1540
- real, imaginary, unit_s = unit_string.scan(COMPLEX_REGEX)[0]
1541
- result = self.class.new(unit_s || '1') * Complex(real.to_f, imaginary.to_f)
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 defined?(Rational) && unit_string =~ RATIONAL_NUMBER
1547
- sign, proper, numerator, denominator, unit_s = unit_string.scan(RATIONAL_REGEX)[0]
1548
- sign = sign == '-' ? -1 : 1
1549
- rational = sign * (proper.to_i + Rational(numerator.to_i, denominator.to_i))
1550
- result = self.class.new(unit_s || '1') * rational
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
- unit_string =~ NUMBER_REGEX
1556
- unit = self.class.cached.get(Regexp.last_match(2))
1557
- mult = Regexp.last_match(1).nil? ? 1.0 : Regexp.last_match(1).to_f
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!(/(<#{@@unit_regex})><(#{@@unit_regex}>)/, '\1*\2')
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 unit_string =~ TIME_REGEX
1574
- hours, minutes, seconds, microseconds = unit_string.scan(TIME_REGEX)[0]
1575
- raise ArgumentError, 'Invalid Duration' if [hours, minutes, seconds, microseconds].all?(&:nil?)
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} h") +
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("#{microseconds || 0} usec")
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
- feet, inches = unit_string.scan(FEET_INCH_REGEX)[0]
1588
- if feet && inches
1589
- result = self.class.new("#{feet} ft") + self.class.new("#{inches} inches")
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
- pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0]
1596
- if pounds && oz
1597
- result = self.class.new("#{pounds} lbs") + self.class.new("#{oz} oz")
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
- stone, pounds = unit_string.scan(STONE_LB_REGEX)[0]
1604
- if stone && pounds
1605
- result = self.class.new("#{stone} stone") + self.class.new("#{pounds} lbs")
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 < 0
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
- @@prefix_map[item[0]] ? [@@prefix_map[item[0]], @@unit_map[item[1]]] : [@@unit_map[item[1]]]
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
- @@prefix_map[item[0]] ? [@@prefix_map[item[0]], @@unit_map[item[1]]] : [@@unit_map[item[1]]]
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?