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