ruby-units 4.0.3 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,6 @@
1
- require 'date'
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
2
4
  module RubyUnits
3
5
  # Copyright 2006-2024
4
6
  # @author Kevin C. Olbrich, Ph.D.
@@ -49,7 +51,7 @@ module RubyUnits
49
51
  self.unit_values = {}
50
52
  @unit_regex = nil
51
53
  @unit_match_regex = nil
52
- UNITY = '<1>'.freeze
54
+ UNITY = "<1>"
53
55
  UNITY_ARRAY = [UNITY].freeze
54
56
 
55
57
  SIGN_REGEX = /(?:[+-])?/.freeze # +, -, or nothing
@@ -61,10 +63,10 @@ module RubyUnits
61
63
  DIGITS_REGEX = /\d+/.freeze # 0, 1, 2, 3
62
64
  DECIMAL_REGEX = /\d*[.]?#{DIGITS_REGEX}/.freeze # 1, 0.1, .1
63
65
  # 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.
66
+ RATIONAL_NUMBER = %r{\(?(?:(?<proper>#{SIGN_REGEX}#{DECIMAL_REGEX})[ -])?(?<numerator>#{SIGN_REGEX}#{DECIMAL_REGEX})/(?<denominator>#{SIGN_REGEX}#{DECIMAL_REGEX})\)?}.freeze # 1 2/3, -1 2/3, 5/3, 1-2/3, (1/2) etc.
65
67
  # Scientific notation: 1, -1, +1, 1.2, +1.2, -1.2, 123.4E5, +123.4e5,
66
68
  # -123.4E+5, -123.4e-5, etc.
67
- SCI_NUMBER = /([+-]?\d*[.]?\d+(?:[Ee][+-]?\d+(?![.]))?)/
69
+ SCI_NUMBER = /([+-]?\d*[.]?\d+(?:[Ee][+-]?\d+(?![.]))?)/.freeze
68
70
  # ideally we would like to generate this regex from the alias for a 'feet'
69
71
  # and 'inches', but they aren't defined at the point in the code where we
70
72
  # need this regex.
@@ -95,10 +97,10 @@ module RubyUnits
95
97
  NUMBER_UNIT_REGEX = /#{SCI_NUMBER}?(.*)/.freeze
96
98
  COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(?<unit>.+)?/.freeze
97
99
  RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(?<unit>.+)?/.freeze
98
- KELVIN = ['<kelvin>'].freeze
99
- FAHRENHEIT = ['<fahrenheit>'].freeze
100
- RANKINE = ['<rankine>'].freeze
101
- CELSIUS = ['<celsius>'].freeze
100
+ KELVIN = ["<kelvin>"].freeze
101
+ FAHRENHEIT = ["<fahrenheit>"].freeze
102
+ RANKINE = ["<rankine>"].freeze
103
+ CELSIUS = ["<celsius>"].freeze
102
104
  @temp_regex = nil
103
105
  SIGNATURE_VECTOR = %i[
104
106
  length
@@ -227,7 +229,7 @@ module RubyUnits
227
229
  # RubyUnits::Unit.define(unit_definition)
228
230
  def self.define(unit_definition, &block)
229
231
  if block_given?
230
- raise ArgumentError, 'When using the block form of RubyUnits::Unit.define, pass the name of the unit' unless unit_definition.is_a?(String)
232
+ raise ArgumentError, "When using the block form of RubyUnits::Unit.define, pass the name of the unit" unless unit_definition.is_a?(String)
231
233
 
232
234
  unit_definition = RubyUnits::Unit::Definition.new(unit_definition, &block)
233
235
  end
@@ -245,7 +247,7 @@ module RubyUnits
245
247
  # redefined
246
248
  # @return (see RubyUnits::Unit.define)
247
249
  def self.redefine!(name, &_block)
248
- raise ArgumentError, 'A block is required to redefine a unit' unless block_given?
250
+ raise ArgumentError, "A block is required to redefine a unit" unless block_given?
249
251
 
250
252
  unit_definition = definition(name)
251
253
  raise(ArgumentError, "'#{name}' Unit not recognized") unless unit_definition
@@ -356,7 +358,7 @@ module RubyUnits
356
358
  when RATIONAL_NUMBER
357
359
  # We use this method instead of relying on `to_r` because it does not
358
360
  # handle improper fractions correctly.
359
- sign = Regexp.last_match(1) == '-' ? -1 : 1
361
+ sign = Regexp.last_match(1) == "-" ? -1 : 1
360
362
  n = Regexp.last_match(2).to_i
361
363
  f = Rational(Regexp.last_match(3).to_i, Regexp.last_match(4).to_i)
362
364
  sign * (n + f)
@@ -371,7 +373,7 @@ module RubyUnits
371
373
  # Unit names are reverse sorted by length so the regexp matcher will prefer longer and more specific names
372
374
  # @return [String]
373
375
  def self.unit_regex
374
- @unit_regex ||= unit_map.keys.sort_by { [_1.length, _1] }.reverse.join('|')
376
+ @unit_regex ||= unit_map.keys.sort_by { [_1.length, _1] }.reverse.join("|")
375
377
  end
376
378
 
377
379
  # return a regex used to match units
@@ -384,7 +386,7 @@ module RubyUnits
384
386
  # @return [String]
385
387
  # @private
386
388
  def self.prefix_regex
387
- @prefix_regex ||= prefix_map.keys.sort_by { [_1.length, _1] }.reverse.join('|')
389
+ @prefix_regex ||= prefix_map.keys.sort_by { [_1.length, _1] }.reverse.join("|")
388
390
  end
389
391
 
390
392
  # Generates (and memoizes) a regexp matching any of the temperature units or their aliases.
@@ -397,7 +399,7 @@ module RubyUnits
397
399
  d = definition(unit)
398
400
  d && d.aliases
399
401
  end.flatten.compact
400
- regex_str = aliases.empty? ? '(?!x)x' : aliases.join('|')
402
+ regex_str = aliases.empty? ? "(?!x)x" : aliases.join("|")
401
403
  Regexp.new "(?:#{regex_str})"
402
404
  end
403
405
  end
@@ -491,7 +493,7 @@ module RubyUnits
491
493
  @unit_name = nil
492
494
  @signature = nil
493
495
  @output = {}
494
- raise ArgumentError, 'Invalid Unit Format' if options[0].nil?
496
+ raise ArgumentError, "Invalid Unit Format" if options[0].nil?
495
497
 
496
498
  if options.size == 2
497
499
  # options[0] is the scalar
@@ -521,7 +523,7 @@ module RubyUnits
521
523
  copy(options[0])
522
524
  return
523
525
  when Hash
524
- @scalar = (options[0][:scalar] || 1)
526
+ @scalar = options[0][:scalar] || 1
525
527
  @numerator = options[0][:numerator] || UNITY_ARRAY
526
528
  @denominator = options[0][:denominator] || UNITY_ARRAY
527
529
  @signature = options[0][:signature]
@@ -533,23 +535,23 @@ module RubyUnits
533
535
  @numerator = @denominator = UNITY_ARRAY
534
536
  when Time
535
537
  @scalar = options[0].to_f
536
- @numerator = ['<second>']
538
+ @numerator = ["<second>"]
537
539
  @denominator = UNITY_ARRAY
538
540
  when DateTime, Date
539
541
  @scalar = options[0].ajd
540
- @numerator = ['<day>']
542
+ @numerator = ["<day>"]
541
543
  @denominator = UNITY_ARRAY
542
544
  when /^\s*$/
543
- raise ArgumentError, 'No Unit Specified'
545
+ raise ArgumentError, "No Unit Specified"
544
546
  when String
545
547
  parse(options[0])
546
548
  else
547
- raise ArgumentError, 'Invalid Unit Format'
549
+ raise ArgumentError, "Invalid Unit Format"
548
550
  end
549
551
  update_base_scalar
550
- raise ArgumentError, 'Temperatures must not be less than absolute zero' if temperature? && base_scalar.negative?
552
+ raise ArgumentError, "Temperatures must not be less than absolute zero" if temperature? && base_scalar.negative?
551
553
 
552
- unary_unit = units || ''
554
+ unary_unit = units || ""
553
555
  if options.first.instance_of?(String)
554
556
  _opt_scalar, opt_units = self.class.parse_into_numbers_and_units(options[0])
555
557
  if !(self.class.cached.keys.include?(opt_units) ||
@@ -609,9 +611,9 @@ module RubyUnits
609
611
  if self.class.unit_map[units] =~ /\A<(?:temp|deg)[CRF]>\Z/
610
612
  @signature = self.class.kinds.key(:temperature)
611
613
  base = if temperature?
612
- convert_to('tempK')
614
+ convert_to("tempK")
613
615
  elsif degree?
614
- convert_to('degK')
616
+ convert_to("degK")
615
617
  end
616
618
  return base
617
619
  end
@@ -667,55 +669,57 @@ module RubyUnits
667
669
  # @note Rational scalars that are equal to an integer will be represented as integers (i.e, 6/1 => 6, 4/2 => 2, etc..)
668
670
  # @param [Symbol] target_units
669
671
  # @param [Float] precision - the precision to use when converting to a rational
672
+ # @param format [Symbol] Set to :exponential to force all units to be displayed in exponential format
673
+ #
670
674
  # @return [String]
671
- def to_s(target_units = nil, precision: 0.0001)
675
+ def to_s(target_units = nil, precision: 0.0001, format: RubyUnits.configuration.format)
672
676
  out = @output[target_units]
673
677
  return out if out
674
678
 
675
679
  separator = RubyUnits.configuration.separator
676
680
  case target_units
677
681
  when :ft
678
- feet, inches = convert_to('in').scalar.abs.divmod(12)
682
+ feet, inches = convert_to("in").scalar.abs.divmod(12)
679
683
  improper, frac = inches.divmod(1)
680
- frac = frac.zero? ? '' : "-#{frac.rationalize(precision)}"
684
+ frac = frac.zero? ? "" : "-#{frac.rationalize(precision)}"
681
685
  out = "#{negative? ? '-' : nil}#{feet}'#{improper}#{frac}\""
682
686
  when :lbs
683
- pounds, ounces = convert_to('oz').scalar.abs.divmod(16)
687
+ pounds, ounces = convert_to("oz").scalar.abs.divmod(16)
684
688
  improper, frac = ounces.divmod(1)
685
- frac = frac.zero? ? '' : "-#{frac.rationalize(precision)}"
689
+ frac = frac.zero? ? "" : "-#{frac.rationalize(precision)}"
686
690
  out = "#{negative? ? '-' : nil}#{pounds}#{separator}lbs #{improper}#{frac}#{separator}oz"
687
691
  when :stone
688
- stone, pounds = convert_to('lbs').scalar.abs.divmod(14)
692
+ stone, pounds = convert_to("lbs").scalar.abs.divmod(14)
689
693
  improper, frac = pounds.divmod(1)
690
- frac = frac.zero? ? '' : "-#{frac.rationalize(precision)}"
694
+ frac = frac.zero? ? "" : "-#{frac.rationalize(precision)}"
691
695
  out = "#{negative? ? '-' : nil}#{stone}#{separator}stone #{improper}#{frac}#{separator}lbs"
692
696
  when String
693
697
  out = case target_units.strip
694
698
  when /\A\s*\Z/ # whitespace only
695
- ''
699
+ ""
696
700
  when /(%[-+.\w#]+)\s*(.+)*/ # format string like '%0.2f in'
697
701
  begin
698
702
  if Regexp.last_match(2) # unit specified, need to convert
699
- convert_to(Regexp.last_match(2)).to_s(Regexp.last_match(1))
703
+ convert_to(Regexp.last_match(2)).to_s(Regexp.last_match(1), format: format)
700
704
  else
701
- "#{Regexp.last_match(1) % @scalar}#{separator}#{Regexp.last_match(2) || units}".strip
705
+ "#{Regexp.last_match(1) % @scalar}#{separator}#{Regexp.last_match(2) || units(format: format)}".strip
702
706
  end
703
707
  rescue StandardError # parse it like a strftime format string
704
708
  (DateTime.new(0) + self).strftime(target_units)
705
709
  end
706
710
  when /(\S+)/ # unit only 'mm' or '1/mm'
707
- convert_to(Regexp.last_match(1)).to_s
711
+ convert_to(Regexp.last_match(1)).to_s(format: format)
708
712
  else
709
- raise 'unhandled case'
713
+ raise "unhandled case"
710
714
  end
711
715
  else
712
716
  out = case @scalar
713
717
  when Complex
714
- "#{@scalar}#{separator}#{units}"
718
+ "#{@scalar}#{separator}#{units(format: format)}"
715
719
  when Rational
716
- "#{@scalar == @scalar.to_i ? @scalar.to_i : @scalar}#{separator}#{units}"
720
+ "#{@scalar == @scalar.to_i ? @scalar.to_i : @scalar}#{separator}#{units(format: format)}"
717
721
  else
718
- "#{'%g' % @scalar}#{separator}#{units}"
722
+ "#{'%g' % @scalar}#{separator}#{units(format: format)}"
719
723
  end.strip
720
724
  end
721
725
  @output[target_units] = out
@@ -761,7 +765,7 @@ module RubyUnits
761
765
  # false, even if the units are "unitless" like 'radians, each, etc'
762
766
  # @return [Boolean]
763
767
  def unitless?
764
- (@numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY)
768
+ @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
765
769
  end
766
770
 
767
771
  # Compare two Unit objects. Throws an exception if they are not of compatible types.
@@ -847,7 +851,7 @@ module RubyUnits
847
851
  else
848
852
  begin
849
853
  x, y = coerce(other)
850
- x === y
854
+ x.same_as?(y)
851
855
  rescue ArgumentError
852
856
  false
853
857
  end
@@ -871,14 +875,12 @@ module RubyUnits
871
875
  if zero?
872
876
  other.dup
873
877
  elsif self =~ other
874
- raise ArgumentError, 'Cannot add two temperatures' if [self, other].all?(&:temperature?)
878
+ raise ArgumentError, "Cannot add two temperatures" if [self, other].all?(&:temperature?)
875
879
 
876
- if [self, other].any?(&:temperature?)
877
- if temperature?
878
- self.class.new(scalar: (scalar + other.convert_to(temperature_scale).scalar), numerator: @numerator, denominator: @denominator, signature: @signature)
879
- else
880
- self.class.new(scalar: (other.scalar + convert_to(other.temperature_scale).scalar), numerator: other.numerator, denominator: other.denominator, signature: other.signature)
881
- end
880
+ if temperature?
881
+ self.class.new(scalar: (scalar + other.convert_to(temperature_scale).scalar), numerator: @numerator, denominator: @denominator, signature: @signature)
882
+ elsif other.temperature?
883
+ self.class.new(scalar: (other.scalar + convert_to(other.temperature_scale).scalar), numerator: other.numerator, denominator: other.denominator, signature: other.signature)
882
884
  else
883
885
  self.class.new(scalar: (base_scalar + other.base_scalar), numerator: base.numerator, denominator: base.denominator, signature: @signature).convert_to(self)
884
886
  end
@@ -886,7 +888,7 @@ module RubyUnits
886
888
  raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')"
887
889
  end
888
890
  when Date, Time
889
- raise ArgumentError, 'Date and Time objects represent fixed points in time and cannot be added to a Unit'
891
+ raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be added to a Unit"
890
892
  else
891
893
  x, y = coerce(other)
892
894
  y + x
@@ -912,9 +914,9 @@ module RubyUnits
912
914
  if [self, other].all?(&:temperature?)
913
915
  self.class.new(scalar: (base_scalar - other.base_scalar), numerator: KELVIN, denominator: UNITY_ARRAY, signature: @signature).convert_to(temperature_scale)
914
916
  elsif temperature?
915
- self.class.new(scalar: (base_scalar - other.base_scalar), numerator: ['<tempK>'], denominator: UNITY_ARRAY, signature: @signature).convert_to(self)
917
+ self.class.new(scalar: (base_scalar - other.base_scalar), numerator: ["<tempK>"], denominator: UNITY_ARRAY, signature: @signature).convert_to(self)
916
918
  elsif other.temperature?
917
- raise ArgumentError, 'Cannot subtract a temperature from a differential degree unit'
919
+ raise ArgumentError, "Cannot subtract a temperature from a differential degree unit"
918
920
  else
919
921
  self.class.new(scalar: (base_scalar - other.base_scalar), numerator: base.numerator, denominator: base.denominator, signature: @signature).convert_to(self)
920
922
  end
@@ -922,7 +924,7 @@ module RubyUnits
922
924
  raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')"
923
925
  end
924
926
  when Time
925
- raise ArgumentError, 'Date and Time objects represent fixed points in time and cannot be subtracted from a Unit'
927
+ raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be subtracted from a Unit"
926
928
  else
927
929
  x, y = coerce(other)
928
930
  y - x
@@ -936,7 +938,7 @@ module RubyUnits
936
938
  def *(other)
937
939
  case other
938
940
  when Unit
939
- raise ArgumentError, 'Cannot multiply by temperatures' if [other, self].any?(&:temperature?)
941
+ raise ArgumentError, "Cannot multiply by temperatures" if [other, self].any?(&:temperature?)
940
942
 
941
943
  opts = self.class.eliminate_terms(@scalar * other.scalar, @numerator + other.numerator, @denominator + other.denominator)
942
944
  opts[:signature] = @signature + other.signature
@@ -959,7 +961,7 @@ module RubyUnits
959
961
  case other
960
962
  when Unit
961
963
  raise ZeroDivisionError if other.zero?
962
- raise ArgumentError, 'Cannot divide with temperatures' if [other, self].any?(&:temperature?)
964
+ raise ArgumentError, "Cannot divide with temperatures" if [other, self].any?(&:temperature?)
963
965
 
964
966
  sc = Rational(@scalar, other.scalar)
965
967
  sc = sc.numerator if sc.denominator == 1
@@ -1035,7 +1037,7 @@ module RubyUnits
1035
1037
  # @raise [ArgumentError] when attempting to raise to a complex number
1036
1038
  # @raise [ArgumentError] when an invalid exponent is passed
1037
1039
  def **(other)
1038
- raise ArgumentError, 'Cannot raise a temperature to a power' if temperature?
1040
+ raise ArgumentError, "Cannot raise a temperature to a power" if temperature?
1039
1041
 
1040
1042
  if other.is_a?(Numeric)
1041
1043
  return inverse if other == -1
@@ -1051,13 +1053,13 @@ module RubyUnits
1051
1053
  return self**other.to_i if other == other.to_i
1052
1054
 
1053
1055
  valid = (1..9).map { Rational(1, _1) }
1054
- raise ArgumentError, 'Not a n-th root (1..9), use 1/n' unless valid.include? other.abs
1056
+ raise ArgumentError, "Not a n-th root (1..9), use 1/n" unless valid.include? other.abs
1055
1057
 
1056
1058
  root(Rational(1, other).to_int)
1057
1059
  when Complex
1058
- raise ArgumentError, 'exponentiation of complex numbers is not supported.'
1060
+ raise ArgumentError, "exponentiation of complex numbers is not supported."
1059
1061
  else
1060
- raise ArgumentError, 'Invalid Exponent'
1062
+ raise ArgumentError, "Invalid Exponent"
1061
1063
  end
1062
1064
  end
1063
1065
 
@@ -1067,8 +1069,8 @@ module RubyUnits
1067
1069
  # @raise [ArgumentError] when attempting to raise a temperature to a power
1068
1070
  # @raise [ArgumentError] when n is not an integer
1069
1071
  def power(n)
1070
- raise ArgumentError, 'Cannot raise a temperature to a power' if temperature?
1071
- raise ArgumentError, 'Exponent must an Integer' unless n.is_a?(Integer)
1072
+ raise ArgumentError, "Cannot raise a temperature to a power" if temperature?
1073
+ raise ArgumentError, "Exponent must an Integer" unless n.is_a?(Integer)
1072
1074
  return inverse if n == -1
1073
1075
  return 1 if n.zero?
1074
1076
  return self if n == 1
@@ -1085,15 +1087,15 @@ module RubyUnits
1085
1087
  # @raise [ArgumentError] when n is not an integer
1086
1088
  # @raise [ArgumentError] when n is 0
1087
1089
  def root(n)
1088
- raise ArgumentError, 'Cannot take the root of a temperature' if temperature?
1089
- raise ArgumentError, 'Exponent must an Integer' unless n.is_a?(Integer)
1090
- raise ArgumentError, '0th root undefined' if n.zero?
1090
+ raise ArgumentError, "Cannot take the root of a temperature" if temperature?
1091
+ raise ArgumentError, "Exponent must an Integer" unless n.is_a?(Integer)
1092
+ raise ArgumentError, "0th root undefined" if n.zero?
1091
1093
  return self if n == 1
1092
1094
  return root(n.abs).inverse if n.negative?
1093
1095
 
1094
1096
  vec = unit_signature_vector
1095
1097
  vec = vec.map { _1 % n }
1096
- raise ArgumentError, 'Illegal root' unless vec.max.zero?
1098
+ raise ArgumentError, "Illegal root" unless vec.max.zero?
1097
1099
 
1098
1100
  num = @numerator.dup
1099
1101
  den = @denominator.dup
@@ -1115,7 +1117,7 @@ module RubyUnits
1115
1117
  # returns inverse of Unit (1/unit)
1116
1118
  # @return [Unit]
1117
1119
  def inverse
1118
- self.class.new('1') / self
1120
+ self.class.new("1") / self
1119
1121
  end
1120
1122
 
1121
1123
  # convert to a specified unit string or to the same units as another Unit
@@ -1143,11 +1145,11 @@ module RubyUnits
1143
1145
  # @raise [ArgumentError] when target unit is incompatible
1144
1146
  def convert_to(other)
1145
1147
  return self if other.nil?
1146
- return self if TrueClass === other
1147
- return self if FalseClass === other
1148
+ return self if other.is_a?(TrueClass)
1149
+ return self if other.is_a?(FalseClass)
1148
1150
 
1149
1151
  if (other.is_a?(Unit) && other.temperature?) || (other.is_a?(String) && other =~ self.class.temp_regex)
1150
- raise ArgumentError, 'Receiver is not a temperature unit' unless degree?
1152
+ raise ArgumentError, "Receiver is not a temperature unit" unless degree?
1151
1153
 
1152
1154
  start_unit = units
1153
1155
  # @type [String]
@@ -1157,30 +1159,30 @@ module RubyUnits
1157
1159
  when String
1158
1160
  other
1159
1161
  else
1160
- raise ArgumentError, 'Unknown target units'
1162
+ raise ArgumentError, "Unknown target units"
1161
1163
  end
1162
1164
  return self if target_unit == start_unit
1163
1165
 
1164
1166
  # @type [Numeric]
1165
1167
  @base_scalar ||= case self.class.unit_map[start_unit]
1166
- when '<tempC>'
1168
+ when "<tempC>"
1167
1169
  @scalar + 273.15
1168
- when '<tempK>'
1170
+ when "<tempK>"
1169
1171
  @scalar
1170
- when '<tempF>'
1172
+ when "<tempF>"
1171
1173
  (@scalar + 459.67).to_r * Rational(5, 9)
1172
- when '<tempR>'
1174
+ when "<tempR>"
1173
1175
  @scalar.to_r * Rational(5, 9)
1174
1176
  end
1175
1177
  # @type [Numeric]
1176
1178
  q = case self.class.unit_map[target_unit]
1177
- when '<tempC>'
1179
+ when "<tempC>"
1178
1180
  @base_scalar - 273.15
1179
- when '<tempK>'
1181
+ when "<tempK>"
1180
1182
  @base_scalar
1181
- when '<tempF>'
1183
+ when "<tempF>"
1182
1184
  (@base_scalar.to_r * Rational(9, 5)) - 459.67r
1183
- when '<tempR>'
1185
+ when "<tempR>"
1184
1186
  @base_scalar.to_r * Rational(9, 5)
1185
1187
  end
1186
1188
  self.class.new("#{q} #{target_unit}")
@@ -1192,7 +1194,7 @@ module RubyUnits
1192
1194
  when String
1193
1195
  self.class.new(other)
1194
1196
  else
1195
- raise ArgumentError, 'Unknown target units'
1197
+ raise ArgumentError, "Unknown target units"
1196
1198
  end
1197
1199
  return self if target.units == units
1198
1200
 
@@ -1266,11 +1268,13 @@ module RubyUnits
1266
1268
  # Returns the 'unit' part of the Unit object without the scalar
1267
1269
  #
1268
1270
  # @param with_prefix [Boolean] include prefixes in output
1271
+ # @param format [Symbol] Set to :exponential to force all units to be displayed in exponential format
1272
+ #
1269
1273
  # @return [String]
1270
- def units(with_prefix: true)
1271
- return '' if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
1274
+ def units(with_prefix: true, format: nil)
1275
+ return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
1272
1276
 
1273
- output_numerator = ['1']
1277
+ output_numerator = ["1"]
1274
1278
  output_denominator = []
1275
1279
  num = @numerator.clone.compact
1276
1280
  den = @denominator.clone.compact
@@ -1289,15 +1293,24 @@ module RubyUnits
1289
1293
  output_denominator = definitions.map { _1.map(&:display_name).join }
1290
1294
  end
1291
1295
 
1292
- on = output_numerator
1293
- .uniq
1294
- .map { [_1, output_numerator.count(_1)] }
1295
- .map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }
1296
- od = output_denominator
1297
- .uniq
1298
- .map { [_1, output_denominator.count(_1)] }
1299
- .map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }
1300
- "#{on.join('*')}#{od.empty? ? '' : "/#{od.join('*')}"}".strip
1296
+ on = output_numerator
1297
+ .uniq
1298
+ .map { [_1, output_numerator.count(_1)] }
1299
+ .map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : "")) }
1300
+
1301
+ if format == :exponential
1302
+ od = output_denominator
1303
+ .uniq
1304
+ .map { [_1, output_denominator.count(_1)] }
1305
+ .map { |element, power| (element.to_s.strip + (power.positive? ? "^#{-power}" : "")) }
1306
+ (on + od).join("*").strip
1307
+ else
1308
+ od = output_denominator
1309
+ .uniq
1310
+ .map { [_1, output_denominator.count(_1)] }
1311
+ .map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : "")) }
1312
+ "#{on.join('*')}#{od.empty? ? '' : "/#{od.join('*')}"}".strip
1313
+ end
1301
1314
  end
