ruby-units 4.0.3 → 4.1.0

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,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|