Class: RubyUnits::Unit

Inherits:
Numeric show all
Includes:
Comparable
Defined in:
lib/ruby_units/unit.rb,
lib/ruby_units/version.rb,
lib/ruby_units/definition.rb

Overview

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.

Copyright 2006-2024 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..

Examples:

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

See Also:

Author:

  • Kevin C. Olbrich, Ph.D.

Defined Under Namespace

Classes: Definition

Constant Summary collapse

UNITY =
"<1>"
UNITY_ARRAY =
[UNITY].freeze
SIGN_REGEX =

+, -, or nothing

/(?:[+-])?/.freeze
INTEGER_DIGITS_REGEX =

regex for matching an integer number but not a fraction

%r{(?<!/)\d+(?!/)}.freeze
INTEGER_REGEX =

-1, 1, +1, but not 1/2

/(#{SIGN_REGEX}#{INTEGER_DIGITS_REGEX})/.freeze
UNSIGNED_INTEGER_REGEX =

1, 2, 3, but not -1

/((?<!-)#{INTEGER_DIGITS_REGEX})/.freeze
DIGITS_REGEX =

0, 1, 2, 3

/\d+/.freeze
DECIMAL_REGEX =

1, 0.1, .1

/\d*[.]?#{DIGITS_REGEX}/.freeze
RATIONAL_NUMBER =

Rational number, including improper fractions: 1 2/3, -1 2/3, 5/3, etc.

%r{\(?(?:(?<proper>#{SIGN_REGEX}#{DECIMAL_REGEX})[ -])?(?<numerator>#{SIGN_REGEX}#{DECIMAL_REGEX})/(?<denominator>#{SIGN_REGEX}#{DECIMAL_REGEX})\)?}.freeze
SCI_NUMBER =

Scientific notation: 1, -1, 1, 1.2, 1.2, -1.2, 123.4E5, +123.4e5,

-123.4E+5, -123.4e-5, etc.
/([+-]?\d*[.]?\d+(?:[Ee][+-]?\d+(?![.]))?)/.freeze
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.

/(?:'|ft|feet)\s*(?<inches>#{RATIONAL_NUMBER}|#{SCI_NUMBER})\s*(?:"|in|inch(?:es)?)/.freeze
FEET_INCH_REGEX =
/(?<feet>#{INTEGER_REGEX})\s*#{FEET_INCH_UNITS_REGEX}/.freeze
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.

/(?:#|lbs?|pounds?|pound-mass)+[\s,]*(?<oz>#{RATIONAL_NUMBER}|#{UNSIGNED_INTEGER_REGEX})\s*(?:ozs?|ounces?)/.freeze
LBS_OZ_REGEX =
/(?<pounds>#{INTEGER_REGEX})\s*#{LBS_OZ_UNIT_REGEX}/.freeze
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.

/(?:sts?|stones?)+[\s,]*(?<pounds>#{RATIONAL_NUMBER}|#{UNSIGNED_INTEGER_REGEX})\s*(?:#|lbs?|pounds?|pound-mass)*/.freeze
STONE_LB_REGEX =
/(?<stone>#{INTEGER_REGEX})\s*#{STONE_LB_UNIT_REGEX}/.freeze
TIME_REGEX =

Time formats: 12:34:56,78, (hh:mm:ss,msec) etc.

/(?<hour>\d+):(?<min>\d+):?(?:(?<sec>\d+))?(?:[.](?<msec>\d+))?/.freeze
COMPLEX_NUMBER =

Complex numbers: 1+2i, 1.0+2.0i, -1-1i, etc.

/(?<real>#{SCI_NUMBER})?(?<imaginary>#{SCI_NUMBER})i\b/.freeze
ANY_NUMBER =

Any Complex, Rational, or scientific number

/(#{COMPLEX_NUMBER}|#{RATIONAL_NUMBER}|#{SCI_NUMBER})/.freeze
ANY_NUMBER_REGEX =
/(?:#{ANY_NUMBER})?\s?([^-\d.].*)?/.freeze
NUMBER_REGEX =

a number followed by a unit

/(?<scalar>#{SCI_NUMBER}*)\s*(?<unit>.+)?/.freeze
UNIT_STRING_REGEX =
%r{#{SCI_NUMBER}*\s*([^/]*)/*(.+)*}.freeze
TOP_REGEX =
/([^ *]+)(?:\^|\*\*)([\d-]+)/.freeze
BOTTOM_REGEX =
/([^* ]+)(?:\^|\*\*)(\d+)/.freeze
NUMBER_UNIT_REGEX =
/#{SCI_NUMBER}?(.*)/.freeze
COMPLEX_REGEX =
/#{COMPLEX_NUMBER}\s?(?<unit>.+)?/.freeze
RATIONAL_REGEX =
/#{RATIONAL_NUMBER}\s?(?<unit>.+)?/.freeze
KELVIN =
["<kelvin>"].freeze
FAHRENHEIT =
["<fahrenheit>"].freeze
RANKINE =
["<rankine>"].freeze
CELSIUS =
["<celsius>"].freeze
SIGNATURE_VECTOR =
%i[
  length
  time
  temperature
  mass
  current
  substance
  luminosity
  currency
  information
  angle
].freeze
VERSION =
"4.1.0"

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*options) ⇒ Unit

Create a new Unit object. Can be initialized using a String, a Hash, an Array, Time, DateTime

Examples:

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'}

Parameters:

Raises:

  • (ArgumentError)

    if absolute value of a temperature is less than absolute zero

  • (ArgumentError)

    if no unit is specified

  • (ArgumentError)

    if an invalid unit is specified



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
# 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?(.+)?|&plusmn;|\+/-})) && (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

Class Attribute Details

.definitionsHash{Symbol=>RubyUnits::Units::Definition}

return a list of all defined units

Returns:

  • (Hash{Symbol=>RubyUnits::Units::Definition})


30
31
32
# File 'lib/ruby_units/unit.rb', line 30

def definitions
  @definitions
end

.kindsHash{Integer => Symbol} (readonly)

Returns:

  • (Hash{Integer => Symbol})


45
46
47
# File 'lib/ruby_units/unit.rb', line 45

def kinds
  @kinds
end

.prefix_mapHash{Symbol => String}

Returns:



36
37
38
# File 'lib/ruby_units/unit.rb', line 36

def prefix_map
  @prefix_map
end

.prefix_valuesHash{Symbol => String}

Returns the list of units and their prefixes.

Returns:

  • (Hash{Symbol => String})

    the list of units and their prefixes



33
34
35
# File 'lib/ruby_units/unit.rb', line 33

def prefix_values
  @prefix_values
end

.unit_mapHash{Symbol => String}

Returns:



39
40
41
# File 'lib/ruby_units/unit.rb', line 39

def unit_map
  @unit_map
end

.unit_valuesHash{Symbol => String}

Returns:



42
43
44
# File 'lib/ruby_units/unit.rb', line 42

def unit_values
  @unit_values
end

Instance Attribute Details

#base_denominatorArray

Returns:



448
449
450
# File 'lib/ruby_units/unit.rb', line 448

def base_denominator
  @base_denominator
end

#base_numeratorArray

Returns:



445
446
447
# File 'lib/ruby_units/unit.rb', line 445

def base_numerator
  @base_numerator
end

#base_scalarNumeric

Returns:



442
443
444
# File 'lib/ruby_units/unit.rb', line 442

def base_scalar
  @base_scalar
end

#denominatorArray

Returns:



436
437
438
# File 'lib/ruby_units/unit.rb', line 436

def denominator
  @denominator
end

#numeratorArray

Returns:



433
434
435
# File 'lib/ruby_units/unit.rb', line 433

def numerator
  @numerator
end

#outputString

Returns:



451
452
453
# File 'lib/ruby_units/unit.rb', line 451

def output
  @output
end

#scalarNumeric

Returns:



430
431
432
# File 'lib/ruby_units/unit.rb', line 430

def scalar
  @scalar
end

#signatureInteger

Returns:

  • (Integer)


439
440
441
# File 'lib/ruby_units/unit.rb', line 439

def signature
  @signature
end

#unit_nameString

Returns:



454
455
456
# File 'lib/ruby_units/unit.rb', line 454

def unit_name
  @unit_name
end

Class Method Details

.base_unit_cacheRubyUnits::Cache

Returns:



286
287
288
# File 'lib/ruby_units/unit.rb', line 286

def self.base_unit_cache
  @base_unit_cache ||= RubyUnits::Cache.new
end

.base_unitsArray

return an array of base units

Returns:



340
341
342
# 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

.cachedRubyUnits::Cache

Unit cache

Returns:



273
274
275
# File 'lib/ruby_units/unit.rb', line 273

def self.cached
  @cached ||= RubyUnits::Cache.new
end

.clear_cacheBoolean

Returns:

  • (Boolean)


278
279
280
281
282
283
# File 'lib/ruby_units/unit.rb', line 278

def self.clear_cache
  cached.clear
  base_unit_cache.clear
  new(1)
  true
end

.define(unit_definition, &block) ⇒ RubyUnits::Unit::Definition

Unpack a unit definition and add it to the array of defined units

Examples:

Block form

RubyUnits::Unit.define('foobar') do |foobar|
  foobar.definition = RubyUnits::Unit.new("1 baz")
end

RubyUnits::Unit::Definition form

unit_definition = RubyUnits::Unit::Definition.new("foobar") {|foobar| foobar.definition = RubyUnits::Unit.new("1 baz")}
RubyUnits::Unit.define(unit_definition)

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when passed a non-string if using the block form



230
231
232
233
234
235
236
237
238
239
# 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

.defined?(unit) ⇒ Boolean

determine if a unit is already defined

Parameters:

Returns:

  • (Boolean)


204
205
206
# File 'lib/ruby_units/unit.rb', line 204

def self.defined?(unit)
  definitions.values.any? { _1.aliases.include?(unit) }
end

.definition(unit_name) ⇒ RubyUnits::Unit::Definition?

return the unit definition for a unit

Parameters:

Returns:



211
212
213
214
# File 'lib/ruby_units/unit.rb', line 211

def self.definition(unit_name)
  unit = unit_name =~ /^<.+>$/ ? unit_name : "<#{unit_name}>"
  definitions[unit]
end

.eliminate_terms(q, n, d) ⇒ Hash

Parameters:

Returns:

  • (Hash)


303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# 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

.inherited(subclass) ⇒ Object

Callback triggered when a subclass is created. This properly sets up the internal variables, and copies definitions from the parent class.

Parameters:

  • subclass (Class)


174
175
176
177
178
179
# 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

.parse(input) ⇒ Unit

Examples:

parse strings

"1 minute in seconds"

Parameters:

Returns:



294
295
296
297
# 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_into_numbers_and_units(string) ⇒ Array(Numeric, String)

Parse a string consisting of a number and a unit string NOTE: This does not properly handle units formatted like ‘12mg/6ml’

Parameters:

Returns:



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# 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

.prefix_regexString

return a regexp fragment used to match prefixes

Returns:



388
389
390
# 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

.redefine!(name, &_block) {|the| ... } ⇒ RubyUnits::Unit::Definition

Get the definition for a unit and allow it to be redefined

Parameters:

  • name (String)

    Name of unit to redefine

  • _block (Proc)

Yield Parameters:

Returns:

Raises:

  • (ArgumentError)

    if a block is not given



249
250
251
252
253
254
255
256
257
258
259
# 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

.setupBoolean

setup internal arrays and hashes

Returns:

  • (Boolean)


183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# 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

.temp_regexRegexp

Generates (and memoizes) a regexp matching any of the temperature units or their aliases.

Returns:

  • (Regexp)


395
396
397
398
399
400
401
402
403
404
405
# 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!(unit) ⇒ Boolean

Undefine a unit. Will not raise an exception for unknown units.

Parameters:

  • unit (String)

    name of unit to undefine

Returns:

  • (Boolean)


265
266
267
268
# File 'lib/ruby_units/unit.rb', line 265

def self.undefine!(unit)
  definitions.delete("<#{unit}>")
  setup
end

.unit_match_regexRegexp

return a regex used to match units

Returns:

  • (Regexp)


381
382
383
# File 'lib/ruby_units/unit.rb', line 381

def self.unit_match_regex
  @unit_match_regex ||= /(#{prefix_regex})??(#{unit_regex})\b/
end

.unit_regexString

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

Returns:



375
376
377
# 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

.use_definition(definition) ⇒ Object

inject a definition into the internal array and set it up for use

Parameters:



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# 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

Instance Method Details

#%(other) ⇒ Integer Also known as: modulo

Perform a modulo on a unit, will raise an exception if the units are not compatible

Parameters:

Returns:

  • (Integer)

Raises:

  • (ArgumentError)

    if units are not compatible



1010
1011
1012
1013
1014
# 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

#*(other) ⇒ Unit

Multiply two units.

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when attempting to multiply two temperatures



938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
# 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

#**(other) ⇒ Unit

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

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when raising a temperature to a power

  • (ArgumentError)

    when n not in the set integers from (1..9)

  • (ArgumentError)

    when attempting to raise to a complex number

  • (ArgumentError)

    when an invalid exponent is passed



1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
# 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

#+(other) ⇒ Unit

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

Parameters:

  • other (Object)

Returns:

Raises:

  • (ArgumentError)

    when two temperatures are added

  • (ArgumentError)

    when units are not compatible

  • (ArgumentError)

    when adding a fixed time or date to a time span



872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
# 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

#-(other) ⇒ Unit

Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when subtracting a temperature from a degree

  • (ArgumentError)

    when units are not compatible

  • (ArgumentError)

    when subtracting a fixed time from a time span



904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
# 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

#-@Numeric, Unit

negates the scalar of the Unit

Returns:



1318
1319
1320
1321
1322
# File 'lib/ruby_units/unit.rb', line 1318

def -@
  return -@scalar if unitless?

  dup * -1
end

#/(other) ⇒ Unit

Divide two units. Throws an exception if divisor is 0

Parameters:

Returns:

Raises:

  • (ZeroDivisionError)

    if divisor is zero

  • (ArgumentError)

    if attempting to divide a temperature by another temperature



960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
# 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

#<=>(other) ⇒ Integer?

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.

Parameters:

  • other (Object)

Returns:

  • (Integer, nil)

Raises:

  • (NoMethodError)

    when other does not define <=>

  • (ArgumentError)

    when units are not compatible



777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
# 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

#==(other) ⇒ Boolean

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.

Parameters:

  • other (Object)

Returns:

  • (Boolean)


802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
# 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

#===(other) ⇒ Boolean Also known as: same?, same_as?

Compare two units. Returns true if quantities and units match

Examples:

RubyUnits::Unit.new("100 cm") === RubyUnits::Unit.new("100 cm")   # => true
RubyUnits::Unit.new("100 cm") === RubyUnits::Unit.new("1 m")      # => false

Parameters:

  • other (Object)

Returns:

  • (Boolean)


847
848
849
850
851
852
853
854
855
856
857
858
859
# 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

#=~(other) ⇒ Boolean Also known as: compatible?, compatible_with?

Note:

if you want to do a regexp comparison of the unit string do this … unit.units =~ /regexp/

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.

Examples:

this permits a syntax like:

unit =~ "mm"

Parameters:

  • other (Object)

Returns:

  • (Boolean)


829
830
831
832
833
834
835
836
# 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

#absNumeric, Unit

absolute value of a unit

Returns:



1326
1327
1328
1329
1330
# File 'lib/ruby_units/unit.rb', line 1326

def abs
  return @scalar.abs if unitless?

  self.class.new(@scalar.abs, @numerator, @denominator)
end

#agoUnit

Examples:

‘5 min’.to_unit.ago

Returns:



1420
1421
1422
# File 'lib/ruby_units/unit.rb', line 1420

def ago
  before
end

#as_jsonString

Returns string formatted for json

Returns:



1264
1265
1266
# File 'lib/ruby_units/unit.rb', line 1264

def as_json(*)
  to_s
end

#base?Boolean Also known as: is_base?

Is this unit in base form?

Returns:

  • (Boolean)


591
592
593
594
595
596
597
598
599
600
# 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

#before(time_point = ::Time.now) ⇒ Unit Also known as: before_now

Examples:

‘5 min’.before(time)

Returns:



1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
# 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

#best_prefixUnit

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

Returns:



1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
# 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(*args) ⇒ Numeric, Unit

ceil of a unit

Returns:



1334
1335
1336
1337
1338
# 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

#coerce(other) ⇒ Array(Unit, Unit)

Automatically coerce objects to [Unit] when possible. If an object defines a ‘#to_unit’ method, it will be coerced using that method.

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when ‘other` cannot be converted to a [Unit]



1496
1497
1498
1499
1500
# 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(other) ⇒ Unit Also known as: >>, to

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.

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.

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when attempting to convert a degree to a temperature

  • (ArgumentError)

    when target unit is unknown

  • (ArgumentError)

    when target unit is incompatible



1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
# 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

#copy(from) ⇒ RubyUnits::Unit

Used to copy one unit to another

Parameters:

Returns:



459
460
461
462
463
464
465
466
467
468
# 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

#degree?Boolean Also known as: is_degree?

true if a degree unit or equivalent.

Returns:

  • (Boolean)


749
750
751
# File 'lib/ruby_units/unit.rb', line 749

def degree?
  kind == :temperature
end

#divmod(other) ⇒ Array(Integer, Unit)

Divide two units and return quotient and remainder

Parameters:

Returns:

Raises:

  • (ArgumentError)

    if units are not compatible



999
1000
1001
1002
1003
# 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

#eliminate_termsRubyUnits::Unit

Creates a new unit from the current one with all common terms eliminated.

Returns:



334
335
336
# File 'lib/ruby_units/unit.rb', line 334

def eliminate_terms
  self.class.new(self.class.eliminate_terms(@scalar, @numerator, @denominator))
end

#floor(*args) ⇒ Numeric, Unit

Returns:



1341
1342
1343
1344
1345
# 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

#from(time_point) ⇒ Time, ... Also known as: after, from_now

Examples:

‘5 min’.from(time)

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when passed argument is not a Time, Date, or DateTime



1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
# 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

#hashObject

override hash method so objects with same values are considered equal



1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
# File 'lib/ruby_units/unit.rb', line 1525

def hash
  [
    @scalar,
    @numerator,
    @denominator,
    @base,
    @signature,
    @base_scalar,
    @unit_name
  ].hash
end

#inspect(dump = nil) ⇒ String

Deprecated.

Normally pretty prints the unit, but if you really want to see the guts of it, pass ‘:dump’

Returns:



732
733
734
735
736
# File 'lib/ruby_units/unit.rb', line 732

def inspect(dump = nil)
  return super() if dump

  to_s
end

#inverseUnit

returns inverse of Unit (1/unit)

Returns:



1119
1120
1121
# File 'lib/ruby_units/unit.rb', line 1119

def inverse
  self.class.new("1") / self
end

#kindSymbol

@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…)

Returns:

  • (Symbol)


573
574
575
# File 'lib/ruby_units/unit.rb', line 573

def kind
  self.class.kinds[signature]
end

#power(n) ⇒ Unit

returns the unit raised to the n-th power

Parameters:

  • n (Integer)

Returns:

Raises:

  • (ArgumentError)

    when attempting to raise a temperature to a power

  • (ArgumentError)

    when n is not an integer



1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
# 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

#predUnit

returns previous unit in a range. ‘2 mm’.to_unit.pred #=> ‘1 mm’.to_unit only works when the scalar is an integer

Returns:

Raises:

  • (ArgumentError)

    when scalar is not equal to an integer



1386
1387
1388
1389
1390
# 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

#quo(other) ⇒ Unit Also known as: fdiv

Parameters:

  • other (Object)

Returns:

Raises:

  • (ZeroDivisionError)

    if other is zero



1020
1021
1022
# File 'lib/ruby_units/unit.rb', line 1020

def quo(other)
  self / other
end

#remainder(other) ⇒ Unit

Returns the remainder when one unit is divided by another

Parameters:

Returns:

Raises:

  • (ArgumentError)

    if units are not compatible



988
989
990
991
992
# 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

#root(n) ⇒ Unit

Calculates the n-th root of a unit if n < 0, returns 1/unit^(1/n)

Parameters:

  • n (Integer)

Returns:

Raises:

  • (ArgumentError)

    when attempting to take the root of a temperature

  • (ArgumentError)

    when n is not an integer

  • (ArgumentError)

    when n is 0



1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
# 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(*args, **kwargs) ⇒ Numeric, Unit

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.

Examples:

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

Returns:



1357
1358
1359
1360
1361
# 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

#since(time_point) ⇒ Unit

Examples:

‘min’.since(time)

Parameters:

Returns:

Raises:

  • (ArgumentError)

    when time point is not a Time, Date, or DateTime



1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
# 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

#succUnit Also known as: next

returns next unit in a range. ‘1 mm’.to_unit.succ #=> ‘2 mm’.to_unit only works when the scalar is an integer

Returns:

Raises:

  • (ArgumentError)

    when scalar is not equal to an integer



1374
1375
1376
1377
1378
# 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

#temperature?Boolean Also known as: is_temperature?

TODO:

use unit definition to determine if it’s a temperature instead of a regex

true if unit is a ‘temperature’, false if a ‘degree’ or anything else

Returns:

  • (Boolean)


741
742
743
# File 'lib/ruby_units/unit.rb', line 741

def temperature?
  degree? && units.match?(self.class.temp_regex)
end

#temperature_scaleString

returns the ‘degree’ unit associated with a temperature unit

Examples:

‘100 tempC’.to_unit.temperature_scale #=> ‘degC’

Returns:

  • (String)

    possible values: degC, degF, degR, or degK



758
759
760
761
762
# 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

#to_baseUnit Also known as: base

TODO:

this is brittle as it depends on the display_name of a unit, which can be changed

convert to base SI units results of the conversion are cached so subsequent calls to this will be fast

Returns:



608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
# 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

#to_cComplex

converts the unit back to a complex if it is unitless. Otherwise raises an exception

Returns:

  • (Complex)

Raises:

  • (RuntimeError)

    when not unitless



1236
1237
1238
1239
1240
# 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

#to_dateDate

Returns:



1408
1409
1410
# File 'lib/ruby_units/unit.rb', line 1408

def to_date
  Date.new0(convert_to("d").scalar)
end

#to_datetime::DateTime

convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date defined by DateTime

Returns:

  • (::DateTime)


1403
1404
1405
# File 'lib/ruby_units/unit.rb', line 1403

def to_datetime
  DateTime.new!(convert_to("d").scalar)
end

#to_fFloat

converts the unit back to a float if it is unitless. Otherwise raises an exception

Returns:

  • (Float)

Raises:

  • (RuntimeError)

    when not unitless



1227
1228
1229
1230
1231
# 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

#to_iInteger Also known as: to_int

if unitless, returns an int, otherwise raises an error

Returns:

  • (Integer)

Raises:

  • (RuntimeError)

    when not unitless



1245
1246
1247
1248
1249
# 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

#to_rRational

if unitless, returns a Rational, otherwise raises an error

Returns:

  • (Rational)

Raises:

  • (RuntimeError)

    when not unitless



1256
1257
1258
1259
1260
# 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

#to_s(target_units = nil, precision: 0.0001, format: RubyUnits.configuration.format) ⇒ String

Note:

Rational scalars that are equal to an integer will be represented as integers (i.e, 6/1 => 6, 4/2 => 2, etc..)

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

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

Examples:

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)

Parameters:

  • target_units (Symbol) (defaults to: nil)
  • precision (Float) (defaults to: 0.0001)
    • the precision to use when converting to a rational

  • format (Symbol) (defaults to: RubyUnits.configuration.format)

    Set to :exponential to force all units to be displayed in exponential format

Returns:



675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
# 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

#to_timeTime Also known as: time

Tries to make a Time object from current unit. Assumes the current unit hold the duration in seconds from the epoch.

Returns:



1394
1395
1396
# File 'lib/ruby_units/unit.rb', line 1394

def to_time
  Time.at(self)
end

#to_unit(other = nil) ⇒ RubyUnits::Unit Also known as: unit

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.

Parameters:

Returns:



583
584
585
# File 'lib/ruby_units/unit.rb', line 583

def to_unit(other = nil)
  other ? convert_to(other) : self
end

#truncate(*args) ⇒ Numeric, Unit

Returns:



1364
1365
1366
1367
1368
# 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

#unitless?Boolean

returns true if no associated units false, even if the units are “unitless” like ‘radians, each, etc’

Returns:

  • (Boolean)


767
768
769
# File 'lib/ruby_units/unit.rb', line 767

def unitless?
  @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY
end

#units(with_prefix: true, format: nil) ⇒ String

Returns the ‘unit’ part of the Unit object without the scalar

Parameters:

  • with_prefix (Boolean) (defaults to: true)

    include prefixes in output

  • format (Symbol) (defaults to: nil)

    Set to :exponential to force all units to be displayed in exponential format

Returns:



1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
# 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

#until(time_point) ⇒ Unit

Examples:

‘min’.until(time)

Parameters:

Returns:



1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
# 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

#zero?Boolean

true if scalar is zero

Returns:

  • (Boolean)


1414
1415
1416
# File 'lib/ruby_units/unit.rb', line 1414

def zero?
  base_scalar.zero?
end