1302
1315
 
1303
1316
  # negates the scalar of the Unit
@@ -1359,7 +1372,7 @@ module RubyUnits
1359
1372
  # @return [Unit]
1360
1373
  # @raise [ArgumentError] when scalar is not equal to an integer
1361
1374
  def succ
1362
- raise ArgumentError, 'Non Integer Scalar' unless @scalar == @scalar.to_i
1375
+ raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
1363
1376
 
1364
1377
  self.class.new(@scalar.to_i.succ, @numerator, @denominator)
1365
1378
  end
@@ -1371,7 +1384,7 @@ module RubyUnits
1371
1384
  # @return [Unit]
1372
1385
  # @raise [ArgumentError] when scalar is not equal to an integer
1373
1386
  def pred
1374
- raise ArgumentError, 'Non Integer Scalar' unless @scalar == @scalar.to_i
1387
+ raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i
1375
1388
 
1376
1389
  self.class.new(@scalar.to_i.pred, @numerator, @denominator)
1377
1390
  end
@@ -1388,12 +1401,12 @@ module RubyUnits
1388
1401
  # defined by DateTime
1389
1402
  # @return [::DateTime]
1390
1403
  def to_datetime
1391
- DateTime.new!(convert_to('d').scalar)
1404
+ DateTime.new!(convert_to("d").scalar)
1392
1405
  end
