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.
@@ -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?