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.
- checksums.yaml +4 -4
- data/.github/workflows/tests.yml +9 -5
- data/.rubocop.yml +12 -8
- data/.rubocop_todo.yml +193 -0
- data/.ruby-version +1 -1
- data/.tool-versions +1 -1
- data/Gemfile +19 -17
- data/Gemfile.lock +47 -49
- data/Guardfile +7 -5
- data/README.md +9 -4
- data/Rakefile +4 -2
- data/lib/ruby-units.rb +3 -1
- data/lib/ruby_units/array.rb +2 -0
- data/lib/ruby_units/cache.rb +2 -0
- data/lib/ruby_units/configuration.rb +31 -1
- data/lib/ruby_units/date.rb +7 -5
- data/lib/ruby_units/definition.rb +5 -4
- data/lib/ruby_units/math.rb +13 -11
- data/lib/ruby_units/namespaced.rb +14 -12
- data/lib/ruby_units/numeric.rb +2 -0
- data/lib/ruby_units/string.rb +3 -1
- data/lib/ruby_units/time.rb +11 -9
- data/lib/ruby_units/unit.rb +137 -114
- data/lib/ruby_units/unit_definitions/base.rb +17 -15
- data/lib/ruby_units/unit_definitions/prefix.rb +32 -30
- data/lib/ruby_units/unit_definitions/standard.rb +270 -268
- data/lib/ruby_units/unit_definitions.rb +5 -3
- data/lib/ruby_units/version.rb +3 -1
- metadata +4 -3
data/lib/ruby_units/unit.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
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 =
|
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 = [
|
99
|
-
FAHRENHEIT = [
|
100
|
-
RANKINE = [
|
101
|
-
CELSIUS = [
|
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,
|
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,
|
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) ==
|
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? ?
|
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,
|
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 =
|
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 = [
|
538
|
+
@numerator = ["<second>"]
|
537
539
|
@denominator = UNITY_ARRAY
|
538
540
|
when DateTime, Date
|
539
541
|
@scalar = options[0].ajd
|
540
|
-
@numerator = [
|
542
|
+
@numerator = ["<day>"]
|
541
543
|
@denominator = UNITY_ARRAY
|
542
544
|
when /^\s*$/
|
543
|
-
raise ArgumentError,
|
545
|
+
raise ArgumentError, "No Unit Specified"
|
544
546
|
when String
|
545
547
|
parse(options[0])
|
546
548
|
else
|
547
|
-
raise ArgumentError,
|
549
|
+
raise ArgumentError, "Invalid Unit Format"
|
548
550
|
end
|
549
551
|
update_base_scalar
|
550
|
-
raise ArgumentError,
|
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(
|
614
|
+
convert_to("tempK")
|
613
615
|
elsif degree?
|
614
|
-
convert_to(
|
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(
|
682
|
+
feet, inches = convert_to("in").scalar.abs.divmod(12)
|
679
683
|
improper, frac = inches.divmod(1)
|
680
|
-
frac = frac.zero? ?
|
684
|
+
frac = frac.zero? ? "" : "-#{frac.rationalize(precision)}"
|
681
685
|
out = "#{negative? ? '-' : nil}#{feet}'#{improper}#{frac}\""
|
682
686
|
when :lbs
|
683
|
-
pounds, ounces = convert_to(
|
687
|
+
pounds, ounces = convert_to("oz").scalar.abs.divmod(16)
|
684
688
|
improper, frac = ounces.divmod(1)
|
685
|
-
frac = frac.zero? ?
|
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(
|
692
|
+
stone, pounds = convert_to("lbs").scalar.abs.divmod(14)
|
689
693
|
improper, frac = pounds.divmod(1)
|
690
|
-
frac = frac.zero? ?
|
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
|
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
|
-
|
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
|
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,
|
878
|
+
raise ArgumentError, "Cannot add two temperatures" if [self, other].all?(&:temperature?)
|
875
879
|
|
876
|
-
if
|
877
|
-
|
878
|
-
|
879
|
-
|
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,
|
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: [
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
1060
|
+
raise ArgumentError, "exponentiation of complex numbers is not supported."
|
1059
1061
|
else
|
1060
|
-
raise ArgumentError,
|
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,
|
1071
|
-
raise ArgumentError,
|
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,
|
1089
|
-
raise ArgumentError,
|
1090
|
-
raise ArgumentError,
|
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,
|
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(
|
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
|
1147
|
-
return self if FalseClass
|
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,
|
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,
|
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
|
1168
|
+
when "<tempC>"
|
1167
1169
|
@scalar + 273.15
|
1168
|
-
when
|
1170
|
+
when "<tempK>"
|
1169
1171
|
@scalar
|
1170
|
-
when
|
1172
|
+
when "<tempF>"
|
1171
1173
|
(@scalar + 459.67).to_r * Rational(5, 9)
|
1172
|
-
when
|
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
|
1179
|
+
when "<tempC>"
|
1178
1180
|
@base_scalar - 273.15
|
1179
|
-
when
|
1181
|
+
when "<tempK>"
|
1180
1182
|
@base_scalar
|
1181
|
-
when
|
1183
|
+
when "<tempF>"
|
1182
1184
|
(@base_scalar.to_r * Rational(9, 5)) - 459.67r
|
1183
|
-
when
|
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,
|
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
|
1274
|
+
def units(with_prefix: true, format: nil)
|
1275
|
+
return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
|
1272
1276
|
|
1273
|
-
output_numerator = [
|
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
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
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,
|
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,
|
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(
|
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(
|
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,
|
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,
|
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,
|
1450
|
+
self.class.new(::DateTime.now - time_point, "day").convert_to(self)
|
1438
1451
|
else
|
1439
|
-
raise ArgumentError,
|
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,
|
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,
|
1464
|
+
self.class.new(time_point - ::DateTime.now, "day").convert_to(self)
|
1452
1465
|
else
|
1453
|
-
raise ArgumentError,
|
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,
|
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
|
-
#
|
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,
|
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 =
|
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".
|
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!(/[%'"#]/,
|
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,
|
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] ==
|
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,
|
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(
|
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,
|
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|
|