1393
1406
 
1394
1407
  # @return [Date]
1395
1408
  def to_date
1396
- Date.new0(convert_to('d').scalar)
1409
+ Date.new0(convert_to("d").scalar)
1397
1410
  end
1398
1411
 
1399
1412
  # true if scalar is zero
@@ -1419,7 +1432,7 @@ module RubyUnits
1419
1432
  time_point.to_datetime - self
1420
1433
  end)
1421
1434
  else
1422
- raise ArgumentError, 'Must specify a Time, Date, or DateTime'
1435
+ raise ArgumentError, "Must specify a Time, Date, or DateTime"
1423
1436
  end
1424
1437
  end
1425
1438
 
@@ -1432,11 +1445,11 @@ module RubyUnits
1432
1445
  def since(time_point)
1433
1446
  case time_point
1434
1447
  when Time
1435
- self.class.new(::Time.now - time_point, 'second').convert_to(self)
1448
+ self.class.new(::Time.now - time_point, "second").convert_to(self)
1436
1449
  when DateTime, Date
1437
- self.class.new(::DateTime.now - time_point, 'day').convert_to(self)
1450
+ self.class.new(::DateTime.now - time_point, "day").convert_to(self)
1438
1451
  else
1439
- raise ArgumentError, 'Must specify a Time, Date, or DateTime'
1452
+ raise ArgumentError, "Must specify a Time, Date, or DateTime"
1440
1453
  end
