class RubyUnits::Unit
Copyright 2006-2024 @author Kevin C. Olbrich, Ph.D. @see github.com/olbrich/ruby-units
@note The accuracy of unit conversions depends on the precision of the conversion factor.
If you have more accurate estimates for particular conversion factors, please send them to me and I will incorporate them into the next release. It is also incumbent on the end-user to ensure that the accuracy of any conversions is sufficient for their intended application.
While there are a large number of unit specified in the base package, there are also a large number of units that are not included. This package covers nearly all SI, Imperial, and units commonly used in the United States. If your favorite units are not listed here, file an issue on GitHub.
To add or override a unit definition, add a code block like this.. @example Define a new unit
RubyUnits::Unit.define("foobar") do |unit| unit.aliases = %w{foo fb foo-bar} unit.definition = RubyUnits::Unit.new("1 baz") end
Constants
- ANY_NUMBER
Any Complex, Rational, or scientific number
- ANY_NUMBER_REGEX
- BOTTOM_REGEX
- CELSIUS
- COMPLEX_NUMBER
Complex numbers: 1+2i, 1.0+2.0i, -1-1i, etc.
- COMPLEX_REGEX
- DECIMAL_REGEX
- DIGITS_REGEX
- FAHRENHEIT
- FEET_INCH_REGEX
- FEET_INCH_UNITS_REGEX
ideally we would like to generate this regex from the alias for a ‘feet’ and ‘inches’, but they aren’t defined at the point in the code where we need this regex.
- INTEGER_DIGITS_REGEX
regex for matching an integer number but not a fraction
- INTEGER_REGEX
- KELVIN
- LBS_OZ_REGEX
- LBS_OZ_UNIT_REGEX
ideally we would like to generate this regex from the alias for a ‘pound’ and ‘ounce’, but they aren’t defined at the point in the code where we need this regex.
- NUMBER_REGEX
- NUMBER_UNIT_REGEX
- RANKINE
- RATIONAL_NUMBER
- RATIONAL_REGEX
- SCI_NUMBER
-123.4E+5, -123.4e-5, etc.
- SIGNATURE_VECTOR
- SIGN_REGEX
- STONE_LB_REGEX
- STONE_LB_UNIT_REGEX
ideally we would like to generate this regex from the alias for a ‘stone’ and ‘pound’, but they aren’t defined at the point in the code where we need this regex. also note that the plural of ‘stone’ is still ‘stone’, but we accept ‘stones’ anyway.
- TIME_REGEX
Time
formats: 12:34:56,78, (hh:mm:ss,msec) etc.- TOP_REGEX
- UNITY
- UNITY_ARRAY
- UNIT_STRING_REGEX
- UNSIGNED_INTEGER_REGEX
- VERSION
Attributes
return a list of all defined units @return [Hash{Symbol=>RubyUnits::Units::Definition}]
@return [Hash{Integer => Symbol}]
@return [Hash{Symbol => String}]
@return [Hash{Symbol => String}] the list of units and their prefixes
@return [Hash{Symbol => String}]
@return [Hash{Symbol => String}]
@return [Array]
@return [Array]
@return [Numeric]
@return [Array]
@return [Array]
@return [String]
@return [Numeric]
@return [Integer]
@return [String]
Public Class Methods
@return [RubyUnits::Cache]
# File lib/ruby_units/unit.rb, line 286 def self.base_unit_cache @base_unit_cache ||= RubyUnits::Cache.new end
return an array of base units @return [Array]
# File lib/ruby_units/unit.rb, line 340 def self.base_units @base_units ||= definitions.dup.select { |_, definition| definition.base? }.keys.map { new(_1) } end
Unit
cache
@return [RubyUnits::Cache]
# File lib/ruby_units/unit.rb, line 273 def self.cached @cached ||= RubyUnits::Cache.new end
@return [Boolean]
# File lib/ruby_units/unit.rb, line 278 def self.clear_cache cached.clear base_unit_cache.clear new(1) true end
@param [RubyUnits::Unit::Definition, String] unit_definition @param [Proc] block @return [RubyUnits::Unit::Definition] @raise [ArgumentError] when passed a non-string if using the block form Unpack a unit definition and add it to the array of defined units
@example Block form
RubyUnits::Unit.define('foobar') do |foobar| foobar.definition = RubyUnits::Unit.new("1 baz") end
@example RubyUnits::Unit::Definition
form
unit_definition = RubyUnits::Unit::Definition.new("foobar") {|foobar| foobar.definition = RubyUnits::Unit.new("1 baz")} RubyUnits::Unit.define(unit_definition)
# File lib/ruby_units/unit.rb, line 230 def self.define(unit_definition, &block) if block_given? raise ArgumentError, "When using the block form of RubyUnits::Unit.define, pass the name of the unit" unless unit_definition.is_a?(String) unit_definition = RubyUnits::Unit::Definition.new(unit_definition, &block) end definitions[unit_definition.name] = unit_definition use_definition(unit_definition) unit_definition end
determine if a unit is already defined @param [String] unit @return [Boolean]
# File lib/ruby_units/unit.rb, line 204 def self.defined?(unit) definitions.values.any? { _1.aliases.include?(unit) } end
return the unit definition for a unit @param unit_name
[String] @return [RubyUnits::Unit::Definition, nil]
# File lib/ruby_units/unit.rb, line 211 def self.definition(unit_name) unit = unit_name =~ /^<.+>$/ ? unit_name : "<#{unit_name}>" definitions[unit] end
@param q [Numeric] quantity @param n [Array] numerator @param d [Array] denominator @return [Hash]
# File lib/ruby_units/unit.rb, line 303 def self.eliminate_terms(q, n, d) num = n.dup den = d.dup num.delete(UNITY) den.delete(UNITY) combined = ::Hash.new(0) [[num, 1], [den, -1]].each do |array, increment| array.chunk_while { |elt_before, _| definition(elt_before).prefix? } .to_a .each { combined[_1] += increment } end num = [] den = [] combined.each do |key, value| if value.positive? value.times { num << key } elsif value.negative? value.abs.times { den << key } end end num = UNITY_ARRAY if num.empty? den = UNITY_ARRAY if den.empty? { scalar: q, numerator: num.flatten, denominator: den.flatten } end
Callback triggered when a subclass is created. This properly sets up the internal variables, and copies definitions from the parent class.
@param [Class] subclass
# File lib/ruby_units/unit.rb, line 174 def self.inherited(subclass) super subclass.definitions = definitions.dup subclass.instance_variable_set(:@kinds, @kinds.dup) subclass.setup end
Create a new Unit
object. Can be initialized using a String
, a Hash, an Array
, Time
, DateTime
@example Valid options include:
"5.6 kg*m/s^2" "5.6 kg*m*s^-2" "5.6 kilogram*meter*second^-2" "2.2 kPa" "37 degC" "1" -- creates a unitless constant with value 1 "GPa" -- creates a unit with scalar 1 with units 'GPa' "6'4\""" -- recognized as 6 feet + 4 inches "8 lbs 8 oz" -- recognized as 8 lbs + 8 ounces [1, 'kg'] {scalar: 1, numerator: 'kg'}
@param [Unit,String,Hash,Array,Date,Time,DateTime] options @return [Unit] @raise [ArgumentError] if absolute value of a temperature is less than absolute zero @raise [ArgumentError] if no unit is specified @raise [ArgumentError] if an invalid unit is specified
# File lib/ruby_units/unit.rb, line 490 def initialize(*options) @scalar = nil @base_scalar = nil @unit_name = nil @signature = nil @output = {} raise ArgumentError, "Invalid Unit Format" if options[0].nil? if options.size == 2 # options[0] is the scalar # options[1] is a unit string cached = self.class.cached.get(options[1]) if cached.nil? initialize("#{options[0]} #{options[1]}") else copy(cached * options[0]) end return end if options.size == 3 options[1] = options[1].join if options[1].is_a?(Array) options[2] = options[2].join if options[2].is_a?(Array) cached = self.class.cached.get("#{options[1]}/#{options[2]}") if cached.nil? initialize("#{options[0]} #{options[1]}/#{options[2]}") else copy(cached) * options[0] end return end case options[0] when Unit copy(options[0]) return when Hash @scalar = options[0][:scalar] || 1 @numerator = options[0][:numerator] || UNITY_ARRAY @denominator = options[0][:denominator] || UNITY_ARRAY @signature = options[0][:signature] when Array initialize(*options[0]) return when Numeric @scalar = options[0] @numerator = @denominator = UNITY_ARRAY when Time @scalar = options[0].to_f @numerator = ["<second>"] @denominator = UNITY_ARRAY when DateTime, Date @scalar = options[0].ajd @numerator = ["<day>"] @denominator = UNITY_ARRAY when /^\s*$/ raise ArgumentError, "No Unit Specified" when String parse(options[0]) else raise ArgumentError, "Invalid Unit Format" end update_base_scalar raise ArgumentError, "Temperatures must not be less than absolute zero" if temperature? && base_scalar.negative? unary_unit = units || "" if options.first.instance_of?(String) _opt_scalar, opt_units = self.class.parse_into_numbers_and_units(options[0]) if !(self.class.cached.keys.include?(opt_units) || (opt_units =~ %r{\D/[\d+.]+}) || (opt_units =~ %r{(#{self.class.temp_regex})|(#{STONE_LB_UNIT_REGEX})|(#{LBS_OZ_UNIT_REGEX})|(#{FEET_INCH_UNITS_REGEX})|%|(#{TIME_REGEX})|i\s?(.+)?|±|\+/-})) && (opt_units && !opt_units.empty?) self.class.cached.set(opt_units, scalar == 1 ? self : opt_units.to_unit) end end unless self.class.cached.keys.include?(unary_unit) || (unary_unit =~ self.class.temp_regex) self.class.cached.set(unary_unit, scalar == 1 ? self : unary_unit.to_unit) end [@scalar, @numerator, @denominator, @base_scalar, @signature, @base].each(&:freeze) super() end
@example parse strings
"1 minute in seconds"
@param [String] input @return [Unit]
# File lib/ruby_units/unit.rb, line 294 def self.parse(input) first, second = input.scan(/(.+)\s(?:in|to|as)\s(.+)/i).first second.nil? ? new(first) : new(first).convert_to(second) end
Parse a string consisting of a number and a unit string NOTE: This does not properly handle units formatted like ‘12mg/6ml’
@param [String] string @return [Array(Numeric
, String
)] consisting of [number, “unit”]
# File lib/ruby_units/unit.rb, line 349 def self.parse_into_numbers_and_units(string) num, unit = string.scan(ANY_NUMBER_REGEX).first [ case num when nil # This happens when no number is passed and we are parsing a pure unit string 1 when COMPLEX_NUMBER num.to_c when RATIONAL_NUMBER # We use this method instead of relying on `to_r` because it does not # handle improper fractions correctly. sign = Regexp.last_match(1) == "-" ? -1 : 1 n = Regexp.last_match(2).to_i f = Rational(Regexp.last_match(3).to_i, Regexp.last_match(4).to_i) sign * (n + f) else num.to_f end, unit.to_s.strip ] end
return a regexp fragment used to match prefixes @return [String] @private
# File lib/ruby_units/unit.rb, line 388 def self.prefix_regex @prefix_regex ||= prefix_map.keys.sort_by { [_1.length, _1] }.reverse.join("|") end
Get the definition for a unit and allow it to be redefined
@param [String] name Name of unit to redefine @param [Proc] _block @raise [ArgumentError] if a block is not given @yieldparam [RubyUnits::Unit::Definition] the definition of the unit being
redefined
@return (see RubyUnits::Unit.define
)
# File lib/ruby_units/unit.rb, line 249 def self.redefine!(name, &_block) raise ArgumentError, "A block is required to redefine a unit" unless block_given? unit_definition = definition(name) raise(ArgumentError, "'#{name}' Unit not recognized") unless unit_definition yield unit_definition definitions.delete("<#{name}>") define(unit_definition) setup end
setup internal arrays and hashes @return [Boolean]
# File lib/ruby_units/unit.rb, line 183 def self.setup clear_cache self.prefix_values = {} self.prefix_map = {} self.unit_map = {} self.unit_values = {} @unit_regex = nil @unit_match_regex = nil @prefix_regex = nil definitions.each_value do |definition| use_definition(definition) end new(1) true end
Generates (and memoizes) a regexp matching any of the temperature units or their aliases.
@return [Regexp]
# File lib/ruby_units/unit.rb, line 395 def self.temp_regex @temp_regex ||= begin temp_units = %w[tempK tempC tempF tempR degK degC degF degR] aliases = temp_units.map do |unit| d = definition(unit) d && d.aliases end.flatten.compact regex_str = aliases.empty? ? "(?!x)x" : aliases.join("|") Regexp.new "(?:#{regex_str})" end end
Undefine a unit. Will not raise an exception for unknown units.
@param unit [String] name of unit to undefine @return (see RubyUnits::Unit.setup
)
# File lib/ruby_units/unit.rb, line 265 def self.undefine!(unit) definitions.delete("<#{unit}>") setup end
return a regex used to match units @return [Regexp]
# File lib/ruby_units/unit.rb, line 381 def self.unit_match_regex @unit_match_regex ||= /(#{prefix_regex})??(#{unit_regex})\b/ end
return a fragment of a regex to be used for matching units or reconstruct it if hasn’t been used yet. Unit
names are reverse sorted by length so the regexp matcher will prefer longer and more specific names @return [String]
# File lib/ruby_units/unit.rb, line 375 def self.unit_regex @unit_regex ||= unit_map.keys.sort_by { [_1.length, _1] }.reverse.join("|") end
inject a definition into the internal array and set it up for use
@param definition [RubyUnits::Unit::Definition]
# File lib/ruby_units/unit.rb, line 410 def self.use_definition(definition) @unit_match_regex = nil # invalidate the unit match regex @temp_regex = nil # invalidate the temp regex if definition.prefix? prefix_values[definition.name] = definition.scalar definition.aliases.each { prefix_map[_1] = definition.name } @prefix_regex = nil # invalidate the prefix regex else unit_values[definition.name] = {} unit_values[definition.name][:scalar] = definition.scalar unit_values[definition.name][:numerator] = definition.numerator if definition.numerator unit_values[definition.name][:denominator] = definition.denominator if definition.denominator definition.aliases.each { unit_map[_1] = definition.name } @unit_regex = nil # invalidate the unit regex end end
Public Instance Methods
Perform a modulo on a unit, will raise an exception if the units are not compatible
@param [Unit] other @return [Integer] @raise [ArgumentError] if units are not compatible
# File lib/ruby_units/unit.rb, line 1010 def %(other) raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other) self.class.new(base_scalar % other.to_unit.base_scalar, to_base.units).convert_to(self) end
Multiply two units. @param [Numeric] other @return [Unit] @raise [ArgumentError] when attempting to multiply two temperatures
# File lib/ruby_units/unit.rb, line 938 def *(other) case other when Unit raise ArgumentError, "Cannot multiply by temperatures" if [other, self].any?(&:temperature?) opts = self.class.eliminate_terms(@scalar * other.scalar, @numerator + other.numerator, @denominator + other.denominator) opts[:signature] = @signature + other.signature self.class.new(opts) when Numeric self.class.new(scalar: @scalar * other, numerator: @numerator, denominator: @denominator, signature: @signature) else x, y = coerce(other) x * y end end
Exponentiation. Only takes integer powers. Note that anything raised to the power of 0 results in a [Unit] object with a scalar of 1, and no units. Throws an exception if exponent is not an integer. Ideally this routine should accept a float for the exponent It should then convert the float to a rational and raise the unit by the numerator and root it by the denominator but, sadly, floats can’t be converted to rationals.
For now, if a rational is passed in, it will be used, otherwise we are stuck with integers and certain floats < 1 @param [Numeric] other @return [Unit] @raise [ArgumentError] when raising a temperature to a power @raise [ArgumentError] when n not in the set integers from (1..9) @raise [ArgumentError] when attempting to raise to a complex number @raise [ArgumentError] when an invalid exponent is passed
# File lib/ruby_units/unit.rb, line 1039 def **(other) raise ArgumentError, "Cannot raise a temperature to a power" if temperature? if other.is_a?(Numeric) return inverse if other == -1 return self if other == 1 return 1 if other.zero? end case other when Rational power(other.numerator).root(other.denominator) when Integer power(other) when Float return self**other.to_i if other == other.to_i valid = (1..9).map { Rational(1, _1) } raise ArgumentError, "Not a n-th root (1..9), use 1/n" unless valid.include? other.abs root(Rational(1, other).to_int) when Complex raise ArgumentError, "exponentiation of complex numbers is not supported." else raise ArgumentError, "Invalid Exponent" end end
Add two units together. Result is same units as receiver and scalar and base_scalar
are updated appropriately throws an exception if the units are not compatible. It is possible to add Time
objects to units of time @param [Object] other @return [Unit] @raise [ArgumentError] when two temperatures are added @raise [ArgumentError] when units are not compatible @raise [ArgumentError] when adding a fixed time or date to a time span
# File lib/ruby_units/unit.rb, line 872 def +(other) case other when Unit if zero? other.dup elsif self =~ other raise ArgumentError, "Cannot add two temperatures" if [self, other].all?(&:temperature?) if temperature? self.class.new(scalar: (scalar + other.convert_to(temperature_scale).scalar), numerator: @numerator, denominator: @denominator, signature: @signature) elsif other.temperature? self.class.new(scalar: (other.scalar + convert_to(other.temperature_scale).scalar), numerator: other.numerator, denominator: other.denominator, signature: other.signature) else self.class.new(scalar: (base_scalar + other.base_scalar), numerator: base.numerator, denominator: base.denominator, signature: @signature).convert_to(self) end else raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" end when Date, Time raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be added to a Unit" else x, y = coerce(other) y + x end end
Subtract two units. Result is same units as receiver and scalar and base_scalar
are updated appropriately @param [Numeric] other @return [Unit] @raise [ArgumentError] when subtracting a temperature from a degree @raise [ArgumentError] when units are not compatible @raise [ArgumentError] when subtracting a fixed time from a time span
# File lib/ruby_units/unit.rb, line 904 def -(other) case other when Unit if zero? if other.zero? other.dup * -1 # preserve Units class else -other.dup end elsif self =~ other if [self, other].all?(&:temperature?) self.class.new(scalar: (base_scalar - other.base_scalar), numerator: KELVIN, denominator: UNITY_ARRAY, signature: @signature).convert_to(temperature_scale) elsif temperature? self.class.new(scalar: (base_scalar - other.base_scalar), numerator: ["<tempK>"], denominator: UNITY_ARRAY, signature: @signature).convert_to(self) elsif other.temperature? raise ArgumentError, "Cannot subtract a temperature from a differential degree unit" else self.class.new(scalar: (base_scalar - other.base_scalar), numerator: base.numerator, denominator: base.denominator, signature: @signature).convert_to(self) end else raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" end when Time raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be subtracted from a Unit" else x, y = coerce(other) y - x end end
negates the scalar of the Unit
@return [Numeric,Unit]
# File lib/ruby_units/unit.rb, line 1318 def -@ return -@scalar if unitless? dup * -1 end
Divide two units. Throws an exception if divisor is 0 @param [Numeric] other @return [Unit] @raise [ZeroDivisionError] if divisor is zero @raise [ArgumentError] if attempting to divide a temperature by another temperature
# File lib/ruby_units/unit.rb, line 960 def /(other) case other when Unit raise ZeroDivisionError if other.zero? raise ArgumentError, "Cannot divide with temperatures" if [other, self].any?(&:temperature?) sc = Rational(@scalar, other.scalar) sc = sc.numerator if sc.denominator == 1 opts = self.class.eliminate_terms(sc, @numerator + other.denominator, @denominator + other.numerator) opts[:signature] = @signature - other.signature self.class.new(opts) when Numeric raise ZeroDivisionError if other.zero? sc = Rational(@scalar, other) sc = sc.numerator if sc.denominator == 1 self.class.new(scalar: sc, numerator: @numerator, denominator: @denominator, signature: @signature) else x, y = coerce(other) y / x end end
Compare two Unit
objects. Throws an exception if they are not of compatible types. Comparisons are done based on the value of the unit in base SI units. @param [Object] other @return [Integer,nil] @raise [NoMethodError] when other does not define <=> @raise [ArgumentError] when units are not compatible
# File lib/ruby_units/unit.rb, line 777 def <=>(other) raise NoMethodError, "undefined method `<=>' for #{base_scalar.inspect}" unless base_scalar.respond_to?(:<=>) if other.nil? base_scalar <=> nil elsif !temperature? && other.respond_to?(:zero?) && other.zero? base_scalar <=> 0 elsif other.instance_of?(Unit) raise ArgumentError, "Incompatible Units ('#{units}' not compatible with '#{other.units}')" unless self =~ other base_scalar <=> other.base_scalar else x, y = coerce(other) y <=> x end end
Compare Units for equality this is necessary mostly for Complex units. Complex units do not have a <=> operator so we define this one here so that we can properly check complex units for equality. Units of incompatible types are not equal, except when they are both zero and neither is a temperature Equality checks can be tricky since round off errors may make essentially equivalent units appear to be different. @param [Object] other @return [Boolean]
# File lib/ruby_units/unit.rb, line 802 def ==(other) if other.respond_to?(:zero?) && other.zero? zero? elsif other.instance_of?(Unit) return false unless self =~ other base_scalar == other.base_scalar else begin x, y = coerce(other) x == y rescue ArgumentError # return false when object cannot be coerced false end end end
Compare two units. Returns true if quantities and units match @example
RubyUnits::Unit.new("100 cm") === RubyUnits::Unit.new("100 cm") # => true RubyUnits::Unit.new("100 cm") === RubyUnits::Unit.new("1 m") # => false
@param [Object] other @return [Boolean]
# File lib/ruby_units/unit.rb, line 847 def ===(other) case other when Unit (scalar == other.scalar) && (units == other.units) else begin x, y = coerce(other) x.same_as?(y) rescue ArgumentError false end end end
Check to see if units are compatible, ignoring the scalar part. This check is done by comparing unit signatures for performance reasons. If passed a string, this will create a [Unit] object with the string and then do the comparison.
@example this permits a syntax like:
unit =~ "mm"
@note if you want to do a regexp comparison of the unit string do this …
unit.units =~ /regexp/
@param [Object] other @return [Boolean]
# File lib/ruby_units/unit.rb, line 829 def =~(other) return signature == other.signature if other.is_a?(Unit) x, y = coerce(other) x =~ y rescue ArgumentError # return false when `other` cannot be converted to a [Unit] false end
absolute value of a unit @return [Numeric,Unit]
# File lib/ruby_units/unit.rb, line 1326 def abs return @scalar.abs if unitless? self.class.new(@scalar.abs, @numerator, @denominator) end
@example ‘5 min’.to_unit.ago @return [Unit]
# File lib/ruby_units/unit.rb, line 1420 def ago before end
Returns string formatted for json @return [String]
# File lib/ruby_units/unit.rb, line 1264 def as_json(*) to_s end
Is this unit in base form? @return [Boolean]
# File lib/ruby_units/unit.rb, line 591 def base? return @base if defined? @base @base = (@numerator + @denominator) .compact .uniq .map { self.class.definition(_1) } .all? { _1.unity? || _1.base? } @base end
@example ‘5 min’.before(time) @return [Unit]
# File lib/ruby_units/unit.rb, line 1426 def before(time_point = ::Time.now) case time_point when Time, Date, DateTime (begin time_point - self rescue StandardError time_point.to_datetime - self end) else raise ArgumentError, "Must specify a Time, Date, or DateTime" end end
Returns a new unit that has been scaled to be more in line with typical usage. This is highly opinionated and not based on any standard. It is intended to be used to make the units more human readable.
Some key points:
-
Units containing ‘kg’ will be returned as is. The prefix in ‘kg’ makes this an odd case.
-
It will use ‘centi` instead of `milli` when the scalar is between 0.01 and 0.001
@return [Unit]
# File lib/ruby_units/unit.rb, line 1510 def best_prefix return to_base if scalar.zero? return self if units.include?("kg") best_prefix = if kind == :information self.class.prefix_values.key(2**((::Math.log(base_scalar, 2) / 10.0).floor * 10)) elsif ((1/100r)..(1/10r)).cover?(base_scalar) self.class.prefix_values.key(1/100r) else self.class.prefix_values.key(10**((::Math.log10(base_scalar) / 3.0).floor * 3)) end to(self.class.new(self.class.prefix_map.key(best_prefix) + units(with_prefix: false))) end
ceil of a unit @return [Numeric,Unit]
# File lib/ruby_units/unit.rb, line 1334 def ceil(*args) return @scalar.ceil(*args) if unitless? self.class.new(@scalar.ceil(*args), @numerator, @denominator) end
Automatically coerce objects to [Unit] when possible. If an object defines a ‘#to_unit’ method, it will be coerced using that method.
@param other [Object, to_unit
] @return [Array(Unit
, Unit
)] @raise [ArgumentError] when ‘other` cannot be converted to a [Unit]
# File lib/ruby_units/unit.rb, line 1496 def coerce(other) return [other.to_unit, self] if other.respond_to?(:to_unit) [self.class.new(other), self] end
convert to a specified unit string or to the same units as another Unit
unit.convert_to "kg" will covert to kilograms unit1.convert_to unit2 converts to same units as unit2 object
To convert a Unit
object to match another Unit
object, use:
unit1 >>= unit2
Special handling for temperature conversions is supported. If the Unit
object is converted from one temperature unit to another, the proper temperature offsets will be used. Supports Kelvin, Celsius, Fahrenheit, and Rankine scales.
@note If temperature is part of a compound unit, the temperature will be
treated as a differential and the units will be scaled appropriately.
@note When converting units with Integer scalars, the scalar will be
converted to a Rational to avoid unexpected behavior caused by Integer division.
@param other [Unit, String] @return [Unit] @raise [ArgumentError] when attempting to convert a degree to a temperature @raise [ArgumentError] when target unit is unknown @raise [ArgumentError] when target unit is incompatible
# File lib/ruby_units/unit.rb, line 1146 def convert_to(other) return self if other.nil? return self if other.is_a?(TrueClass) return self if other.is_a?(FalseClass) if (other.is_a?(Unit) && other.temperature?) || (other.is_a?(String) && other =~ self.class.temp_regex) raise ArgumentError, "Receiver is not a temperature unit" unless degree? start_unit = units # @type [String] target_unit = case other when Unit other.units when String other else raise ArgumentError, "Unknown target units" end return self if target_unit == start_unit # @type [Numeric] @base_scalar ||= case self.class.unit_map[start_unit] when "<tempC>" @scalar + 273.15 when "<tempK>" @scalar when "<tempF>" (@scalar + 459.67).to_r * Rational(5, 9) when "<tempR>" @scalar.to_r * Rational(5, 9) end # @type [Numeric] q = case self.class.unit_map[target_unit] when "<tempC>" @base_scalar - 273.15 when "<tempK>" @base_scalar when "<tempF>" (@base_scalar.to_r * Rational(9, 5)) - 459.67r when "<tempR>" @base_scalar.to_r * Rational(9, 5) end self.class.new("#{q} #{target_unit}") else # @type [Unit] target = case other when Unit other when String self.class.new(other) else raise ArgumentError, "Unknown target units" end return self if target.units == units raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless self =~ target numerator1 = @numerator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact denominator1 = @denominator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact numerator2 = target.numerator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact denominator2 = target.denominator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact # If the scalar is an Integer, convert it to a Rational number so that # if the value is scaled during conversion, resolution is not lost due # to integer math # @type [Rational, Numeric] conversion_scalar = @scalar.is_a?(Integer) ? @scalar.to_r : @scalar q = conversion_scalar * (numerator1 + denominator2).reduce(1, :*) / (numerator2 + denominator1).reduce(1, :*) # Convert the scalar to an Integer if the result is equivalent to an # integer q = q.to_i if @scalar.is_a?(Integer) && q.to_i == q self.class.new(scalar: q, numerator: target.numerator, denominator: target.denominator, signature: target.signature) end end
Used to copy one unit to another @param from [RubyUnits::Unit] Unit
to copy definition from @return [RubyUnits::Unit]
# File lib/ruby_units/unit.rb, line 459 def copy(from) @scalar = from.scalar @numerator = from.numerator @denominator = from.denominator @base = from.base? @signature = from.signature @base_scalar = from.base_scalar @unit_name = from.unit_name self end
true if a degree unit or equivalent. @return [Boolean]
# File lib/ruby_units/unit.rb, line 749 def degree? kind == :temperature end
Divide two units and return quotient and remainder
@param [Unit] other @return [Array(Integer, Unit
)] @raise [ArgumentError] if units are not compatible
# File lib/ruby_units/unit.rb, line 999 def divmod(other) raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other) [quo(other).to_base.floor, self % other] end
Creates a new unit from the current one with all common terms eliminated.
@return [RubyUnits::Unit]
# File lib/ruby_units/unit.rb, line 334 def eliminate_terms self.class.new(self.class.eliminate_terms(@scalar, @numerator, @denominator)) end
@return [Numeric,Unit]
# File lib/ruby_units/unit.rb, line 1341 def floor(*args) return @scalar.floor(*args) if unitless? self.class.new(@scalar.floor(*args), @numerator, @denominator) end
@example ‘5 min’.from(time) @param [Time, Date
, DateTime] time_point @return [Time, Date
, DateTime] @raise [ArgumentError] when passed argument is not a Time
, Date
, or DateTime
# File lib/ruby_units/unit.rb, line 1474 def from(time_point) case time_point when Time, DateTime, Date (begin time_point + self rescue StandardError time_point.to_datetime + self end) else raise ArgumentError, "Must specify a Time, Date, or DateTime" end end
override hash method so objects with same values are considered equal
# File lib/ruby_units/unit.rb, line 1525 def hash [ @scalar, @numerator, @denominator, @base, @signature, @base_scalar, @unit_name ].hash end
Normally pretty prints the unit, but if you really want to see the guts of it, pass ‘:dump’ @deprecated @return [String]
# File lib/ruby_units/unit.rb, line 732 def inspect(dump = nil) return super() if dump to_s end
returns inverse of Unit
(1/unit) @return [Unit]
# File lib/ruby_units/unit.rb, line 1119 def inverse self.class.new("1") / self end
@todo: figure out how to handle :counting units. This method should probably return :counting instead of :unitless for ‘each’ return the kind of the unit (:mass, :length, etc…) @return [Symbol]
# File lib/ruby_units/unit.rb, line 573 def kind self.class.kinds[signature] end
returns the unit raised to the n-th power @param [Integer] n @return [Unit] @raise [ArgumentError] when attempting to raise a temperature to a power @raise [ArgumentError] when n is not an integer
# File lib/ruby_units/unit.rb, line 1071 def power(n) raise ArgumentError, "Cannot raise a temperature to a power" if temperature? raise ArgumentError, "Exponent must an Integer" unless n.is_a?(Integer) return inverse if n == -1 return 1 if n.zero? return self if n == 1 return (1..(n - 1).to_i).inject(self) { |acc, _elem| acc * self } if n >= 0 (1..-(n - 1).to_i).inject(self) { |acc, _elem| acc / self } end
returns previous unit in a range. ‘2 mm’.to_unit.pred #=> ‘1 mm’.to_unit only works when the scalar is an integer @return [Unit] @raise [ArgumentError] when scalar is not equal to an integer
# File lib/ruby_units/unit.rb, line 1386 def pred raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i self.class.new(@scalar.to_i.pred, @numerator, @denominator) end
@param [Object] other @return [Unit] @raise [ZeroDivisionError] if other is zero
# File lib/ruby_units/unit.rb, line 1020 def quo(other) self / other end
Returns the remainder when one unit is divided by another
@param [Unit] other @return [Unit] @raise [ArgumentError] if units are not compatible
# File lib/ruby_units/unit.rb, line 988 def remainder(other) raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other) self.class.new(base_scalar.remainder(other.to_unit.base_scalar), to_base.units).convert_to(self) end
Calculates the n-th root of a unit if n < 0, returns 1/unit^(1/n) @param [Integer] n @return [Unit] @raise [ArgumentError] when attempting to take the root of a temperature @raise [ArgumentError] when n is not an integer @raise [ArgumentError] when n is 0
# File lib/ruby_units/unit.rb, line 1089 def root(n) raise ArgumentError, "Cannot take the root of a temperature" if temperature? raise ArgumentError, "Exponent must an Integer" unless n.is_a?(Integer) raise ArgumentError, "0th root undefined" if n.zero? return self if n == 1 return root(n.abs).inverse if n.negative? vec = unit_signature_vector vec = vec.map { _1 % n } raise ArgumentError, "Illegal root" unless vec.max.zero? num = @numerator.dup den = @denominator.dup @numerator.uniq.each do |item| x = num.find_all { _1 == item }.size r = ((x / n) * (n - 1)).to_int r.times { num.delete_at(num.index(item)) } end @denominator.uniq.each do |item| x = den.find_all { _1 == item }.size r = ((x / n) * (n - 1)).to_int r.times { den.delete_at(den.index(item)) } end self.class.new(scalar: @scalar**Rational(1, n), numerator: num, denominator: den) end
Round the unit according to the rules of the scalar’s class. Call this with the arguments appropriate for the scalar’s class (e.g., Integer, Rational, etc..). Because unit conversions can often result in Rational scalars (to preserve precision), it may be advisable to use to_s
to format output instead of using round
. @example
RubyUnits::Unit.new('21870 mm/min').convert_to('m/min').round(1) #=> 2187/100 m/min RubyUnits::Unit.new('21870 mm/min').convert_to('m/min').to_s('%0.1f') #=> 21.9 m/min
@return [Numeric,Unit]
# File lib/ruby_units/unit.rb, line 1357 def round(*args, **kwargs) return @scalar.round(*args, **kwargs) if unitless? self.class.new(@scalar.round(*args, **kwargs), @numerator, @denominator) end
@example ‘min’.since(time) @param [Time, Date
, DateTime] time_point @return [Unit] @raise [ArgumentError] when time point is not a Time
, Date
, or DateTime
# File lib/ruby_units/unit.rb, line 1445 def since(time_point) case time_point when Time self.class.new(::Time.now - time_point, "second").convert_to(self) when DateTime, Date self.class.new(::DateTime.now - time_point, "day").convert_to(self) else raise ArgumentError, "Must specify a Time, Date, or DateTime" end end
returns next unit in a range. ‘1 mm’.to_unit.succ #=> ‘2 mm’.to_unit only works when the scalar is an integer @return [Unit] @raise [ArgumentError] when scalar is not equal to an integer
# File lib/ruby_units/unit.rb, line 1374 def succ raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i self.class.new(@scalar.to_i.succ, @numerator, @denominator) end
true if unit is a ‘temperature’, false if a ‘degree’ or anything else @return [Boolean] @todo use unit definition to determine if it’s a temperature instead of a regex
# File lib/ruby_units/unit.rb, line 741 def temperature? degree? && units.match?(self.class.temp_regex) end
returns the ‘degree’ unit associated with a temperature unit @example ‘100 tempC’.to_unit.temperature_scale #=> ‘degC’ @return [String] possible values: degC, degF, degR, or degK
# File lib/ruby_units/unit.rb, line 758 def temperature_scale return nil unless temperature? "deg#{self.class.unit_map[units][/temp([CFRK])/, 1]}" end
convert to base SI units results of the conversion are cached so subsequent calls to this will be fast @return [Unit] @todo this is brittle as it depends on the display_name of a unit, which can be changed
# File lib/ruby_units/unit.rb, line 608 def to_base return self if base? if self.class.unit_map[units] =~ /\A<(?:temp|deg)[CRF]>\Z/ @signature = self.class.kinds.key(:temperature) base = if temperature? convert_to("tempK") elsif degree? convert_to("degK") end return base end cached_unit = self.class.base_unit_cache.get(units) return cached_unit * scalar unless cached_unit.nil? num = [] den = [] q = Rational(1) @numerator.compact.each do |num_unit| if self.class.prefix_values[num_unit] q *= self.class.prefix_values[num_unit] else q *= self.class.unit_values[num_unit][:scalar] if self.class.unit_values[num_unit] num << self.class.unit_values[num_unit][:numerator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:numerator] den << self.class.unit_values[num_unit][:denominator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:denominator] end end @denominator.compact.each do |num_unit| if self.class.prefix_values[num_unit] q /= self.class.prefix_values[num_unit] else q /= self.class.unit_values[num_unit][:scalar] if self.class.unit_values[num_unit] den << self.class.unit_values[num_unit][:numerator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:numerator] num << self.class.unit_values[num_unit][:denominator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:denominator] end end num = num.flatten.compact den = den.flatten.compact num = UNITY_ARRAY if num.empty? base = self.class.new(self.class.eliminate_terms(q, num, den)) self.class.base_unit_cache.set(units, base) base * @scalar end
converts the unit back to a complex if it is unitless. Otherwise raises an exception @return [Complex] @raise [RuntimeError] when not unitless
# File lib/ruby_units/unit.rb, line 1236 def to_c return Complex(@scalar) if unitless? raise "Cannot convert '#{self}' to Complex unless unitless. Use Unit#scalar" end
@return [Date]
# File lib/ruby_units/unit.rb, line 1408 def to_date Date.new0(convert_to("d").scalar) end
convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date defined by DateTime @return [::DateTime]
# File lib/ruby_units/unit.rb, line 1403 def to_datetime DateTime.new!(convert_to("d").scalar) end
converts the unit back to a float if it is unitless. Otherwise raises an exception @return [Float] @raise [RuntimeError] when not unitless
# File lib/ruby_units/unit.rb, line 1227 def to_f return @scalar.to_f if unitless? raise "Cannot convert '#{self}' to Float unless unitless. Use Unit#scalar" end
if unitless, returns an int, otherwise raises an error @return [Integer] @raise [RuntimeError] when not unitless
# File lib/ruby_units/unit.rb, line 1245 def to_i return @scalar.to_int if unitless? raise "Cannot convert '#{self}' to Integer unless unitless. Use Unit#scalar" end
if unitless, returns a Rational, otherwise raises an error @return [Rational] @raise [RuntimeError] when not unitless
# File lib/ruby_units/unit.rb, line 1256 def to_r return @scalar.to_r if unitless? raise "Cannot convert '#{self}' to Rational unless unitless. Use Unit#scalar" end
Generate human readable output. If the name of a unit is passed, the unit will first be converted to the target unit before output. some named conversions are available
@example
unit.to_s(:ft) - outputs in feet and inches (e.g., 6'4") unit.to_s(:lbs) - outputs in pounds and ounces (e.g, 8 lbs, 8 oz)
You can also pass a standard format string (i.e., ‘%0.2f’) or a strftime format string.
output is cached so subsequent calls for the same format will be fast
@note Rational scalars that are equal to an integer will be represented as integers (i.e, 6/1 => 6, 4/2 => 2, etc..) @param [Symbol] target_units @param [Float] precision - the precision to use when converting to a rational @param format [Symbol] Set to :exponential to force all units to be displayed in exponential format
@return [String]
# File lib/ruby_units/unit.rb, line 675 def to_s(target_units = nil, precision: 0.0001, format: RubyUnits.configuration.format) out = @output[target_units] return out if out separator = RubyUnits.configuration.separator case target_units when :ft feet, inches = convert_to("in").scalar.abs.divmod(12) improper, frac = inches.divmod(1) frac = frac.zero? ? "" : "-#{frac.rationalize(precision)}" out = "#{negative? ? '-' : nil}#{feet}'#{improper}#{frac}\"" when :lbs pounds, ounces = convert_to("oz").scalar.abs.divmod(16) improper, frac = ounces.divmod(1) frac = frac.zero? ? "" : "-#{frac.rationalize(precision)}" out = "#{negative? ? '-' : nil}#{pounds}#{separator}lbs #{improper}#{frac}#{separator}oz" when :stone stone, pounds = convert_to("lbs").scalar.abs.divmod(14) improper, frac = pounds.divmod(1) frac = frac.zero? ? "" : "-#{frac.rationalize(precision)}" out = "#{negative? ? '-' : nil}#{stone}#{separator}stone #{improper}#{frac}#{separator}lbs" when String out = case target_units.strip when /\A\s*\Z/ # whitespace only "" when /(%[-+.\w#]+)\s*(.+)*/ # format string like '%0.2f in' begin if Regexp.last_match(2) # unit specified, need to convert convert_to(Regexp.last_match(2)).to_s(Regexp.last_match(1), format: format) else "#{Regexp.last_match(1) % @scalar}#{separator}#{Regexp.last_match(2) || units(format: format)}".strip end rescue StandardError # parse it like a strftime format string (DateTime.new(0) + self).strftime(target_units) end when /(\S+)/ # unit only 'mm' or '1/mm' convert_to(Regexp.last_match(1)).to_s(format: format) else raise "unhandled case" end else out = case @scalar when Complex "#{@scalar}#{separator}#{units(format: format)}" when Rational "#{@scalar == @scalar.to_i ? @scalar.to_i : @scalar}#{separator}#{units(format: format)}" else "#{'%g' % @scalar}#{separator}#{units(format: format)}" end.strip end @output[target_units] = out out end
Tries to make a Time
object from current unit. Assumes the current unit hold the duration in seconds from the epoch. @return [Time]
# File lib/ruby_units/unit.rb, line 1394 def to_time Time.at(self) end
Convert the unit to a Unit
, possibly performing a conversion. > The ability to pass a Unit
to convert to was added in v3.0.0 for > consistency with other uses of to_unit
.
@param other [RubyUnits::Unit, String] unit to convert to @return [RubyUnits::Unit]
# File lib/ruby_units/unit.rb, line 583 def to_unit(other = nil) other ? convert_to(other) : self end
@return [Numeric, Unit]
# File lib/ruby_units/unit.rb, line 1364 def truncate(*args) return @scalar.truncate(*args) if unitless? self.class.new(@scalar.truncate(*args), @numerator, @denominator) end
returns true if no associated units false, even if the units are “unitless” like ‘radians, each, etc’ @return [Boolean]
# File lib/ruby_units/unit.rb, line 767 def unitless? @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY end
Returns the ‘unit’ part of the Unit
object without the scalar
@param with_prefix [Boolean] include prefixes in output @param format [Symbol] Set to :exponential to force all units to be displayed in exponential format
@return [String]
# File lib/ruby_units/unit.rb, line 1274 def units(with_prefix: true, format: nil) return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY output_numerator = ["1"] output_denominator = [] num = @numerator.clone.compact den = @denominator.clone.compact unless num == UNITY_ARRAY definitions = num.map { self.class.definition(_1) } definitions.reject!(&:prefix?) unless with_prefix definitions = definitions.chunk_while { |definition, _| definition.prefix? }.to_a output_numerator = definitions.map { _1.map(&:display_name).join } end unless den == UNITY_ARRAY definitions = den.map { self.class.definition(_1) } definitions.reject!(&:prefix?) unless with_prefix definitions = definitions.chunk_while { |definition, _| definition.prefix? }.to_a output_denominator = definitions.map { _1.map(&:display_name).join } end on = output_numerator .uniq .map { [_1, output_numerator.count(_1)] } .map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : "")) } if format == :exponential od = output_denominator .uniq .map { [_1, output_denominator.count(_1)] } .map { |element, power| (element.to_s.strip + (power.positive? ? "^#{-power}" : "")) } (on + od).join("*").strip else od = output_denominator .uniq .map { [_1, output_denominator.count(_1)] } .map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : "")) } "#{on.join('*')}#{od.empty? ? '' : "/#{od.join('*')}"}".strip end end
@example ‘min’.until(time) @param [Time, Date
, DateTime] time_point @return [Unit]
# File lib/ruby_units/unit.rb, line 1459 def until(time_point) case time_point when Time self.class.new(time_point - ::Time.now, "second").convert_to(self) when DateTime, Date self.class.new(time_point - ::DateTime.now, "day").convert_to(self) else raise ArgumentError, "Must specify a Time, Date, or DateTime" end end
true if scalar is zero @return [Boolean]
# File lib/ruby_units/unit.rb, line 1414 def zero? base_scalar.zero? end
Protected Instance Methods
calculates the unit signature vector used by unit_signature
@return [Array] @raise [ArgumentError] when exponent associated with a unit is > 20 or < -20
# File lib/ruby_units/unit.rb, line 1556 def unit_signature_vector return to_base.unit_signature_vector unless base? vector = ::Array.new(SIGNATURE_VECTOR.size, 0) # it's possible to have a kind that misses the array... kinds like :counting # are more like prefixes, so don't use them to calculate the vector @numerator.map { self.class.definition(_1) }.each do |definition| index = SIGNATURE_VECTOR.index(definition.kind) vector[index] += 1 if index end @denominator.map { self.class.definition(_1) }.each do |definition| index = SIGNATURE_VECTOR.index(definition.kind) vector[index] -= 1 if index end raise ArgumentError, "Power out of range (-20 < net power of a unit < 20)" if vector.any? { _1.abs >= 20 } vector end
figure out what the scalar part of the base unit for this unit is @return [nil]
# File lib/ruby_units/unit.rb, line 1542 def update_base_scalar if base? @base_scalar = @scalar @signature = unit_signature else base = to_base @base_scalar = base.scalar @signature = base.signature end end
Private Instance Methods
used by dup to duplicate a Unit
@param [Unit] other @private
# File lib/ruby_units/unit.rb, line 1580 def initialize_copy(other) @numerator = other.numerator.dup @denominator = other.denominator.dup end
parse a string into a unit object. Typical formats like :
"5.6 kg*m/s^2" "5.6 kg*m*s^-2" "5.6 kilogram*meter*second^-2" "2.2 kPa" "37 degC" "1" -- creates a unitless constant with value 1 "GPa" -- creates a unit with scalar 1 with units 'GPa' 6'4" -- recognized as 6 feet + 4 inches 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces
@return [nil,RubyUnits::Unit] @todo This should either be a separate class or at least a class method
# File lib/ruby_units/unit.rb, line 1613 def parse(passed_unit_string = "0") unit_string = passed_unit_string.dup unit_string = "#{Regexp.last_match(1)} USD" if unit_string =~ /\$\s*(#{NUMBER_REGEX})/ unit_string.gsub!("\u00b0".encode("utf-8"), "deg") if unit_string.encoding == Encoding::UTF_8 unit_string.gsub!(/(\d)[_,](\d)/, '\1\2') # remove underscores and commas in numbers unit_string.gsub!(/[%'"#]/, "%" => "percent", "'" => "feet", '"' => "inch", "#" => "pound") if unit_string.start_with?(COMPLEX_NUMBER) match = unit_string.match(COMPLEX_REGEX) real = Float(match[:real]) if match[:real] imaginary = Float(match[:imaginary]) unit_s = match[:unit] real = real.to_i if real.to_i == real imaginary = imaginary.to_i if imaginary.to_i == imaginary complex = Complex(real || 0, imaginary) complex = complex.to_i if complex.imaginary.zero? && complex.real == complex.real.to_i result = self.class.new(unit_s || 1) * complex copy(result) return end if unit_string.start_with?(RATIONAL_NUMBER) match = unit_string.match(RATIONAL_REGEX) numerator = Integer(match[:numerator]) denominator = Integer(match[:denominator]) raise ArgumentError, "Improper fractions must have a whole number part" if !match[:proper].nil? && !match[:proper].match?(/^#{INTEGER_REGEX}$/) proper = match[:proper].to_i unit_s = match[:unit] rational = if proper.negative? (proper - Rational(numerator, denominator)) else (proper + Rational(numerator, denominator)) end rational = rational.to_int if rational.to_int == rational result = self.class.new(unit_s || 1) * rational copy(result) return end match = unit_string.match(NUMBER_REGEX) unit = self.class.cached.get(match[:unit]) mult = match[:scalar] == "" ? 1.0 : match[:scalar].to_f mult = mult.to_int if mult.to_int == mult if unit copy(unit) @scalar *= mult @base_scalar *= mult return self end while unit_string.gsub!(/<(#{self.class.prefix_regex})><(#{self.class.unit_regex})>/, '<\1\2>') # replace <prefix><unit> with <prefixunit> end while unit_string.gsub!(/<#{self.class.unit_match_regex}><#{self.class.unit_match_regex}>/, '<\1\2>*<\3\4>') # collapse <prefixunit><prefixunit> into <prefixunit>*<prefixunit>... end # ... and then strip the remaining brackets for x*y*z unit_string.gsub!(/[<>]/, "") if (match = unit_string.match(TIME_REGEX)) hours = match[:hour] minutes = match[:min] seconds = match[:sec] milliseconds = match[:msec] raise ArgumentError, "Invalid Duration" if [hours, minutes, seconds, milliseconds].all?(&:nil?) result = self.class.new("#{hours || 0} hours") + self.class.new("#{minutes || 0} minutes") + self.class.new("#{seconds || 0} seconds") + self.class.new("#{milliseconds || 0} milliseconds") copy(result) return end # Special processing for unusual unit strings # feet -- 6'5" if (match = unit_string.match(FEET_INCH_REGEX)) feet = Integer(match[:feet]) inches = match[:inches] result = if feet.negative? self.class.new("#{feet} ft") - self.class.new("#{inches} inches") else self.class.new("#{feet} ft") + self.class.new("#{inches} inches") end copy(result) return end # weight -- 8 lbs 12 oz if (match = unit_string.match(LBS_OZ_REGEX)) pounds = Integer(match[:pounds]) oz = match[:oz] result = if pounds.negative? self.class.new("#{pounds} lbs") - self.class.new("#{oz} oz") else self.class.new("#{pounds} lbs") + self.class.new("#{oz} oz") end copy(result) return end # stone -- 3 stone 5, 2 stone, 14 stone 3 pounds, etc. if (match = unit_string.match(STONE_LB_REGEX)) stone = Integer(match[:stone]) pounds = match[:pounds] result = if stone.negative? self.class.new("#{stone} stone") - self.class.new("#{pounds} lbs") else self.class.new("#{stone} stone") + self.class.new("#{pounds} lbs") end copy(result) return end # more than one per. I.e., "1 m/s/s" raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count("/") > 1 raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized #{unit_string}") if unit_string =~ /\s[02-9]/ @scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] # parse the string into parts top.scan(TOP_REGEX).each do |item| n = item[1].to_i x = "#{item[0]} " if n >= 0 top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) { x * n } elsif n.negative? bottom = "#{bottom} #{x * -n}" top.gsub!(/#{item[0]}(\^|\*\*)#{n}/, "") end end if bottom bottom.gsub!(BOTTOM_REGEX) { "#{Regexp.last_match(1)} " * Regexp.last_match(2).to_i } # Separate leading decimal from denominator, if any bottom_scalar, bottom = bottom.scan(NUMBER_UNIT_REGEX)[0] end @scalar = @scalar.to_f unless @scalar.nil? || @scalar.empty? @scalar = 1 unless @scalar.is_a? Numeric @scalar = @scalar.to_int if @scalar.to_int == @scalar bottom_scalar = 1 if bottom_scalar.nil? || bottom_scalar.empty? bottom_scalar = if bottom_scalar.to_i == bottom_scalar bottom_scalar.to_i else bottom_scalar.to_f end @scalar /= bottom_scalar @numerator ||= UNITY_ARRAY @denominator ||= UNITY_ARRAY @numerator = top.scan(self.class.unit_match_regex).delete_if(&:empty?).compact if top @denominator = bottom.scan(self.class.unit_match_regex).delete_if(&:empty?).compact if bottom # eliminate all known terms from this string. This is a quick check to see if the passed unit # contains terms that are not defined. used = "#{top} #{bottom}".to_s.gsub(self.class.unit_match_regex, "").gsub(%r{[\d*, "'_^/$]}, "") raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless used.empty? @numerator = @numerator.map do |item| self.class.prefix_map[item[0]] ? [self.class.prefix_map[item[0]], self.class.unit_map[item[1]]] : [self.class.unit_map[item[1]]] end.flatten.compact.delete_if(&:empty?) @denominator = @denominator.map do |item| self.class.prefix_map[item[0]] ? [self.class.prefix_map[item[0]], self.class.unit_map[item[1]]] : [self.class.unit_map[item[1]]] end.flatten.compact.delete_if(&:empty?) @numerator = UNITY_ARRAY if @numerator.empty? @denominator = UNITY_ARRAY if @denominator.empty? self end
calculates the unit signature id for use in comparing compatible units and simplification the signature is based on a simple classification of units and is based on the following publication
Novak, G.S., Jr. “Conversion of units of measurement”, IEEE Transactions on Software Engineering, 21(8), Aug 1995, pp.651-661 @see doi.ieeecomputersociety.org/10.1109/32.403789 @return [Array]
# File lib/ruby_units/unit.rb, line 1591 def unit_signature return @signature unless @signature.nil? vector = unit_signature_vector vector.each_with_index { |item, index| vector[index] = item * (20**index) } @signature = vector.inject(0) { |acc, elem| acc + elem } @signature end