1441
1454
  end
1442
1455
 
@@ -1446,11 +1459,11 @@ module RubyUnits
1446
1459
  def until(time_point)
1447
1460
  case time_point
1448
1461
  when Time
1449
- self.class.new(time_point - ::Time.now, 'second').convert_to(self)
1462
+ self.class.new(time_point - ::Time.now, "second").convert_to(self)
1450
1463
  when DateTime, Date
1451
- self.class.new(time_point - ::DateTime.now, 'day').convert_to(self)
1464
+ self.class.new(time_point - ::DateTime.now, "day").convert_to(self)
1452
1465
  else
1453
- raise ArgumentError, 'Must specify a Time, Date, or DateTime'
1466
+ raise ArgumentError, "Must specify a Time, Date, or DateTime"
1454
1467
  end
1455
1468
  end
1456
1469
 
@@ -1467,7 +1480,7 @@ module RubyUnits
1467
1480
  time_point.to_datetime + self
1468
1481
  end)
1469
1482
  else
1470
- raise ArgumentError, 'Must specify a Time, Date, or DateTime'
1483
+ raise ArgumentError, "Must specify a Time, Date, or DateTime"
1471
1484
  end
1472
1485
  end
1473
1486
 
@@ -1486,12 +1499,22 @@ module RubyUnits
1486
1499
  [self.class.new(other), self]
1487
1500
  end
1488
1501
 
1489
- # returns a new unit that has been scaled to be more in line with typical usage.
1502
+ # Returns a new unit that has been scaled to be more in line with typical usage. This is highly opinionated and not
1503
+ # based on any standard. It is intended to be used to make the units more human readable.
1504
+ #
1505
+ # Some key points:
1506
+ # * Units containing 'kg' will be returned as is. The prefix in 'kg' makes this an odd case.
1507
+ # * It will use `centi` instead of `milli` when the scalar is between 0.01 and 0.001
1508
+ #
1509
+ # @return [Unit]
1490
1510
  def best_prefix
1491
1511
  return to_base if scalar.zero?
1512
+ return self if units.include?("kg")
1492
1513
 
1493
1514
  best_prefix = if kind == :information
1494
1515
  self.class.prefix_values.key(2**((::Math.log(base_scalar, 2) / 10.0).floor * 10))
1516
+ elsif ((1/100r)..(1/10r)).cover?(base_scalar)
1517
+ self.class.prefix_values.key(1/100r)
1495
1518
  else
1496
1519
  self.class.prefix_values.key(10**((::Math.log10(base_scalar) / 3.0).floor * 3))
1497
1520
  end
@@ -1544,7 +1567,7 @@ module RubyUnits
1544
1567
  index = SIGNATURE_VECTOR.index(definition.kind)
1545
1568
  vector[index] -= 1 if index
1546
1569
  end
1547
- raise ArgumentError, 'Power out of range (-20 < net power of a unit < 20)' if vector.any? { _1.abs >= 20 }
1570
+ raise ArgumentError, "Power out of range (-20 < net power of a unit < 20)" if vector.any? { _1.abs >= 20 }
1548
1571
 
1549
1572
  vector
1550
1573
  end
@@ -1587,14 +1610,14 @@ module RubyUnits
1587
1610
  # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
1588
1611
  # @return [nil,RubyUnits::Unit]
1589
1612
  # @todo This should either be a separate class or at least a class method
1590
- def parse(passed_unit_string = '0')
1613
+ def parse(passed_unit_string = "0")
1591
1614
  unit_string = passed_unit_string.dup
1592
1615
  unit_string = "#{Regexp.last_match(1)} USD" if unit_string =~ /\$\s*(#{NUMBER_REGEX})/
1593
- unit_string.gsub!("\u00b0".force_encoding('utf-8'), 'deg') if unit_string.encoding == Encoding::UTF_8
1616
+ unit_string.gsub!("\u00b0".encode("utf-8"), "deg") if unit_string.encoding == Encoding::UTF_8
1594
1617
 
1595
1618
  unit_string.gsub!(/(\d)[_,](\d)/, '\1\2') # remove underscores and commas in numbers
1596
1619
 
1597
- unit_string.gsub!(/[%'"#]/, '%' => 'percent', "'" => 'feet', '"' => 'inch', '#' => 'pound')
1620
+ unit_string.gsub!(/[%'"#]/, "%" => "percent", "'" => "feet", '"' => "inch", "#" => "pound")
1598
1621
  if unit_string.start_with?(COMPLEX_NUMBER)
1599
1622
  match = unit_string.match(COMPLEX_REGEX)
1600
1623
  real = Float(match[:real]) if match[:real]
@@ -1613,7 +1636,7 @@ module RubyUnits
1613
1636
  match = unit_string.match(RATIONAL_REGEX)
1614
1637
  numerator = Integer(match[:numerator])
1615
1638
  denominator = Integer(match[:denominator])
1616
- raise ArgumentError, 'Improper fractions must have a whole number part' if !match[:proper].nil? && !match[:proper].match?(/^#{INTEGER_REGEX}$/)
1639
+ raise ArgumentError, "Improper fractions must have a whole number part" if !match[:proper].nil? && !match[:proper].match?(/^#{INTEGER_REGEX}$/)
1617
1640
 
1618
1641
  proper = match[:proper].to_i
1619
1642
  unit_s = match[:unit]
@@ -1630,7 +1653,7 @@ module RubyUnits
1630
1653
 
1631
1654
  match = unit_string.match(NUMBER_REGEX)
1632
1655
  unit = self.class.cached.get(match[:unit])
1633
- mult = match[:scalar] == '' ? 1.0 : match[:scalar].to_f
1656
+ mult = match[:scalar] == "" ? 1.0 : match[:scalar].to_f
1634
1657
  mult = mult.to_int if mult.to_int == mult
1635
1658
 
1636
1659
  if unit
@@ -1647,14 +1670,14 @@ module RubyUnits
1647
1670
  # collapse <prefixunit><prefixunit> into <prefixunit>*<prefixunit>...
1648
1671
  end
1649
1672
  # ... and then strip the remaining brackets for x*y*z
1650
- unit_string.gsub!(/[<>]/, '')
1673
+ unit_string.gsub!(/[<>]/, "")
1651
1674
 
1652
1675
  if (match = unit_string.match(TIME_REGEX))
1653
1676
  hours = match[:hour]
1654
1677
  minutes = match[:min]
1655
1678
  seconds = match[:sec]
1656
1679
  milliseconds = match[:msec]
1657
- raise ArgumentError, 'Invalid Duration' if [hours, minutes, seconds, milliseconds].all?(&:nil?)
1680
+ raise ArgumentError, "Invalid Duration" if [hours, minutes, seconds, milliseconds].all?(&:nil?)
1658
1681
 
1659
1682
  result = self.class.new("#{hours || 0} hours") +
1660
1683
  self.class.new("#{minutes || 0} minutes") +
@@ -1705,7 +1728,7 @@ module RubyUnits
1705
1728
  end
1706
1729
 
1707
1730
  # more than one per. I.e., "1 m/s/s"
1708
- raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1
1731
+ raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count("/") > 1
1709
1732
  raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized #{unit_string}") if unit_string =~ /\s[02-9]/
1710
1733
 
1711
1734
  @scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] # parse the string into parts
@@ -1716,7 +1739,7 @@ module RubyUnits
1716
1739
  top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) { x * n }
1717
1740
  elsif n.negative?
1718
1741
  bottom = "#{bottom} #{x * -n}"
1719
- top.gsub!(/#{item[0]}(\^|\*\*)#{n}/, '')
1742
+ top.gsub!(/#{item[0]}(\^|\*\*)#{n}/, "")
1720
1743
  end
1721
1744
  end
1722
1745
  if bottom
@@ -1745,7 +1768,7 @@ module RubyUnits
1745
1768
 
1746
1769
  # eliminate all known terms from this string. This is a quick check to see if the passed unit
1747
1770
  # contains terms that are not defined.
1748
- used = "#{top} #{bottom}".to_s.gsub(self.class.unit_match_regex, '').gsub(%r{[\d*, "'_^/$]}, '')
1771
+ used = "#{top} #{bottom}".to_s.gsub(self.class.unit_match_regex, "").gsub(%r{[\d*, "'_^/$]}, "")
1749
1772
  raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless used.empty?
1750
1773
 
1751
1774
  @numerator = @numerator.map do |item|